Operator reference · migration runbook · five production exits

Life After WordPress:
Running Five Sites With No CMS

WordPress was the right answer for a long time, and it remains the right answer when many nontechnical editors need a browser UI. But for an agent-operated site, its three core jobs now have simpler replacements that are versioned, inspectable, and portable.

CMS = editor + database + theme
new stack = git + files + model

The CMS teardown Three WordPress jobs, editor, database, and theme, are unplugged and connected to git, plain files, and an AI design agent. THE DECOMMISSIONING UNPLUG THE CMS. KEEP THE JOBS. The interface changes. Editorial control, durable data, and design still exist. W WORDPRESS JOB 01 EDITOR wp-admin · roles · revisions WORDPRESS JOB 02 DATABASE MySQL · posts · builder data WORDPRESS JOB 03 THEME PHP · CSS · builder UI REPLACEMENT 01 GIT IS THE EDITOR diff · review · commit · rollback · audit trail + PR REPLACEMENT 02 FILES ARE THE DATABASE content/*.md · data/*.json · evidence.json REPLACEMENT 03 THE MODEL IS THE DESIGNER spec · render · browser QA · iterate REQUEST TIME: quiet HTML BUILD TIME: where the motion moved
The interface changes; the responsibilities do not. Preserve review, data integrity, design quality, and rollback explicitly.

00 / Quick reference

Should this site be static?

ConstraintStatic works when…Keep a CMS/app when…
Publishing rate✓ A build per change is acceptable, even dozens per day.✕ Sub-second publication or request-time inventory is required.
Editors✓ Editors can use git, a repository UI, or agents that open PRs.✕ Many nontechnical editors require visual drafts, roles, and scheduling.
Personalization✓ The document is the same for everyone; small widgets can call an API.✕ Most HTML differs by user, account, geography, or entitlements.
Auth/accounts✓ Auth is absent or isolated in an external service.✕ The core product is a signed-in dashboard or user-generated workspace.
Forms✓ A small PHP endpoint or edge Worker can validate and forward submissions.✕ Forms mutate complex relational state or power approval workflows.
Search/comments✓ A client index handles a bounded corpus; comments can be dropped or outsourced.✕ Search needs live permissions/ranking or discussion is the product.
E-commerce△ Static product pages hand off checkout to a hosted commerce service.✕ Pricing, stock, carts, and fulfillment are request-time application logic.
Operations✓ Git, Node, CI, redirects, and server config are owned deliberately.✕ A managed CMS is cheaper than maintaining the build/deploy path.

01 / Mental model

What does a CMS actually do?

A CMS bundles several responsibilities behind one UI. A static migration succeeds when each responsibility gets an explicit new home.

Past: editor

Browser editing hid change control

WordPress provides roles, drafts, previews, revisions, and a Publish button. That is useful when the browser is the team’s shared workspace.

Replacement: git commits are revisions, PRs are review, branch previews are drafts, and a deploy script is Publish. The approval boundary becomes inspectable.

Present: git

Git becomes the editorial system

A correction is a diff with an author, timestamp, review trail, and exact rollback point. Agents can propose changes without acquiring production credentials.

Fleet proof: CFW’s scheduled watchers change versioned data through PRs; the public site changes only after review and deployment.

Past: database

MySQL made content queryable, but opaque

Posts and metadata are structured, but page builders often serialize layout into database fields. A visual Elementor page is difficult to grep, diff, or review as code.

When to keep it: request-time relations, user writes, complex permissions, or a genuinely collaborative editorial database.

Present: files

Plain files become durable data

Markdown, HTML, JSON, YAML, and TypeScript modules are portable records. The repository holds content and the rules that render it.

Fleet proof: CFW changed from list-2026-05-15.json to list-2026-07-30.json; the latter contains 872 entries. A fix is a reviewed patch, not a phpMyAdmin session.

Past: theme

The theme bundled design decisions

PHP templates, widgets, CSS, and page-builder controls let a human assemble pages without writing the underlying system.

Cost: the abstraction becomes an obstacle when an agent must reason about serialized blocks, plugin shortcodes, and theme overrides together.

Present: model

The model works from source to rendered page

An AI collaborator can create the design, implement it in plain source, render in a real browser, test at 375 px, and iterate against a binding spec.

Boundary: the model is not the approval gate. Automated checks and a human-authorized deploy preserve the separation between drafting and publishing.

Control

Build time replaces request time

Pages, feeds, indexes, and route lists are computed once during a build. Visitors receive ordinary HTML instead of invoking PHP and database queries per request.

Tradeoff: freshness now depends on rebuild triggers. A “static” fleet may run more cron and CI automation than the old site.

Portability

The output is boring on purpose

A complete build is a directory of HTML, CSS, images, feeds, and small scripts. Any static file host can serve it without framework-aware infrastructure.

Fleet proof: all five sites moved from a self-hosted nginx origin to Cloudflare Workers static assets on 2026-09-27 without touching a generator. The build directories stayed the same; nginx rules became _redirects and _headers files.

Fit, not dogma

WordPress still wins some decisions

Keep it when many people need a visual editor, plugins already solve the business workflow, or the organization cannot own a code-based publishing pipeline.

Rule: migrate only when the new operating model is cheaper and clearer than the CMS it replaces.

02 / Decision framework

Which static site generator should I use?

This is an operator comparison of the five stacks actually in use. Hugo, Jekyll, and Gatsby may also fit, but adding names does not improve this decision.

CriterionAstro 7/5Eleventy 3.1.6Next.js 16 + vinextRaw agent-written HTMLStay on WordPress
Mental modelFile routes + components + build-time data; hydrate only named islands.Transform a directory of templates and data into an output directory.React App Router rendered through a Next-compatible Vite runtime, then crawled here.The source file is the page; any static host serves it unchanged.PHP application resolves requests from a database, theme, and plugins.
Templating.astro components, layouts, framework components, TypeScript.Nunjucks, Liquid, Markdown, HTML, JS, and more; engines configurable per format.React Server and Client Components in app/.HTML + CSS + optional JS; includes require manual duplication or generation.PHP theme templates, block editor, shortcodes, page builders.
Content formatsMarkdown/MDX collections, JSON, APIs, custom content loaders.Markdown, HTML, Nunjucks, JSON/YAML data, arbitrary template languages.TypeScript/JS modules, fetched data, MDX with setup; this fleet uses TS.Anything embedded or generated into the file.Posts, pages, custom post types, media, fields, taxonomies.
900 pages from JSONStrong. getStaticPaths() plus typed loaders. CFW builds one route per record.Strong. Pagination/data cascade works, but custom relationships take code.Strong. Generate/discover route params, then prerender or crawl.Possible. Write a generator script first; raw HTML alone has no loop.Possible. Import records or write a plugin; runtime queries remain.
Client JS by defaultNone for static components. JavaScript ships only with explicit client:* islands.None. Only authored scripts ship.Some. Client Components and navigation/hydration can add bundles; Server Components do not.None. Only authored scripts ship.Varies. Theme and plugins decide.
InteractivityReact/Vue/Svelte/etc. islands can coexist with static HTML.Bring vanilla JS, Web Components, or an island helper; no prescribed client runtime.React is first-class; easiest path if the static site may become an application.Native HTML first; hand-author any behavior.Plugin ecosystem and request-time PHP; broadest turnkey choice.
Build behavior hereHundreds of data routes plus content; CI guards minimum page counts.Dozens of imported posts/pages; simple one-command builds to _site/.Build a runnable bundle, start locally, crawl deterministic routes into flat files.No build unless validation, screenshots, or generation scripts are added.No full-site build; each request executes the app unless cached.
Config surfaceModerate: integrations, content schemas/loaders, routes, client directives.Small core, but the data cascade and template-engine interactions deserve care.Largest: React, Next semantics, vinext/Vite, crawl rules, asset rewriting.Tiny at first; duplication and custom tooling grow with the corpus.Low for basic use; theme/plugin/server interactions grow operationally.
Learning curveComfortable for component-oriented frontend developers.Fast for HTML/Markdown work; subtle configuration is the main edge.Highest in this set: React, server/client boundaries, routing, runtime assumptions.Lowest for one page; highest when inventing your own framework accidentally.Lowest for nontechnical publishing; PHP/theme internals are a separate skill.
Agent friendlinessHigh. Components and data are explicit, typed, and diffable.Very high. Imported bodies can remain plain while layouts stay templated.Medium-high. Explicit code, but more layers and generated artifacts.Highest per page. The model can own the complete file.Low with builders. Opaque DB blobs and plugin state resist diffs.
Ecosystem riskFramework and integration majors can move; static output remains portable.Small core and old-web defaults reduce lock-in; template packages can still drift.Highest churn and dependency surface here; vinext adds compatibility risk.Browser standards are durable; your bespoke conventions become the risk.Mature, vast ecosystem; plugin abandonment and compatibility are recurring work.
Runtime/hostingDefault output is static; serve from any file server when no route opts into runtime rendering.Static output directory; host almost anywhere.Official static export exists, but this fleet instead snapshots a local vinext server.Any static server.Requires PHP + database unless a caching/export layer is added.
Best fitStructured, data-heavy reference with selected interactive islands.Content migration where imported Markdown/HTML should stay close to source.React product that is static today but may gain application behavior.One-off terminal reference, landing page, or deliberately standalone artifact.Multi-editor publishing, plugin-centric commerce/workflows, low-code ownership.

Versions are the locally resolved fleet versions on 2026-08-14: Astro 7.1.6 and 5.18.2; Eleventy 3.1.6; Next.js 16.3.0 with vinext 0.0.45 declared. Behavior is grounded in current official framework documentation and the repositories listed in Sources.

Use Astro when… structured data generates many routes and only a few views need JavaScript. CFW proves the pattern with 872 firearm records, 64 counties, and React islands.
Use Eleventy when… the migration is mostly WordPress-exported Markdown/HTML and you want the content path nearly inert. WalletRecovery and Vellum prove both CommonJS and ESM forms.
Use Next when… React/App Router is already the product language or the site may grow into an app. WhoPaysForAI proves the static-snapshot route, and its empty schema warns against assuming scaffolding is live.
Use raw HTML when… one file is the product and a model can maintain the whole thing. This cheatsheet collection proves the terminal-reference shape.
Keep WordPress when… visual collaboration, browser-native scheduling, plugin workflows, or many editors matter more than source-level review.

03 / Ordered runbook

How do I migrate WordPress to a static site?

Run these as ten workstreams. A beautiful homepage is not a migration if old URLs, feeds, forms, or media break.

Inventory before exporting

Capture published URLs, post types, taxonomies, forms, feeds, redirects, plugins that create public routes, and media references.

Fleet example: preserve a crawler output and treat it as the expected-route set.

The WordPress menu is not a URL inventory. Search engines and backlinks know routes that navigation no longer exposes.

Export WXR and retain it

Use Tools → Export to download WordPress eXtended RSS. It can include posts, pages, custom post types, comments, fields, categories, tags, taxonomies, and users.

Fleet example: Vellum retains vellumcapital.WordPress.2026-07-10.xml and exposes npm run import:wordpress.

WXR references media URLs; it is not necessarily a byte-for-byte archive of uploads.

Choose the media survival strategy

Passthrough: copy the complete wp-content tree so old URLs survive. Rewrite: download selected assets, optimize them, and update HTML.

Fleet example: Vellum passes root wp-content/ through; WalletRecovery uses download_media.py and optimize_images.py for winning WebP derivatives.

Never rewrite URLs until every referenced byte is local and verified.

Freeze the URL contract

Centralize new permalink rules and turn every legacy route into a checked 301 or a deliberate 410. URL preservation is the SEO-critical path.

Fleet example: WalletRecovery owns permalinks in src/lib/routing.js and checks 53 active rules in deploy/redirects.map, which the Cloudflare build turns into _redirects; Vellum generates its _redirects from legacyRoutes.json.

A missing redirect map is not cleanup. It discards accumulated backlinks, bookmarks, and crawler history. On Workers static assets, _redirects caps at 2,000 static plus 100 dynamic rules and defaults to 302: write 301 on every permanent line.

Disable templating over imported bodies

Third-party or WordPress-exported bodies may contain braces, shortcodes, or syntax that resembles executable templates.

// Eleventy: treat imported Markdown as content, not code
markdownTemplateEngine: false

Fleet example: WalletRecovery disables Markdown templating; Vellum disables both Markdown and HTML templating while .njk pages still render as Nunjucks.

The Eleventy default for Markdown and HTML preprocessing is Liquid. Leaving it on can mutate or execute imported text.

Normalize dates and frontmatter

Coerce WordPress strings such as 2026-07-09 14:30:00 to explicit UTC dates and validate required metadata.

Fleet example: WalletRecovery’s custom YAML parser converts that form to 2026-07-09T14:30:00Z.

Locale-dependent parsing can change dates across Windows, Linux, or server time zones.

Replace dynamic features explicitly

Assign a static-era answer to forms, search, comments, newsletters, account pages, and scheduled publication. See the next section.

Fleet example: one PHP contact endpoint survived WalletRecovery’s WordPress exit, then became a forms Worker with a D1 log when the site left nginx; Vellum routes its investor inquiry to a Worker.

“Static” describes page delivery, not an obligation to delete every server or edge function.

Own RSS, sitemap, robots, and AI routes

Generate or hand-author discovery files and test their URLs and MIME types.

Fleet example: Vellum uses @11ty/eleventy-plugin-rss; Astro sites use @astrojs/sitemap; WalletRecovery hand-builds feed/sitemap templates and serves llms.txt plus llms-full.txt.

A valid XML file at the wrong URL is still a broken feed.

Put quality gates inside the build

Make broken claims, SEO metadata, links, redirects, routes, schema, and asset references fail before transfer.

Fleet example: davidveksler.com runs lint-claims.mjs, Astro build, then seo-check.mjs in one npm run build. WhoPaysForAI’s build runs its SEO checker too.

A check documented but not wired into CI/build will eventually be skipped.

Cut over, verify, archive, decommission

Build the final snapshot, swap the origin (vhost, DNS, or edge route), test top routes and 404s through Cloudflare, preserve the export, then remove the old app.

Fleet example: guarded deploy scripts upload a preview version, diff it against production, promote it, then poll the live URL for distinctive content. WhoPaysForAI also matches the public homepage to the promoted version, so a stale edge copy fails closed.

Keep the old WordPress private until redirects, forms, feeds, and live verification pass. Then retire PHP/MySQL/plugin patching and the admin-login surface.

04 / The dynamic 10%

How do you replace WordPress forms, search, and RSS?

Keep the document static and isolate the mutation or query behind the smallest tool that can own it.

Contact / lead form

need → answer: validate one POST with a tiny PHP handler or edge Worker; verify Turnstile server-side.

Fleet: WalletRecovery workers/forms on /api/* (contact form and tool beacon, rows in D1); Vellum workers/investor-inquiry.

A Turnstile widget without Siteverify validation protects nothing.

Search

need → answer: ship a bounded JSON index and query it in the browser.

Fleet: CFW uses Fuse.js 7 for the firearm lookup with a deliberately tuned threshold.

Do not ship private fields or a multi-megabyte corpus merely because client search is convenient.

Newsletter / alerts

need → answer: accept subscriptions at the edge; let a reviewed workflow render and dispatch alerts.

Fleet: CFW’s alert workflow is event-driven and writes evidence/status back through repository-controlled paths.

Subscriber PII never belongs in git.

Scheduled freshness

need → answer: cron Actions fetch authoritative sources, diff versioned data, and open PRs or stage records.

Fleet: CFW has four active scheduled watcher/recheck workflows plus an event-driven alert dispatcher; WhoPaysForAI ingests RSS twice daily into a JSONL corpus.

Scheduled ingest is not automatic truth. Keep provenance, anomaly limits, and publication gates.

Comments

need → answer: drop them when they are not the product; otherwise outsource moderation and identity.

Fleet: none of these static reference sites needs a local comment database.

A third-party comment widget adds privacy, performance, and moderation costs.

RSS / sitemap / robots

need → answer: emit static discovery documents during the build from the same route/content data.

Fleet: hand-built Nunjucks, Eleventy RSS plugin, and Astro sitemap integration all coexist.

Test generated URLs after redirects, not just XML syntax.

AI-agent discovery

need → answer: publish a concise llms.txt, optionally a full content map, and link it from page metadata/robots.

Fleet: WalletRecovery and Vellum treat both routes as first-class build outputs.

Discovery files summarize public content; they are not a place for prompts, secrets, or private runbooks.

05 / Fleet control board

Five sites, three generators, one deploy pattern

Repository-authoritative snapshot: counts and versions as of 2026-08-14, deploy paths as of 2026-09-28. The counts and versions will drift; the commands and approval boundaries are the durable part.

SiteGeneratorContent modelScaleBuild / testQuality + CIDeploy patternDynamic companionsPrimary gotcha
coloradofirearmswatch.orgAstro 7.1.6 + React 19 + Fuse.js 7data/ JSON is primary; one updates Markdown collection872 current firearm records; 64 counties; 6 litigation casesnpm run build
npm test
Vitest; CI requires ≥64 county and ≥100 firearm routesGuarded local build → preview version → parity check → promote → live verifyWatchers, alert dispatch, intake servicesConfig comments have lagged hosting twice; deploy scripts are authoritative.
davidveksler.comAstro 5.18.2work/ + pages/ Markdown; JSON evidence corpus7 case studies; 6 pages; 46 evidence recordsnpm run buildClaims linter + Astro + SEO checker in the buildGuarded local build → preview version → parity check → promote → live verifySeparate AI Worker, outside site buildAn uncited or invalid-status claim fails the build by design.
WalletRecovery.infoEleventy 3.1.6, CommonJS28 Markdown pages + 49 posts; YAML frontmatter77 content files; 53 checked redirect-map rulesnpm run build
python scripts/check_links.py
Python link and redirect checkersSame Workers pipeline; _redirects generated from the redirect mapForms Worker (contact + beacon, D1) + TurnstileBranch is master; Markdown templating is off intentionally.
vellum.capitalEleventy 3.1.6, ESM48 WordPress-exported HTML posts; 15 Nunjucks pages44.4 MB WXR archive; complete legacy/media route preservationnpm run build
npm run check
Build validator + Worker testsSame Workers pipeline; a legacy push-to-build hook serves the editorial serviceInvestor-inquiry WorkerDeploy Worker before site; both content template engines are off.
whopaysforai.orgNext.js 16.3.0 via vinext; React 19.2.8TypeScript modules; generated news module from JSONL6,034 corpus records; generated module is 5,433 linesnpm run build
npm test
npm run check
SEO, lint, typecheck, Node tests, dependency auditsBuild → local server → crawl flat HTML → preview → parity → promote → version-matched live checkIntake Worker; twice-daily news ingestD1/R2/Drizzle are unused scaffolding; never hand-edit generated news data.

06 / Operator cards

Open a repo cold and operate it

These native accordions are mutually exclusive. Commands are safe local operations unless a deploy line explicitly says it crosses the human approval gate.

RACK 01 · CFW
Structured law/data reference with selective React islands
Astro 7.1.6

Mental model

Git is the database. Typed, absent-file-tolerant loaders read committed JSON at build time; React appears only on named interactive views.

Add content

Edit versioned JSON under data/ for lists, counties, litigation, training, or burden records. Human-readable updates live in src/content/updates/.

Local commands

npm run dev
npm run build
npm test

Deploy

scripts/deploy-cloudflare.sh or .ps1 builds, uploads a preview version, diffs it against production, promotes, and verifies live. The older deploy.sh/.ps1 only refresh the droplet copy kept for rollback. Explicit approval required.

Quality gate

Vitest covers search/slug/contrast logic. CI fails if fewer than 64 county or 100 firearm routes build, catching empty-loader regressions.

Sharp edges

  • astro.config.mjs comments have described the wrong host twice (Cloudflare Pages, then the droplet); read the deploy scripts.
  • Public /data/ JSON needs its CORS header restated in _headers; nginx used to add it.
  • Five operational automations surround the site: four scheduled watchers/rechecks plus event-driven alert dispatch.
  • Pipeline changes arrive through PRs; do not bypass provenance fields.
RACK 02 · DAVIDVEKSLER.COM
Static portfolio where every material claim has evidence
Astro 5.18.2

Mental model

Markdown presents the work; src/data/evidence.json supplies the claim corpus and allowed status vocabulary.

Add content

Add a case study to src/content/work/ or a page to src/content/pages/. Resolve every evidence id and date/grade the claim.

Local commands

npm run dev
npm run lint:claims
npm run build

Deploy

npm run deploy (deploy:win on PowerShell) runs the guarded Workers pipeline: build, preview version, parity check, promote, live verify. deploy:droplet is the legacy rsync path. Explicit approval required.

Quality gate

npm run build means claims linter → Astro build → SEO checker. Current corpus: 7 work items, 6 pages, 46 evidence records.

Sharp edges

  • Build failure on an uncited/invalid claim is the intended control.
  • /david/ai-strategy.html answers a 301 at the edge; its noindexed stub file stays out of the sitemap.
  • The AI Worker is a separate layer, not part of this static build.
RACK 03 · WALLETRECOVERY.INFO
WordPress content rebuilt as Markdown around explicit routing rules
Eleventy 3.1.6

Mental model

Editable content is 28 page and 49 post Markdown files. Layouts and data live under src/; src/lib/routing.js owns publication/permalinks.

Add content

Add content/posts/<slug>.md with YAML frontmatter and status: publish, then build and run the link checker.

Local commands

npm run build
python scripts/check_links.py
python scripts/check_redirects.py

Deploy

scripts/deploy-cloudflare.sh builds _site/, generates _redirects from deploy/redirects.map, then runs preview, parity, promote, and live verify. scripts/deploy-forms-cloudflare.sh ships the forms Worker separately. Each is an approval gate.

Quality gate

Python crawlers verify internal links/assets and the 53 active redirect-map rules. Build output is _site/.

Sharp edges

  • The branch is master, not main.
  • markdownTemplateEngine: false protects imported bodies.
  • The PHP contact endpoint became a forms Worker on /api/* that verifies Turnstile and logs to D1; “no CMS” never meant “no server code.”
RACK 04 · VELLUM.CAPITAL
WordPress-exported HTML preserved behind a small Nunjucks shell
Eleventy 3.1.6

Mental model

Forty-eight imported HTML posts remain close to source; 15 .njk pages and layouts provide the new site shell. Root wp-content/ passes through unchanged.

Add content

For a post, edit/add content/posts/*.html with frontmatter. For a designed route, edit a Nunjucks page. Never run imported bodies through Nunjucks/Liquid.

Local commands

npm run serve
npm run build
npm run check
npm run test:worker

Deploy

Deploy the investor-inquiry Worker first, then scripts/deploy-cloudflare.sh. The older git push production main hook stays because the editorial service publishes through it; it is being extended to deploy the Worker too. Each is an approval gate.

Migration assets

The 44.4 MB WXR export remains in-repo. npm run import:wordpress and npm run import:media preserve a reproducible path from source.

Sharp edges

  • Both htmlTemplateEngine and markdownTemplateEngine are false.
  • legacyRoutes.json generates the load-bearing _redirects. Slashless legacy forms get explicit 301s, because Workers trailing-slash handling would answer 307.
  • If the form Worker is not live first, form POSTs reach a static-assets 404.
RACK 05 · WHOPAYSFORAI.ORG
React application source flattened into a verified static snapshot
Next 16.3 / vinext

Mental model

Routes are App Router modules; claims, grades, dates, and source URLs live in app/lib/content.ts. A deploy crawls the local vinext server into static files.

Add content

Edit authored TypeScript modules. The JSONL corpus is the news source of truth; app/lib/news-data.ts is deterministic generated output.

Local commands

npm run dev
npm run build
npm test
npm run check

Deploy

scripts/deploy-cloudflare.ps1 calls the guarded shell path: gates → build → start → crawl → add 404.html and a _redirects porting the nginx rules → preview → parity → promote → version-matched live check. deploy-server.* is the legacy droplet path. Explicit approval required.

News pipeline

Twice-daily ingest appends pending items to a 6,034-record JSONL corpus and does not regenerate the public module. Review or a separately governed high-bar auto-approval path controls publication.

Sharp edges

  • db/schema.ts is literally export {}; D1 and R2 bindings are null.
  • Never hand-edit the 5,433-line generated news-data.ts.
  • The first Workers live checks failed on every page over the 64 KB pipe buffer: echo "$BODY" | grep -q under pipefail exits 141 (SIGPIPE). It was first misread as slow edge propagation; a here-string fixed it.

07 / Shared infrastructure

Where does “static” still have moving parts?

On 2026-09-27 all five sites moved from one self-hosted nginx origin to Cloudflare Workers static assets, one Worker per site on a route over the existing proxied DNS record. The generators did not change; the artifacts stayed portable and the deploy gate stayed human.

Preview, parity, promote

Every site builds locally, uploads a Worker version with a preview URL, diffs it against production, and promotes only on confirmation. Rollback is wrangler rollback; removing the route hands traffic back to the old origin.

Snapshot first

WhoPaysForAI runs and crawls its built server into flat HTML before upload, then matches the public homepage to the promoted version by body hash, so a stale copy fails closed.

Server config became files

nginx redirects, try_files rules, security headers, and CORS became _redirects and _headers in the build output. Anything nginx added silently had to be restated.

Small Workers for the dynamic 10%

Forms and beacons are separate Workers with D1 storage and server-side Turnstile checks, deployed before the HTML that calls them. None of these sites uses Pages, Vercel, or Netlify.

One gate never automates

Every repo uses AGENTS.md as its only agent-instruction file. Builds and drafts automate; production deployment requires explicit human authorization.

08 / Failure modes

Common mistakes and anti-patterns

Most failed migrations are not generator failures. They are missing contracts between content, URLs, automation, and deployment.

1. Execute imported content as templatesWordPress/third-party text can contain braces that explode the build or become injection. Disable the content-path engine; keep templating in controlled layouts.
2. Drop old URLsRedesigning slugs without a complete, tested redirect map is an SEO extinction event. Preserve or 301 every valuable route.
3. Trust comments over executable scriptsCFW’s config comment named Cloudflare Pages while deploys went to an nginx origin, and still named that origin after the Workers cutover. Code and current runbooks beat stale prose.
4. Hand-edit generated filesWhoPaysForAI’s news module and CFW’s emitted public data have source pipelines. Edit the corpus/input, regenerate, and commit the pair.
5. Publish origin infrastructure detailsA Cloudflare-fronted site should not expose origin IPs, SSH identities, zone IDs, credentials, or real server paths in public docs.
6. Assume scaffolding is production architectureA dependency or folder proves nothing. WhoPaysForAI contains Drizzle/D1/R2 starter code while the schema is empty and bindings are disabled.
7. Deploy the site before its form WorkerA static origin cannot answer the route by accident. Deploy and verify the Worker first, then publish HTML that points at it.
8. Equate static with no moving partsThe motion moved from every HTTP request to builds, scheduled watchers, ingest, review, cache, and deploy verification. Operate those explicitly.
9. Change hosts without an inventory of implicit behaviorMoving off nginx dropped X-Frame-Options, nosniff, and Referrer-Policy on three sites, and CFW’s JSON CORS header, until each was restated in _headers. Workers answers trailing-slash fixes with 307 and _redirects defaults to 302. Diff a preview against production before cutover.
10. Blame the platform before testing the checkerThe fleet’s post-deploy checks failed repeatedly on 2026-09-28 and were read as edge lag, prompting longer timeouts and a cache purge. The cause was the check itself: a pipe into grep -q under pipefail fails on any page over 64 KB. Prove a verifier passes on a known-good page first.

09 / Verification

Primary sources and further reading

Fleet facts come from each repository’s package manifest, resolved dependency tree, configuration, content directories, tests, workflows, and guarded deploy scripts.