Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions website/blog/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions website/blogSidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
22 changes: 22 additions & 0 deletions website/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 /<locale>/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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
196 changes: 196 additions & 0 deletions website/scripts/gen-feeds.ts
Original file line number Diff line number Diff line change
@@ -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 <link> 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}

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 [
' <item>',
` <title>${xmlEscape(p.title)}</title>`,
` <link>${url}</link>`,
` <guid isPermaLink="true">${url}</guid>`,
` <pubDate>${p.date.toUTCString()}</pubDate>`,
` <dc:creator>${xmlEscape(AUTHOR_NAME)}</dc:creator>`,
` <description>${xmlEscape(p.description)}</description>`,
' </item>',
].join('\n');
})
.join('\n');
const lastBuild = (posts[0]?.date ?? new Date()).toUTCString();
return [
'<?xml version="1.0" encoding="UTF-8"?>',
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">',
' <channel>',
` <title>${xmlEscape(loc.title)}</title>`,
` <link>${base}/blog</link>`,
` <description>${xmlEscape(loc.description)}</description>`,
` <language>${loc.code || 'en'}</language>`,
` <lastBuildDate>${lastBuild}</lastBuildDate>`,
` <atom:link href="${base}/blog/rss.xml" rel="self" type="application/rss+xml"/>`,
items,
' </channel>',
'</rss>',
'',
].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 [
' <entry>',
` <title>${xmlEscape(p.title)}</title>`,
` <link href="${url}"/>`,
` <id>${url}</id>`,
` <published>${p.date.toISOString()}</published>`,
` <updated>${p.date.toISOString()}</updated>`,
` <author><name>${xmlEscape(AUTHOR_NAME)}</name></author>`,
` <summary>${xmlEscape(p.description)}</summary>`,
' </entry>',
].join('\n');
})
.join('\n');
return [
'<?xml version="1.0" encoding="UTF-8"?>',
'<feed xmlns="http://www.w3.org/2005/Atom">',
` <title>${xmlEscape(loc.title)}</title>`,
` <subtitle>${xmlEscape(loc.description)}</subtitle>`,
` <link href="${base}/blog/atom.xml" rel="self"/>`,
` <link href="${base}/blog"/>`,
` <id>${base}/blog</id>`,
` <updated>${updated}</updated>`,
entries,
'</feed>',
'',
].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();