Skip to content

Migrate from Vercel to Cloudflare Workers via OpenNext#1

Merged
jphein merged 2 commits intomasterfrom
feat/cloudflare-migration
Apr 19, 2026
Merged

Migrate from Vercel to Cloudflare Workers via OpenNext#1
jphein merged 2 commits intomasterfrom
feat/cloudflare-migration

Conversation

@jphein
Copy link
Copy Markdown
Owner

@jphein jphein commented Apr 19, 2026

Summary

Moves hosting off Vercel (free-tier warnings) and onto Cloudflare Workers with static assets via @opennextjs/cloudflare. Same Next.js app, same Notion-as-CMS architecture, ISR preserved using a Workers KV namespace as the incremental cache backend.

Preview deploy: https://techempower.jp5.workers.dev — fully green.

Key changes

  • Adapter: @opennextjs/cloudflare + wrangler as devDeps; cf:build / cf:preview / cf:deploy scripts
  • Worker config: wrangler.jsonc with nodejs_compat, static assets binding, KV binding NEXT_INC_CACHE_KV for ISR, workers_dev: true
  • Transpile fix: Next 16's Turbopack externalizes ESM packages, but Workers has no node_modules at runtime — added all runtime deps to transpilePackages in next.config.js
  • Preview images: Disabled on Workers (sharp is a native binary, can't run in Workers). Made lqip-modern import lazy in lib/notion.ts so it's only loaded when the feature flag is on.
  • Vercel → Cloudflare env: pages/api/version.ts now reads WORKERS_CI_COMMIT_SHA / CF_PAGES_COMMIT_SHA (with VERCEL_GIT_COMMIT_SHA fallback for rollback week). Same in lib/config.ts.

Test plan

Smoke-tested on the preview URL — all routes return 200:

  • / homepage (16 KB, correct title + meta)
  • /guides/how-to-use-techempower (383 KB, Notion blocks rendered)
  • /guides/free-internet (684 KB)
  • /resources (2.6 MB, heavy ISR page)
  • /about, /donate
  • /sitemap.xml, /feed
  • /api/version (realm-sigil responds as forge realm)
  • /api/resources-more (2.4 MB)
  • /api/search-notion — 500, but also broken on Vercel prod (Notion upstream 503). Pre-existing bug, not a regression.
  • /api/social-image — 500, also broken on Vercel prod. Pre-existing bug.

Cold response times: 0.4s homepage / 1–2s guides / 2.8s resources. KV cache will cut repeat hits to <100ms.

Follow-ups (not in this PR)

  • Attach techempower.org as a Workers custom domain + DNS cutover
  • Wire Cloudflare Workers Builds to git-push so WORKERS_CI_COMMIT_SHA populates and /api/version reports the real commit hash
  • After ~1 week of Cloudflare uptime: delete .vercel/, remove deploy: vercel deploy script, strip VERCEL_* fallback code
  • File separate bugs for /api/search-notion and /api/social-image (both broken on Vercel today)

🤖 Generated with Claude Code

Deploys the site as a Cloudflare Worker with static assets using
@opennextjs/cloudflare, replacing the Vercel serverless runtime.

- Add @opennextjs/cloudflare + wrangler; cf:build/preview/deploy scripts
- wrangler.jsonc: Worker name techempower, nodejs_compat, KV binding
  NEXT_INC_CACHE_KV for ISR, workers_dev subdomain enabled
- open-next.config.ts: Cloudflare KV incremental cache override
- next.config.js: transpilePackages for all runtime deps so Next 16's
  Turbopack inlines them (Workers has no node_modules at runtime)
- lib/notion.ts: lazy import preview-images so lqip-modern/sharp are
  only loaded when isPreviewImageSupportEnabled is true (sharp can't
  run on Workers)
- site.config.ts: disable preview images on Workers (no sharp, no Redis)
- pages/api/version.ts: drop child_process/os, read WORKERS_CI_COMMIT_SHA
  / CF_PAGES_COMMIT_SHA with Vercel fallback during transition
- lib/config.ts: add CF_PAGES_URL fallback alongside VERCEL_URL

Preview deploy verified green at techempower.jp5.workers.dev on all
routes (homepage, guides, /resources, sitemap, feed, /api/version).
Domain cutover (techempower.org) deferred to a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 19, 2026 17:55
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates the app’s hosting from Vercel to Cloudflare Workers using OpenNext, including Workers/KV configuration and runtime adjustments to keep ISR working.

Changes:

  • Added Cloudflare Workers/OpenNext configuration (wrangler.jsonc, open-next.config.ts) and new cf:* scripts/deps.
  • Updated runtime configuration/env handling for Cloudflare (commit metadata + API host resolution).
  • Adjusted Next.js bundling and Notion integration to avoid Workers-incompatible preview image dependencies.

Reviewed changes

Copilot reviewed 7 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
wrangler.jsonc Defines the Cloudflare Worker entrypoint, assets binding, observability, and KV namespace for ISR cache.
open-next.config.ts Configures OpenNext to use a KV-backed incremental cache.
package.json Adds OpenNext/Wrangler dev dependencies and Cloudflare build/preview/deploy scripts.
next.config.js Expands transpilePackages to inline runtime deps for Workers (no node_modules at runtime).
site.config.ts Disables preview image support (LQIP) in site config.
lib/notion.ts Lazy-loads preview image module only when the feature flag is enabled.
lib/config.ts Switches API host computation to prefer CF_PAGES_URL with Vercel fallback.
pages/api/version.ts Removes Node-only runtime introspection; uses Cloudflare/Vercel env vars for commit info and reports platform.
.gitignore Ignores OpenNext/Wrangler build artifacts and local dev vars.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread site.config.ts
Comment on lines +29 to 31
// LQIP preview images — disabled on Cloudflare Workers (requires `sharp` native binary)
isPreviewImageSupportEnabled: false,

Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isPreviewImageSupportEnabled is now hardcoded to false in the shared site.config.ts, which disables LQIP preview images for all deployments (local/dev, Vercel rollback, etc.), not just Cloudflare Workers. If the intent is "disable on Workers only", consider deriving this value from an environment/platform check (or a separate config override) so non-Workers environments can still enable preview images without editing source.

Copilot uses AI. Check for mistakes.
Comment thread pages/api/version.ts
Comment on lines 26 to 39
function gitInfo() {
// Vercel provides these env vars at build time (frozen into the serverless bundle)
const sha = process.env.VERCEL_GIT_COMMIT_SHA;
const ref = process.env.VERCEL_GIT_COMMIT_REF;
const sha =
process.env.WORKERS_CI_COMMIT_SHA ||
process.env.CF_PAGES_COMMIT_SHA ||
process.env.VERCEL_GIT_COMMIT_SHA
const ref =
process.env.WORKERS_CI_COMMIT_REF ||
process.env.CF_PAGES_BRANCH ||
process.env.VERCEL_GIT_COMMIT_REF
if (sha) {
return { hash: sha.slice(0, 7), branch: ref || 'unknown', dirty: false };
return { hash: sha.slice(0, 7), branch: ref || 'unknown', dirty: false }
}
// Fallback: try git (works locally, not on Vercel)
const info = { hash: 'dev', branch: 'unknown', dirty: false };
try {
info.hash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { encoding: 'utf8' }).trim() || 'dev';
info.branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8' }).trim() || 'unknown';
try { execFileSync('git', ['diff', '--quiet']); } catch { info.dirty = true; }
} catch {}
return info;
return { hash: 'dev', branch: 'unknown', dirty: false }
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gitInfo() no longer attempts a local git fallback, so /api/version will always report hash: "dev" when running locally even if a .git directory is present. If you still want accurate local version info without breaking Workers, consider adding an optional Node-only fallback using a dynamic import of child_process (and guard it behind detectPlatform() === "local" or a similar runtime check).

Copilot uses AI. Check for mistakes.
- Reformat pages/api/version.ts array literals per prettier
- Use Number.parseInt (unicorn/prefer-number-properties)
- Add .next/, .open-next/, .wrangler/ to eslint.config.js ignores so
  the OpenNext build output doesn't get linted locally

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jphein jphein merged commit 8c0c6a9 into master Apr 19, 2026
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.

2 participants