FRANK
ANAYA

Work / Platform engineering

Designing a headless Drupal 11 platform for a million-visit event site

semashow.com serves 160,000+ attendees of the SEMA Show in Las Vegas and takes over a million visits a season. The mandate: design a replacement for a 15-year-old platform that editors could run and the next engineer could pick up.

1M+
visits / season
30+
components
15
decision records
33
seed scripts
semashow.com semashow.com homepage — the platform this case study is about
semashow.com. Click to enlarge.

The problem

The site had grown for 15 years on legacy Drupal 7/8. Auto-fill metatag templates were losing rankings. The content model fought the editors. There was no upgrade path. And the show does not move its dates for anyone, so the replacement had to be safe to land going into registration season.

Three architecture calls, made early, written down

  • Drupal 11 on Pantheon with zero custom modules. Everything lives in a custom Bootstrap 5 theme with Single Directory Components, config, and seed scripts. Unpopular opinion in the Drupal world, and I'll defend it: custom modules are where upgrade paths go to die. A site built this way takes 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. 30+ components sit on a design-token CSS system.
  • A Cloudflare Worker stitching both halves to one origin. My sema-asset-proxy Worker proxies file and theme paths through to Pantheon, so the visitor sees one domain and never the seam. Per-environment wrangler config makes a dev-to-live switch a one-line change.

Each call is an Architecture Decision Record, fifteen in all. 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

Editors
→
Drupal 11 CMSPantheon · behind Cloudflare Access SSO
→
JSON:API + OAuthcontent at build time
Astro buildstatic HTML · webhook-triggered, ~90s
→
Cloudflare Pagesedge-served, worldwide
→
1M+ visitors
⚙ sema-asset-proxy Worker — proxies media & theme assets back to Pantheon, so visitors see one origin and never the seam

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 did not want anyone, including me, making that call by feel. So I built the test suite that would make it. It 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 would keep its 301. Automated accessibility checks ran in the same gate. Red suite, no cutover.

Things I now know that cost me real hours

These are the bugs you only meet on a real environment. 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 _redirects silently drops rules with http:// destinations, and a missing 404.html makes your home page win every unknown path. Both are invisible until you go looking.
  • Cloudflare _headers merges 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 setTimeout past 30 seconds. Long-running scheduled work belongs in Cron Triggers. The failure is silent, which is the worst kind.
  • Pantheon's /tmp is 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 with 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.

Outcome

The organization moved production rollout to a vendor. The architecture, seed scripts and runbooks carried into the current platform.

What I do on the site now: I'm part of the Drupal 8 to 11 migration of sema.org and semashow.com. I build and deploy daily in the Git-based Cloudflare CI/CD pipeline. I own the custom semashow.com theme and the new Single Directory Components. And I'm redesigning the attendee and exhibitor pages: Why Attend, Why Exhibit, Plan Your Visit, transportation, floor-plan maps, schedule and planner.

What I learned: I ramped Drupal 11 deep and fast. And the parity suite and the runbooks were the parts that survived the politics.

Stack: Drupal 11, PHP 8.3, Pantheon, Astro, Cloudflare Pages / Workers / Access, JSON:API, OAuth 2, Twig, Composer, Drush, Python, Bash.