A blog that prerenders

2 min read

This is the first post on blog.cassandraearls.com. It exists mostly to prove the pipeline works end to end, so it may as well describe the pipeline.

Why not client-render it

The other two sites in this monorepo — cassandraearls.com and cuckooandthebirds.com — are client-rendered single-page apps. That is fine for a portfolio, where nobody is pasting a deep link into Slack and expecting a preview card.

A blog is the opposite. Every post is a link somebody shares, and the crawlers that build those preview cards do not run JavaScript. A client-rendered post previews as the site default, whatever the post is actually about.

So this app runs React Router in framework mode with ssr: false and prerender: true: no server at runtime, but every route rendered to real HTML at build time, each with its own <title>, description, and canonical URL.

The part that was not obvious

Prerendering inlines React's hydration bootstrap into the generated HTML. The blog's Content-Security-Policy has no 'unsafe-inline', so the browser blocked all eight inline scripts on the landing page: the page rendered its static markup and then never hydrated. No dark-mode toggle, no client-side routing, and no error anyone would notice locally, because neither vite preview nor Cypress serves the _headers file that carries the policy.

The fix was a postbuild step that hashes every inline script into script-src:

const hash = createHash("sha256").update(script.textContent).digest("base64");

Hashes rather than 'unsafe-inline', which would forfeit the policy entirely, and rather than nonces, which need a server to generate a fresh one per response — exactly what a static deploy does not have.

What this post is made of

The file you are reading is MDX in apps/blogs/content/posts/, with YAML frontmatter validated at build time. A missing publishedAt, a timestamp with no UTC offset, or two posts claiming the same slug all fail the build with the offending filename, rather than shipping something subtly wrong.

Publishing is a pull request.