The problem
The site had grown for 15 years on legacy Drupal 7/8. Auto-fill metatag templates were actively losing rankings. The content model fought the editors. There was no upgrade path. And the show doesn't move its dates for anyone — the replacement had to land without breaking search traffic going into registration season.
Three architecture calls, made early, written down
- Drupal 11 on Pantheon with zero custom modules. Everything lives in a custom theme, config, and seed scripts. Unpopular opinion in the Drupal world, and I'll defend it: custom modules are where upgrade paths go to die. This site will take Drupal 12 without drama.
- Headless Astro frontend on Cloudflare Pages. Drupal becomes an editor-only CMS behind Cloudflare Access SSO. Visitors get static HTML from the edge. The frontend fetches content at build time over JSON:API with OAuth and cache-aware token refresh.
- A Cloudflare Worker stitching both halves to one origin. My
sema-asset-proxyWorker proxies file and theme paths through to Pantheon, so the visitor sees one domain and never the seam. Per-environment wrangler config makes the dev-to-live cutover a one-line change.
Each call is an Architecture Decision Record — fifteen of them by launch. When someone asks "why is there no custom module for this?" the answer is a document, and the debate stays settled.
The architecture, on one napkin
The principle the whole project ran on
Parity is a function, not an assertion. You write the test that decides when you cut over, and you don't ship until it's green.
Migrations lose SEO because someone eyeballs the new site and declares it close enough. I
refused to let anyone — including me — make that call by feel. The parity suite checked
titles, meta descriptions, canonicals, Open Graph tags, JSON-LD structured data, and robots
directives, old site versus new, page by page. A Python script mirrored Drupal's entire
redirect table into Cloudflare Pages _redirects, so every URL from 15 years of
history kept its 301. Red suite, no launch. It went green, we launched, the rankings held.
Things I now know that cost me real hours
These are the bugs you only meet in production. I document every one so nobody pays for the same lesson twice — including you, if any of these are biting you right now:
- Drupal Metatag 2.x storage: the field stores JSON via its own encoder. Write it with PHP
serialize()and the widget silently can't read it back. Half a day, gone. Documented forever. - Cloudflare Pages
_redirectssilently drops rules withhttp://destinations, and a missing404.htmlmakes your home page win every unknown path. Both are invisible until you go looking. - Cloudflare
_headersmerges values across every matching pattern — there is no "most specific wins." If you assume CSS-style specificity, your security headers are not what you think they are. - Workers truncate
setTimeoutpast 30 seconds. Long-running scheduled work belongs in Cron Triggers. The failure is silent, which is the worst kind. - Pantheon's
/tmpis ephemeral between SSH sessions. Stream JSON over stdout, keep summaries on stderr, and never trust a temp file to still exist.
Operational spine
- Editor saves a page → webhook fires a targeted Cloudflare Pages rebuild → live in about 90 seconds.
- 33 idempotent seed scripts, one orchestrator. They probe state and skip what exists, so re-running on any environment is free and never overwrites editor content.
- Dual-target deploy: full history to GitHub, a filtered tree to Pantheon's Integrated Composer, one command.
- Migrated 2.7 GB of legacy media — 5,600+ files from an archive that turned out to be malformed, no central directory — with a custom streaming ZIP extractor I wrote that resumes safely.
- Found leaked credentials in a handoff doc during a cleanup pass; rotated the PAT and OAuth secret at source in 20 minutes. Rotation beats history-rewriting: it makes the leaked value inert without breaking anyone's clone. That reasoning is an ADR too.
Result
Shipped with zero ranking loss and ran through a full show cycle. About 1,000 commits, 15 ADRs, structured data end to end (JSON-LD, FAQ and Speakable schema, an llms.txt for answer-engine optimization), and a runbook tree that onboards the next engineer in one command. I own this platform end to end on SEMA's two-person web team.
Stack: Drupal 11, PHP 8.3, Pantheon, Astro, Cloudflare Pages / Workers / Access, JSON:API, OAuth 2, Twig, Composer, Drush, Python, Bash.