Skip to content

fix(website): blog bodies named by slug -- the reader opens every post - #1626

Merged
gHashTag merged 1 commit into
mainfrom
fix/blog-body-slug-files
Oct 12, 2026
Merged

gHashTag merged 1 commit into
mainfrom
fix/blog-body-slug-files

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

What broke

The post "Buses and peripherals with t27, in 27 lessons" (and "Clocks, resets and CDC") rendered fine as a static page but died in the SPA reader on every opening: "Интерактивная версия не смогла загрузить этот пост".

Two components resolve a post body independently, and they disagreed:

  • the static build goes through posts.ts, whose import table happily mapped the slug buses-and-peripherals to a file named course-buses-and-peripherals.ts;
  • the reader SPA (Blog.tsx) resolves the same body through import.meta.glob keyed by file name — ../data/blog/bodies/${slug}.ts — found no module, and Promise.resolve(undefined) put the post on the failed path every time.

The build could not see it: posts.ts was consistent, so the site built and prerendered green.

Fix

  • git mv the two body modules to their slugs (buses-and-peripherals.ts, clocks-resets-and-cdc.ts); posts.ts imports updated. Verified in the built output: the Blog chunk's glob table now keys ../data/blog/bodies/buses-and-peripherals.ts and imports ./buses-and-peripherals-BPQfd83k.js, chunks on disk.
  • New gate check:blog-bodies holds the invariant bodies/<slug>.ts for every published post (108/108 pass, zero orphans), loading slugs with loadPublishedPosts() — module eval, so quote style and unpublished entries cannot drift. Runs its own negative control (--self-test) first, then wired into website-checks.yml.

A post whose body module is misfiled now fails CI instead of failing every reader.

Related

Fixing this also restores the Share row on those posts (it renders only on the body-success path), whose buttons share the static URL https://t27.ai/blog/<slug>/ — the URL whose og/twitter card is correct. Pasting the SPA hash URL (#/blog/...) into X reads the generic root card: the fragment never reaches the server. That is a known limitation of hash routing, unchanged here.

{
  "version": 1,
  "head_sha": "2dc02d63ba6d5bdd178b230a6cd4c5c408fff977",
  "summary": "Two blog posts (buses-and-peripherals, clocks-resets-and-cdc) rendered statically but failed on every opening in the SPA reader: the reader resolves bodies by file name while posts.ts maps slugs to imports, and the two disagreed. Body modules renamed to their slugs; a CI gate now holds bodies/<slug>.ts for every published post.",
  "changes": [
    "git mv bodies/course-buses-and-peripherals.ts -> bodies/buses-and-peripherals.ts",
    "git mv bodies/course-3-clocks-resets-and-cdc.ts -> bodies/clocks-resets-and-cdc.ts",
    "posts.ts import paths updated to the new module names",
    "new gate scripts/blog-bodies-check.mjs (self-test + real run) and npm script check:blog-bodies",
    "website-checks.yml: step 'every post opens in the reader' runs the gate"
  ],
  "tests": [
    {
      "command": "npm run check:blog-bodies",
      "status": "passed",
      "result": "self-test: a fileless slug is reported, an orphan is listed; blog bodies: every published post of 108 opens in the reader",
      "evidence": "local run of this head in /tmp/wasm-blog-trinity, 2026-10-12"
    },
    {
      "command": "npm run build",
      "status": "passed",
      "result": "vite build green; dist Blog chunk glob keys ../data/blog/bodies/buses-and-peripherals.ts and ../data/blog/bodies/clocks-resets-and-cdc.ts present, chunks buses-and-peripherals-BPQfd83k.js and clocks-resets-and-cdc-BH4nZpF5.js on disk; old course-* keys absent",
      "evidence": "local build of this head in /tmp/wasm-blog-trinity, 2026-10-12"
    }
  ],
  "limitations": [
    "Sharing the SPA hash URL (t27.ai/#/blog/...) still reads the generic root card on X -- the fragment never reaches the server; the Share buttons share the static URL whose card is correct"
  ],
  "tags": ["website", "blog", "CIGate"],
  "blog": {
    "title": "Two posts that rendered but never opened: one lookup table too many",
    "summary": "The static build mapped post slugs to body modules through posts.ts, the reader resolved the same bodies by file name, and two posts fell between the two tables. Every body module is now named by its slug, and a CI gate holds the invariant for all 108 published posts.",
    "outline": [
      "The symptom: a post renders perfectly as a static page, then dies in the interactive reader with a load-failed notice on every single opening, while the site builds green.",
      "The cause: two independent resolution paths. posts.ts maps slugs to imports and was self-consistent; the reader glob keyed by file name found nothing for slugs whose modules were named course-*.ts.",
      "The fix and the gate: rename the modules to their slugs, then check bodies/<slug>.ts for every published post in CI, self-tested first so the detector is watched failing before it is trusted."
    ]
  }
}

🤖 Generated with Claude Code

The static build resolves a post body through posts.ts, whose import table
happily mapped the slug 'buses-and-peripherals' to a file named
course-buses-and-peripherals.ts. The reader SPA (Blog.tsx) resolves the same
body through import.meta.glob keyed by FILE NAME, found no matching module,
and every opening of that post (and clocks-resets-and-cdc) ended on
"Интерактивная версия не смогла загрузить этот пост". The build could not
see it: posts.ts was consistent, so the site rendered fine.

- git mv the two body modules to their slugs; posts.ts imports updated
- new gate check:blog-bodies holds bodies/<slug>.ts for every published post
  (runs its own negative control first), wired into website-checks.yml

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the status:in-progress 🔵 Agent working label Oct 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

X Brain Health Check

Score: 100.0/100
Status: 🟢 HEALTHY
Threshold: 80/100

X Brain is above merge threshold

@tri-claw

tri-claw Bot commented Oct 12, 2026

Copy link
Copy Markdown

Summary by t27-bees

The PR fixes a mismatch between the static build and SPA reader for blog post bodies: the static build resolves bodies via posts.ts imports (which mapped slugs to course-*.ts files), while the SPA reader uses import.meta.glob keyed by bodies/${slug}.ts. Two posts rendered statically but failed in the reader on every open. The fix renames the body files to match their slugs, updates posts.ts imports, and adds a CI gate (check:blog-bodies) with a self-test to enforce the bodies/<slug>.ts invariant.

Worth a look

  • apps/website/scripts/blog-bodies-check.mjs:15 — Imports loadPublishedPosts from ./update-blog-cover-versions.mjs. If that module doesn't export the function, has side effects, or its export signature changes, the check will fail at runtime in CI.
  • apps/website/scripts/blog-bodies-check.mjs:22–24 — checkBlogBodies only scans for .ts files in the bodies directory. A body module with a different extension (e.g., .tsx, .js) would be invisible to the check: it wouldn't appear in files, so a matching slug would be reported as missing (false positive), and the non-.ts file would never be flagged as an orphan.
  • apps/website/scripts/blog-bodies-check.mjs:64–66 — Self-test cleanup uses rmSync(dir, { recursive: true, force: true }) in a finally block. If mkdtempSync throws before dir is assigned, dir is undefined and the cleanup throws, potentially masking the original error.
  • apps/website/src/data/blog/posts.ts:110–111 — Variable names (body_course_buses_and_peripherals, body_clocks_resets_and_cdc) still reference the old course-* naming. Not a functional bug, but misleading for future maintainers.
  • apps/website/package.json:37 — The script runs check:blog-bodies as node scripts/blog-bodies-check.mjs --self-test && node scripts/blog-bodies-check.mjs. If the self-test passes but the real run fails, the error message from the real run will be printed, but the self-test's success message ("self-test: a fileless slug is reported, an orphan is listed") will also appear in logs, which could confuse log parsing.

Read: 6 files, +94 -2, the whole diff. Head 2dc02d6. Model: openai-compatible/nvidia/nemotron-3-ultra-550b-a55b. Commands: @t27-bees review | summary | help | pause | resume. Free for every repository.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Brain Stress Test Results

Tests: 261/261 passed, 0 leaked
Status: 🟢 PASS
Gate: every test in zig build test-brain-stress passes and none leaks

@gHashTag
gHashTag merged commit b45da4c into main Oct 12, 2026
41 of 45 checks passed
@gHashTag
gHashTag deleted the fix/blog-body-slug-files branch October 12, 2026 09:32
@github-actions github-actions Bot added status:completed Done and removed status:in-progress 🔵 Agent working labels Oct 12, 2026
github-actions Bot added a commit that referenced this pull request Oct 12, 2026
fix(website): blog bodies named by slug -- the reader opens every post (#1626)

The static build resolves a post body through posts.ts, whose import table
happily mapped the slug 'buses-and-peripherals' to a file named
course-buses-and-peripherals.ts. The reader SPA (Blog.tsx) resolves the same
body through import.meta.glob keyed by FILE NAME, found no matching module,
and every opening of that post (and clocks-resets-and-cdc) ended on
"Интерактивная версия не смогла загрузить этот пост". The build could not
see it: posts.ts was consistent, so the site rendered fine.

- git mv the two body modules to their slugs; posts.ts imports updated
- new gate check:blog-bodies holds bodies/<slug>.ts for every published post
  (runs its own negative control first), wired into website-checks.yml

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant