From fd7035416082c189725e3124e0ce2eb6b84403a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Villase=C3=B1or=20Montfort?= <195970+montfort@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:13:58 -0600 Subject: [PATCH 1/2] fix(blog): list "What the follow-up took for granted" in /blog (EN + es + zh-CN) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The blog is a docs-instance (#340), so the /blog listing is maintained by hand — not auto-generated. The published post rendered at its URL but was missing from the index in all three locales. Add it to blogSidebars.ts (shared sidebar) and to the three index.mdx landing pages, newest-first. Build verified: post now appears in build/{,es/,zh-CN/}blog.html; 0 broken links. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/blog/index.mdx | 6 ++++++ website/blogSidebars.ts | 1 + .../docusaurus-plugin-content-docs-blog/current/index.mdx | 6 ++++++ .../docusaurus-plugin-content-docs-blog/current/index.mdx | 6 ++++++ 4 files changed, 19 insertions(+) diff --git a/website/blog/index.mdx b/website/blog/index.mdx index abfc8688..73aee397 100644 --- a/website/blog/index.mdx +++ b/website/blog/index.mdx @@ -12,6 +12,12 @@ The StrayMark chronicle — how the framework emerged, decision by decision. ## 2026 +### [What the follow-up took for granted](./2026-07-18-what-the-follow-up-took-for-granted.md) + +2026-07-18 + +A greenfield adopter drained a follow-up backlog before shipping. Of the three entries that were real work, every one rested on a premise that was false the moment you checked it — a test to replicate that never existed, a gate against a reference that can't exist, an optimization for a cost that wasn't there. Not because the code drifted, but because a follow-up is written when you're least able to verify it. The fix reframes entries as dated hypotheses and moves the check to execution — a new `verify` verb, a `Premise` field, and a re-check at promote time. + ### [What a million assertions can't settle](./2026-07-10-what-a-million-assertions-cant-settle.md) 2026-07-10 diff --git a/website/blogSidebars.ts b/website/blogSidebars.ts index ee69a34c..db05f0fd 100644 --- a/website/blogSidebars.ts +++ b/website/blogSidebars.ts @@ -13,6 +13,7 @@ const sidebars: SidebarsConfig = { collapsible: false, collapsed: false, items: [ + '2026-07-18-what-the-follow-up-took-for-granted', '2026-07-10-what-a-million-assertions-cant-settle', '2026-07-08-who-the-audit-thought-it-was', '2026-06-28-what-the-author-already-knew', diff --git a/website/i18n/es/docusaurus-plugin-content-docs-blog/current/index.mdx b/website/i18n/es/docusaurus-plugin-content-docs-blog/current/index.mdx index 817fdec1..39f3b14c 100644 --- a/website/i18n/es/docusaurus-plugin-content-docs-blog/current/index.mdx +++ b/website/i18n/es/docusaurus-plugin-content-docs-blog/current/index.mdx @@ -12,6 +12,12 @@ La crónica de StrayMark: cómo emergió el framework, decisión por decisión. ## 2026 +### [Lo que el follow-up dio por sentado](./2026-07-18-what-the-follow-up-took-for-granted.md) + +2026-07-18 + +Un adopter greenfield drenó un backlog de follow-ups antes de publicar. De las tres entradas que eran trabajo real, cada una descansaba sobre una premisa falsa en el momento en que la chequeabas — un test a replicar que nunca existió, una gate contra una referencia que no puede existir, una optimización para un coste que no estaba ahí. No porque el código derivara, sino porque un follow-up se escribe cuando menos puedes verificarlo. El fix reencuadra las entradas como hipótesis fechadas y mueve el chequeo a la ejecución — un nuevo verbo `verify`, un campo `Premise`, y un re-chequeo al promover. + ### [Lo que un millón de aserciones no puede zanjar](./2026-07-10-what-a-million-assertions-cant-settle.md) 2026-07-10 diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs-blog/current/index.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs-blog/current/index.mdx index 0abd50ac..f0edf203 100644 --- a/website/i18n/zh-CN/docusaurus-plugin-content-docs-blog/current/index.mdx +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs-blog/current/index.mdx @@ -12,6 +12,12 @@ StrayMark 编年史:这个框架如何随着一次次决策逐步成形。 ## 2026 +### [follow-up 想当然的那件事](./2026-07-18-what-the-follow-up-took-for-granted.md) + +2026-07-18 + +一个 greenfield adopter 在发布前清空了一个 follow-up backlog。在那三个真正是工作的条目里,每一个都建立在一个你一核查就为假的前提之上 —— 一个要复制却从不存在的测试、一个针对无法存在的参照物的 gate、一个针对根本不在那里的成本的优化。不是因为代码漂移了,而是因为一个 follow-up 恰恰写于你最无法验证它的时候。修复把条目重构为有日期的假设,并把核查移到执行时 —— 一个新的 `verify` 动词、一个 `Premise` 字段,以及在提升时的一次重新核查。 + ### [一百万条断言无法裁决的事](./2026-07-10-what-a-million-assertions-cant-settle.md) 2026-07-10 From 65d3d04c8bef4f30a0e211523d3750ad8a88fdfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Villase=C3=B1or=20Montfort?= <195970+montfort@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:20:00 -0600 Subject: [PATCH 2/2] feat(website): generate RSS + Atom feeds for the docs-instance blog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The blog is a docs-plugin instance (#340), so Docusaurus emits no feed and the site advertised none (/blog/rss.xml → 404). Add scripts/gen-feeds.ts, run from `postbuild`, which reads the published posts (EN + es + zh-CN), sorts them newest-first, and writes per-locale RSS 2.0 + Atom 1.0 into build/: /blog/{rss,atom}.xml and //blog/{rss,atom}.xml. Autodiscovery tags added to headTags. CI generates them automatically (npm run build → postbuild); build/ stays gitignored. Verified: all 6 feeds well-formed XML; newest-first (this release's post is item 1); i18n feeds carry translated titles + locale-prefixed links; blog head advertises both feeds. Sitemap already covered the post (plugin auto-includes routes) — feeds are correctly not listed in the sitemap. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/docusaurus.config.ts | 22 ++++ website/package.json | 2 + website/scripts/gen-feeds.ts | 196 +++++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 website/scripts/gen-feeds.ts diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 4de99e22..743a366c 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -144,6 +144,28 @@ const config: Config = { href: 'https://fonts.googleapis.com/css2?family=Fraunces:wght@600;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@500;600&display=swap', }, }, + // Blog feed autodiscovery. The blog is a docs-plugin instance, so Docusaurus + // does not emit or advertise a feed — these point at the files written by + // scripts/gen-feeds.ts (postbuild). Site-wide advertisement of the canonical + // (en) feed; per-locale feeds also exist at //blog/{rss,atom}.xml. + { + tagName: 'link', + attributes: { + rel: 'alternate', + type: 'application/rss+xml', + title: 'StrayMark Blog (RSS)', + href: '/blog/rss.xml', + }, + }, + { + tagName: 'link', + attributes: { + rel: 'alternate', + type: 'application/atom+xml', + title: 'StrayMark Blog (Atom)', + href: '/blog/atom.xml', + }, + }, ], future: { diff --git a/website/package.json b/website/package.json index efe2d729..787f9b0d 100644 --- a/website/package.json +++ b/website/package.json @@ -13,6 +13,8 @@ "start": "docusaurus serve", "prebuild": "npm run sync:docs && npm run generate:llms", "build": "docusaurus build", + "postbuild": "npm run generate:feeds", + "generate:feeds": "tsx scripts/gen-feeds.ts", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", diff --git a/website/scripts/gen-feeds.ts b/website/scripts/gen-feeds.ts new file mode 100644 index 00000000..d659d8af --- /dev/null +++ b/website/scripts/gen-feeds.ts @@ -0,0 +1,196 @@ +// Generates RSS 2.0 + Atom 1.0 feeds for the blog, per locale, into build/. +// +// The blog is a docs-plugin instance (#340), not the classic blog plugin, so +// Docusaurus does not emit feeds for it. This post-build script fills that gap: +// it reads the published posts (canonical EN + the es / zh-CN translations), +// sorts them newest-first, and writes: +// +// build/blog/rss.xml build/blog/atom.xml (en) +// build/es/blog/rss.xml build/es/blog/atom.xml (es) +// build/zh-CN/blog/rss.xml build/zh-CN/blog/atom.xml (zh-CN) +// +// It runs from `postbuild` (feeds are written into an already-built build/), +// mirroring the generated-artifact discipline of scripts/gen-llms-full.ts. +// Autodiscovery tags are declared in docusaurus.config.ts headTags. +import {readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync} from 'node:fs'; +import {join, resolve} from 'node:path'; +import matter from 'gray-matter'; + +const ROOT = resolve(__dirname, '..'); +const BUILD = join(ROOT, 'build'); +const SITE_URL = 'https://straymark.dev'; +const AUTHOR_NAME = readAuthorName(); + +interface Locale { + code: string; // '' for en (root), 'es', 'zh-CN' + postsDir: string; + title: string; + description: string; +} + +// Per-locale post sources. EN is canonical (website/blog); the translations +// live under the docs-blog i18n instance, same filenames/slugs. +const LOCALES: Locale[] = [ + { + code: '', + postsDir: join(ROOT, 'blog'), + title: 'StrayMark Blog', + description: 'The StrayMark chronicle — how the framework emerged, decision by decision.', + }, + { + code: 'es', + postsDir: join(ROOT, 'i18n/es/docusaurus-plugin-content-docs-blog/current'), + title: 'Blog de StrayMark', + description: 'La crónica de StrayMark — cómo emergió el framework, decisión por decisión.', + }, + { + code: 'zh-CN', + postsDir: join(ROOT, 'i18n/zh-CN/docusaurus-plugin-content-docs-blog/current'), + title: 'StrayMark 博客', + description: 'StrayMark 编年史 —— 这个框架如何随着一次次决策逐步成形。', + }, +]; + +interface Post { + slug: string; + title: string; + date: Date; + description: string; +} + +function readAuthorName(): string { + try { + const yml = readFileSync(join(ROOT, 'blog/authors.yml'), 'utf8'); + const m = yml.match(/^\s*name:\s*(.+)\s*$/m); + if (m) return m[1].trim(); + } catch { + /* fall through */ + } + return 'Strange Days Tech'; +} + +function xmlEscape(s: string): string { + return s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function readPosts(dir: string): Post[] { + if (!existsSync(dir)) return []; + const posts: Post[] = []; + for (const name of readdirSync(dir)) { + if (!/\.mdx?$/.test(name)) continue; + if (/^index\./.test(name)) continue; // the landing page, not a post + const {data} = matter(readFileSync(join(dir, name), 'utf8')); + if (data.draft === true) continue; + if (!data.slug || data.slug === '/') continue; + if (!data.date) continue; + posts.push({ + slug: String(data.slug).replace(/^\//, ''), + title: String(data.title ?? data.slug), + date: new Date(data.date), + description: String(data.description ?? '').trim(), + }); + } + return posts.sort((a, b) => b.date.getTime() - a.date.getTime()); +} + +function prefix(code: string): string { + return code ? `/${code}` : ''; +} + +function renderRss(loc: Locale, posts: Post[]): string { + const base = `${SITE_URL}${prefix(loc.code)}`; + const items = posts + .map((p) => { + const url = `${base}/blog/${p.slug}`; + return [ + ' ', + ` ${xmlEscape(p.title)}`, + ` ${url}`, + ` ${url}`, + ` ${p.date.toUTCString()}`, + ` ${xmlEscape(AUTHOR_NAME)}`, + ` ${xmlEscape(p.description)}`, + ' ', + ].join('\n'); + }) + .join('\n'); + const lastBuild = (posts[0]?.date ?? new Date()).toUTCString(); + return [ + '', + '', + ' ', + ` ${xmlEscape(loc.title)}`, + ` ${base}/blog`, + ` ${xmlEscape(loc.description)}`, + ` ${loc.code || 'en'}`, + ` ${lastBuild}`, + ` `, + items, + ' ', + '', + '', + ].join('\n'); +} + +function renderAtom(loc: Locale, posts: Post[]): string { + const base = `${SITE_URL}${prefix(loc.code)}`; + const updated = (posts[0]?.date ?? new Date()).toISOString(); + const entries = posts + .map((p) => { + const url = `${base}/blog/${p.slug}`; + return [ + ' ', + ` ${xmlEscape(p.title)}`, + ` `, + ` ${url}`, + ` ${p.date.toISOString()}`, + ` ${p.date.toISOString()}`, + ` ${xmlEscape(AUTHOR_NAME)}`, + ` ${xmlEscape(p.description)}`, + ' ', + ].join('\n'); + }) + .join('\n'); + return [ + '', + '', + ` ${xmlEscape(loc.title)}`, + ` ${xmlEscape(loc.description)}`, + ` `, + ` `, + ` ${base}/blog`, + ` ${updated}`, + entries, + '', + '', + ].join('\n'); +} + +function main(): void { + if (!existsSync(BUILD)) { + console.error('[gen-feeds] build/ not found — run after `docusaurus build`.'); + process.exit(1); + } + let total = 0; + for (const loc of LOCALES) { + const posts = readPosts(loc.postsDir); + if (posts.length === 0) { + console.warn(`[gen-feeds] no posts for '${loc.code || 'en'}' — skipped`); + continue; + } + const outDir = join(BUILD, ...(loc.code ? [loc.code] : []), 'blog'); + if (!existsSync(outDir)) mkdirSync(outDir, {recursive: true}); + writeFileSync(join(outDir, 'rss.xml'), renderRss(loc, posts)); + writeFileSync(join(outDir, 'atom.xml'), renderAtom(loc, posts)); + console.log(`[gen-feeds] ${loc.code || 'en'}: ${posts.length} posts → ${outDir}/{rss,atom}.xml`); + total += posts.length; + } + console.log(`[gen-feeds] done (${total} entries across ${LOCALES.length} locales).`); +} + +main();