Skip to content

Move hosting to Cloudflare Workers - #28

Merged
dyascj merged 3 commits into
mainfrom
feat/cloudflare-migration
Jul 31, 2026
Merged

Move hosting to Cloudflare Workers#28
dyascj merged 3 commits into
mainfrom
feat/cloudflare-migration

Conversation

@dyascj

@dyascj dyascj commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Moves the site off Vercel onto Cloudflare Workers. Verified against the real runtime rather than just a build, and deployed to a preview at https://cjdyas-design.dyascj.workers.dev where the full e2e suite passes.

Three things only showed up on the Workers runtime, none of which a build would have caught:

  • SvelteKit imports node:async_hooks, so the Worker needs the nodejs_compat flag or it throws at runtime rather than failing the build.
  • Prerendering rss.xml turned it into a static file typed application/xml by extension, losing the application/rss+xml header some readers expect. That route stays dynamic.
  • Vercel Analytics posts to /_vercel/insights, which does not exist off Vercel. Left in it would have fired a failing request on every page view and collected nothing. Removed.

The contact form rate limiter also needed rethinking. It kept its counter in a module-level Map, which survives on a warm Node lambda but not on Workers, where each isolate gets its own empty counter and the limit barely applies. It now prefers the Workers rate limiting binding and falls back to the in-memory limiter locally, in tests, and if the binding throws. The binding only supports 10 or 60 second periods, so the window changes from 5 per 10 minutes to 5 per 60 seconds.

Prerendering: all content is bundled at build time through import.meta.glob, so nothing needed a per-request render. 19 routes are now static assets that Cloudflare serves without invoking a Worker. Only /contact and /rss.xml stay dynamic. This site was 43 percent of the Vercel account function CPU, almost all of it rendering pages that never change.

Playwright now takes an E2E_BASE_URL override so the suite can run against the Workers build or a live deployment instead of the Vite dev server.

Verification: 13 unit tests, typecheck clean, 28 e2e against local workerd, and 28 e2e against the live Cloudflare deployment.

Not done yet: secrets are not set on the Worker, so the contact form returns its not-configured path. DNS is untouched and the site still serves from Vercel.

dyascj added 3 commits July 31, 2026 12:17
Swaps adapter-vercel (pinned to nodejs22.x) for adapter-cloudflare and adds
wrangler config. Adds the nodejs_compat flag, which SvelteKit's server
runtime needs for node:async_hooks; without it the Worker throws at runtime
rather than failing the build.

The contact form rate limiter kept its counter in a module-level Map. That
survives on a warm Node lambda but not on Workers, where each isolate would
get its own empty counter and the limit would barely apply. checkRateLimit
now prefers the Workers rate limiting binding and falls back to the
in-memory limiter locally, in tests, and if the binding throws.

The binding's period is restricted to 10 or 60 seconds, so this is a
5-per-60s burst guard rather than the previous 5-per-10-minutes window.

Verified against the real workerd runtime with wrangler dev: every route
including sitemap, rss, robots and llms.txt, and a contact form POST that
exercised CSRF, the rate limit binding (5 through, 6th and 7th blocked) and
the Resend call.
All content is bundled at build time through import.meta.glob, so nothing
needed a per-request render. Prerendering the site turns 19 routes into
static assets, which Cloudflare serves free and without invoking a Worker.

Only two routes stay dynamic: /contact, because form actions need a live
handler, and /rss.xml, because as a static file it would be typed
application/xml by extension and lose the application/rss+xml header that
some feed readers expect. The e2e suite caught that one.

Dropped the s-maxage header on the homepage; it existed for Vercel's CDN
and a prerendered page never renders per request.

Also makes the Playwright base URL overridable via E2E_BASE_URL so the
suite can run against the Workers build rather than the Vite dev server.

Verified against workerd: 28 e2e, 13 unit, typecheck clean.
injectAnalytics posts to /_vercel/insights, which only exists on Vercel. Left
in place it would have produced failing requests and console noise on every
page view after the move, while collecting nothing.

Cloudflare Web Analytics covers the same ground from the dashboard with no
client code, if we want it back.

Also updates the README, which still described adapter-vercel.
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

Vercel didn’t deploy this pull request to the CJ Dyas' projects team.

GitHub couldn’t verify an account for commit 3448c49. Vercel blocks this deployment before it can verify the account’s access to the team.

Review the commit before changing settings. If you don’t recognize it, don’t deploy it. Ask a repository administrator to investigate.

If you do recognize it, verify the commit email in GitHub, connect that GitHub account to Vercel, then create a new commit and deploy again.

Check your GitHub commit email · Troubleshoot Vercel access

Commit: 3448c4978dc39f22bba06ff1adc585eaf229c241

@dyascj
dyascj merged commit ebeff3a into main Jul 31, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant