Why the blog has two hosts

Static files for readers, a server for editors. The split looks like over-engineering until you look at what a good editing experience actually requires.

Kash Sajadi

This blog builds two different ways from one codebase. For readers it is a folder of static HTML on object storage behind a CDN. For editors it is a server, rendering every request on demand. Same repository, same components, same styles; one environment variable decides which.

That is not where we started. It is where the editing experience pushed us.

Draft mode is a request-time idea

A good visual editor for a static site has to answer one question: what will this look like when it is published? Answering it means rendering unpublished content, which means the renderer has to know who is asking. Our CMS does this with a signed cookie — draft mode on, drafts visible; cookie absent, published content only.

A cookie is read per request. Static files have no requests to read it from. There is no handler between the reader and the file, which is exactly why static hosting is fast and cheap, and exactly why it cannot support a preview.

Everything else the editors wanted hangs off the same hook: click a heading in the rendered page and land on that field in the CMS, see a save reflected without a rebuild, share a link to an unpublished draft. All request-time features.

So: two targets

pnpm build          # static output, published content only
DEPLOY_TARGET=preview pnpm build   # server output, drafts visible

The production target reads the published-content token. The preview target reads the draft-content token, and only the preview target has a server to attach the editor tooling to.

Keeping the split from leaking

The interesting engineering is not the two builds. It is stopping the preview half from bleeding into the production half, because the failure mode is silent: a production bundle that quietly carries editor code, or worse, editing metadata.

  1. The preview-only routes live outside the pages directory entirely, and are injected by a build integration that only runs for the preview target. A production build cannot emit a draft-mode endpoint, because the route does not exist for it.

  2. The editor overlay script is injected the same way, rather than rendered from a component. Importing a component bundles its script whether or not the branch renders — a conditional in the layout put a 38 kB chunk in the production build.

  3. Every data fetch goes through one function that takes a drafts flag. That flag picks the token and switches the editing metadata on together, so published HTML cannot carry editing annotations.

Two hosts is more moving parts than one. It buys the readers a site with no server in the path, and the editors a preview that tells the truth. Both of those were requirements; a single host could only ever have served one of them.