Sharing an Astro blog across two sites
I wanted a dev log on imperfectsystems.com. The blog I already have on vdaluz.com works fine, so the obvious move was to reuse its code instead of writing a second one. “Reuse” turned out to be more interesting than I expected.
You can’t extract a blog
My first instinct was “extract the blog into a package.” That’s not a real thing in Astro. Routes live in src/pages/ and have to stay in each app. Content is a folder of markdown per site. The Shiki config, the Tailwind setup, and the theme CSS are all per-app too. What’s actually portable is smaller than it sounds: the components that render a post card, a pagination bar, the related-posts list, plus the scoring function and the content schema.
So the honest version of the goal isn’t “shared blog.” It’s “a small component library, plus a bit of glue in each site.” Once I framed it that way the design got easy.
One package, two palettes
vdaluz.com is a light/dark site with a blue accent. imperfectsystems is black with neon green. If the shared components hard-coded any color, this falls apart. They don’t. Every component references CSS-variable design tokens like bg, surface, accent. The package never says what those mean. Each site defines the values in its own CSS.
That’s the whole trick. The same PostCard renders blue-on-white on one site and green-on-black on the other, with no forks. I also dropped the dependency on Astro’s CollectionEntry<'blog'> type in favor of a plain structural type, so the package doesn’t care what each site names its collection.
Two ways CI bit me
The package builds locally on the first try. Then I pushed, and CI failed twice for reasons that never show up on my laptop.
First: a github:user/repo dependency gets rewritten by npm to a git+ssh URL in the lockfile. Even an explicit git+https URL gets rewritten. My machine has an SSH key so it clones fine. The CI runner doesn’t, so npm ci couldn’t fetch the package at all. The fix for a public repo is to skip git entirely and depend on a pinned tarball: https://github.com/vdaluz/astro-blog/archive/refs/tags/v0.1.0.tar.gz. Plain https, anonymous, with an integrity hash.
Second, and meaner: I regenerated the lockfile on my Mac to resolve a merge conflict. That quietly wrote only the macOS build of rollup into the lockfile and dropped the Linux one. On the Linux CI runner the build died with Cannot find module @rollup/rollup-linux-x64-gnu. This is a known npm bug with optional native dependencies. The fix was to restore the known-good lockfile and add my one new dependency with npm install --package-lock-only, which keeps every platform’s binaries instead of pruning to mine.
The lesson I keep relearning: a green build on my machine says nothing about a Linux runner with no SSH key. Now I run npm ci && npm run build on node 22 before I trust a push.
Where it landed
imperfectsystems.com/blog is live, rendering through the shared package in full neon. vdaluz.com still has its own copy for now; moving it onto the package is the next step. Two sites, one set of components, and a couple of CI scars to show for it.
Related reading
Clearing a backlog: five small fixes in one release
A dead-decoration a11y fix, a schema field that validated but never rendered, and a formatter run that silently flipped quote style across four files.
The escaping bug that only shows up with a second query param
A rewrite function that worked in every test and every real URL it had ever seen, and would still have broken the moment someone added a second query param.
Testing the part of the codebase that documents its own footgun
A README warning about a subtle failure mode is a confession that the code isn't tested against it yet.