← Changelog

The Build Log Gets a Front Door — LFM Renders the Changelog at lossless.at

Twenty-one entries that only existed in a git directory are now a readable, linkable surface. Getting there cost four bugs worth writing down, including a content collection that reported zero entries while building perfectly.

· Michael Staton · with Claude Code on Claude Opus 5

Why Care?

For three months this repo has been writing changelogs that nobody could read without cloning it. Twenty-one entries — the Plane deploy, the day four CRMs turned out to be unable to accept a second user, the original Day One when the first Twenty instance came up — all sitting in a directory as raw Markdown.

Building in public only works if the public can get at it. As of today lossless.at/changelog is a real surface: every entry rendered, grouped by month, with prev/next navigation and a Changelog button in the header of the apex page.

The rendering runs on Lossless Flavored Markdown, the same pipeline the other Lossless splashes use. Callouts, code blocks, tables, and the ::image directive all work here because they work everywhere — the renderer is a component registry, not a per-site stylesheet. Writing an entry does not now require thinking about how it will look.

What's New?

  • /changelog — all 21 entries, newest first, grouped by month, each card carrying its date and lede.

  • /changelog/<slug> — the full entry through the LFM renderer, with author/augmentation metadata, a Files changed block when the frontmatter carries one, and older/newer navigation at the foot.

  • A header on every page — Stack and Changelog pills, with the current surface highlighted in the brand cyan.

  • pnpm sync:changelog — pulls entries in from the parent repo, honours the publish gate, and is wired into pnpm build so a local build can't ship stale content.

How It Works

The site is a submodule of self-host-stack and deploys to Vercel from its own repository. That single fact drives the whole design.

Vercel checks out lossless-at and nothing else. ../../changelog/ does not exist at build time there. So the entries reach the site by sync, not by reference — scripts/sync-changelog.mjs copies them into src/content/changelog/, and those copies are tracked in git, not gitignored. That looks wrong until you hold the deploy boundary in your head: the tracked copies are the deploy artifact.

The script does three things beyond copying. It honours publish: false — an entry gated in the parent never reaches the site. It clears stale files first, so un-publishing an entry actually removes it rather than leaving it stranded. And when the source directory is absent — exactly the Vercel case — it prints a notice and exits 0, letting the build proceed on the committed copies.

The same boundary explains the dependency pin. LFM comes in as npm:@jsr/lossless-group__lfm, a JSR registry pin, not workspace:*. That rule is already written down in astro-knots' own Workspace-vs-JSR-for-LFM-Consumers issue: a site that deploys needs a registry-resolvable dependency, because there's no parent workspace on the build host to resolve a symlink against. content-farm/splash is pinned the same way.

Under the Hood — Four Things That Cost Time

getStaticPaths was silently ignored. The site is output: "server". Under that setting Astro does not call getStaticPaths on a dynamic route — it treats every request as on-demand, Astro.props arrives empty, entry is undefined, and the page 500s on the first property access. export const prerender = true is both the fix and the correct behaviour: changelog entries are static, built once, served from the edge, no function invocation per read.

pnpm walked up the tree and installed nothing. pnpm install inside hubs/lossless-at found lossless-monorepo/pnpm-workspace.yaml several levels up, decided that was the install root, scoped the operation to that workspace — which lists site and content, not this app — and exited successfully having done nothing. No error, no warning, no node_modules. An empty pnpm-workspace.yaml in this directory pins it as its own install root, which is also exactly what Vercel sees.

The collection reported zero entries while building correctly. astro dev returned an empty list; astro build emitted all 22 pages. Diagnosed by elimination: a deliberate syntax error in content.config.ts surfaced at startup, proving the config was loaded; the generated types listed "changelog", proving the collection was registered. Config loaded, collection registered, files present, build fine — leaving a stale dev content store as the only remaining explanation. Clearing .astro fixed it. Worth recording because the symptom is indistinguishable from a broken loader, and the instinct is to rewrite the loader.

(Adjacent, cost ten minutes: a _debug.astro page written to inspect the collection 404'd. Astro excludes underscore-prefixed files from routing by design. The debugging tool was invisible for the same reason private components are.)

Backfilled entries floated to the top wearing today's date. The list sorted on date_modified ?? date_created, which is the usual precedence and is wrong here. Three entries backfilled yesterday describe work from May and July; their date_modified is current. For a build log the date that matters is when the work happened, so both routes now sort and display date_created first. Day One reads as May 20 again.

Files Touched

Beyond the list in the frontmatter, one inherited artifact: MarkdownReader ships a hardcoded ← All projects back-link aimed at /projects/, a route content-farm has and this site doesn't. Retargeted to /changelog.

What's Next

The changelog is currently noindex along with the rest of the hub. When the apex page is ready to be indexed, the changelog goes with it — and at that point the roll-up question becomes live: this repo's entries are one feed among several, and changelog-conventions already describes how a parent aggregates its children.

References

  • [[changelog-conventions]] — what an entry looks like and when to write one

  • [[prep-images-for-embed]] — how images get into an entry, now that they'll render

  • astro-knots/context-v/issues/Workspace-vs-JSR-for-LFM-Consumers.md — why the pin is a pin

Files changed

  • hubs/lossless-at/package.json
  • hubs/lossless-at/pnpm-workspace.yaml
  • hubs/lossless-at/scripts/sync-changelog.mjs
  • hubs/lossless-at/src/content.config.ts
  • hubs/lossless-at/src/content/changelog/
  • hubs/lossless-at/src/layouts/BaseLayout.astro
  • hubs/lossless-at/src/components/markdown/
  • hubs/lossless-at/src/pages/changelog/index.astro
  • hubs/lossless-at/src/pages/changelog/[...slug].astro
  • hubs/lossless-at/src/pages/index.astro