Cloudflare Workers

The Website runs as a single Cloudflare Worker (swapps-client). The Worker is the application server: it serves static assets, applies redirects and security headers, edge-caches blog pages, and renders pages with Vike SSR.

The entry point is src/worker.ts, bundled to dist/worker.js by esbuild (scripts/build-worker.ts) and declared as main in wrangler.toml.

Wrangler bindings

name = "swapps-client"
main = "dist/worker.js"

[assets]
directory = "dist/client"
binding = "ASSETS"

[[services]]
binding = "API_WORKER"
service = "swapps-worker"
Binding Type Purpose
ASSETS Static Assets Serves files from dist/client
API_WORKER Service Binding Worker-to-Worker calls to the Platform (swapps-worker)

Edge routing

On the swapps.com zone, the React site coexists with WordPress on the same hostname. Cloudflare resolves this with Workers Routes: the swapps-client Worker captures the site via a wildcard route plus explicit per-section routes; WordPress paths run with "Workers disabled" and fall through to the origin (Pantheon).

Routes served by swapps-client

Pattern Worker
swapps.com swapps-client
swapps.com/* swapps-client
swapps.com/assets/* swapps-client
swapps.com/api* swapps-client
swapps.com/about-us* swapps-client
swapps.com/services* swapps-client
swapps.com/blog* swapps-client
swapps.com/case-studies* swapps-client
swapps.com/case-studies/sector* swapps-client
swapps.com/case-studies/tech* swapps-client
swapps.com/case-studies/service* swapps-client
swapps.com/get-swapps* swapps-client
swapps.com/support-center* swapps-client
swapps.com/style-guide* swapps-client
swapps.com/es* swapps-client

The wildcard swapps.com/* already captures the site; the explicit routes pin those sections to the Worker even when more specific "Workers disabled" routes exist on the same domain.

Routes NOT served by the Worker (origin / other Workers)

Pattern Handled by
swapps.com/api/ai/* swapps-ai Worker
swapps.com/wp-content/*, /wp-admin/*, /wp-includes/*, /wp-login.php* WordPress (origin)
swapps.com/wp-json/* WordPress REST API (origin)
swapps.com/wp-cron.php, /health WordPress (origin)
swapps.com/sitemap.xml, /sitemap_index.xml, /post-sitemap.xml, /author-sitemap.xml Yoast / origin

Note

Mental model: on swapps.com, WordPress paths (/wp-*, plus /health) run "Workers disabled" and fall to origin; everything else is the Worker. When adding a new site section, the wildcard usually covers it. When exposing something new from WordPress, add its route with "Workers disabled". The full zone configuration (48 routes, WAF, DNS) lives in the platform CLOUDFLARE.md.

Request lifecycle

src/worker.ts's fetch handler runs these stages in order:

  1. Blog cache-purge webhookPOST /api/blog-cache/purge (auth via X-Cache-Api-Key matching CACHE_API_SECRET); handled first so the body/method survive.
  2. WordPress permalink redirect — legacy ?p= / Yoast AMP query shapes → /.
  3. Legacy redirects — old WordPress URLs via checkLegacyRedirect().
  4. Trailing-slash redirect — append a trailing slash (301) outside assets/API/files.
  5. Canonical redirect — redirect non-canonical hosts to BASE_URL (skipped in dev / workers.dev).
  6. Sentry tunnel/api/events proxies Sentry envelopes to bypass ad blockers.
  7. SEO files/robots.txt, /sitemap.xml, /page-sitemap.xml, /page-sitemap.xsl.
  8. Static assetsisStaticAsset() paths served via the ASSETS binding with tuned cache headers.
  9. Blog edge cache — anonymous blog-post URLs are served from caches.default if cached.
  10. SSRrenderPage() (Vike) renders the page; 404s re-check legacy redirects; successful blog renders are written back to the edge cache.

Security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) are added to every response by addSecurityHeaders().

SSR at the edge

renderSSRPage() builds the Vike pageContextInit with urlOriginal, the original request headers, the runtime env (BASE_URL, API_URL, NODE_ENV), and the Cloudflare bindings — crucially API_WORKER — so that +data.ts files can reach the Platform Worker through the Service Binding during SSR. React 19 requires a MessageChannel polyfill in the Workers runtime, which the Worker defines at module load.

Edge caching for the blog

Anonymous blog-post pages are cached in caches.default:

  • Cacheable = GET + a final blog-post URL (/blog/<slug>/ or /es/blog/<slug>/), not a listing/taxonomy root, and no wordpress_logged_in cookie.
  • Cache key drops the query string, so tracking params (utm_*, fbclid, …) never fragment or bust the cache.
  • TTL is 7 days (s-maxage), acting as a safety net; the WordPress publish webhook actively purges a post on edit/delete (locally via caches.default.delete() and globally via the Cloudflare Cache Purge API).

See Blog system for the end-to-end flow.

Observability

Runtime logs are enabled in wrangler.toml ([observability.logs] enabled = true). Application errors are tracked in Sentry, with events tunneled through the Worker's /api/events endpoint.

Next steps