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.
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, aFiles changedblock when the frontmatter carries one, and older/newer navigation at the foot.A header on every page —
StackandChangelogpills, with the current surface highlighted in the brand cyan.pnpm sync:changelog— pulls entries in from the parent repo, honours thepublishgate, and is wired intopnpm buildso 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.jsonhubs/lossless-at/pnpm-workspace.yamlhubs/lossless-at/scripts/sync-changelog.mjshubs/lossless-at/src/content.config.tshubs/lossless-at/src/content/changelog/hubs/lossless-at/src/layouts/BaseLayout.astrohubs/lossless-at/src/components/markdown/hubs/lossless-at/src/pages/changelog/index.astrohubs/lossless-at/src/pages/changelog/[...slug].astrohubs/lossless-at/src/pages/index.astro