I Shipped My Own Publishing Pipeline — Headless CMS, Static Builds, and Full Ownership

Eleventy, Sanity, Cloudflare and every way the tooling said no before it said yes.

I Shipped My Own Publishing Pipeline — Headless CMS, Static Builds, and Full Ownership

I wanted to publish more often. My site was plain HTML — fine for a static portfolio, slow for anything I’d iterate on weekly. So I moved the blog to Eleventy at build time and Sanity as the headless CMS, with Cloudflare Pages doing the deploys. The architecture is boring in a good way: write in Studio, hit publish, static HTML comes out the other side.

The interesting part wasn’t the diagram — it was the series of small tooling traps that ate an evening.

What I chose (and why)

  • Eleventy — still static output, one npm run build, no React app for a site that doesn’t need it.
  • Sanity — real editor, structured posts, API I can query when the build runs.
  • Cloudflare Pages — already where the site lived; build command + _site output + env vars for the project ID.

I kept the portfolio on templates in src/ and only the blog in the CMS. That boundary stayed clear — I wasn’t trying to componentize my whole life.

What broke (so you can skip it)

The official “create a new project” command crashed before it did anything useful. I stopped fighting the installer and used the studio/ folder that already lived in the repo instead — same outcome, less drama.

I had an extra Sanity config file the tutorials tell you to add. It printed scary errors even when Studio was actually running. I removed it. One main config file was enough.

I treated the CMS config like a server script. I pulled in a small library that loads .env files the way you would in Node. Sanity Studio runs in the browser, not on a server — that choice exploded in the least helpful place. I deleted that import and let Sanity load environment variables its own way.

My .env was right, but Studio kept saying the project ID was missing. I’d written the config to read settings in a shorthand style that works fine in some tools but never made it through to the browser build. I spelled the variable names out explicitly and added a tiny fallback file with the same project ID — boring, but it never ghosts me again.

What worked

  • Deploy hook + Sanity webhook — publish a post, Cloudflare gets a POST, new build runs. No git commit required for copy changes.
  • Same slugs as before/blog/{slug}.html so I didn’t have to break old links when I migrated.
  • README as runbook — so I don’t have to rediscover which env files go where.

If you’re doing the same

  1. Keep a .env inside the studio/ folder for the editor, and match whatever your static build expects at the repo root — same project ID, consistent names.
  2. Restart the Studio dev server after you change .env — otherwise you’ll swear the file is wrong when it’s just stale.
  3. If the editor still can’t see your project ID, put it in a small fallback file in studio/ (the ID is public in the browser anyway — it’s not a secret).

The headless CMS didn’t change how I think about the site. It removed friction between thinking of a post and shipping it — once I stopped assuming every tool worked the way the last one did.