From d32f8dc279eed924ae913dab6587b069260673c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 02:39:06 +0000 Subject: [PATCH 1/4] Switch OpenNext incremental cache from KV to R2 KV has per-day operation limits on the Cloudflare free plan; R2 does not, and includes 10M Class B reads/month free. Swap the incremental cache override in open-next.config.ts, and replace the kv_namespaces binding with an r2_buckets binding pointing to the techempower-cache bucket. Requires the R2 bucket to exist before deploy: npx wrangler r2 bucket create techempower-cache https://claude.ai/code/session_01EtZ6fVjJdyAqDR3WbiAqm3 --- open-next.config.ts | 4 ++-- wrangler.jsonc | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/open-next.config.ts b/open-next.config.ts index 18ca3d0..3f7fe37 100644 --- a/open-next.config.ts +++ b/open-next.config.ts @@ -1,6 +1,6 @@ import { defineCloudflareConfig } from '@opennextjs/cloudflare' -import kvIncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache' +import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache' export default defineCloudflareConfig({ - incrementalCache: kvIncrementalCache + incrementalCache: r2IncrementalCache }) diff --git a/wrangler.jsonc b/wrangler.jsonc index 63f136a..6f770f7 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -16,10 +16,10 @@ "observability": { "enabled": true }, - "kv_namespaces": [ + "r2_buckets": [ { - "binding": "NEXT_INC_CACHE_KV", - "id": "77055535c51e442193e5dad69ceb5075" + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "techempower-cache" } ] } From 71399b4517c607babb46fa1733dcf440a65e5ff3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 03:16:15 +0000 Subject: [PATCH 2/4] Switch CI/CD and docs from Vercel to Cloudflare Workers - .github/workflows/deploy.yml now runs `pnpm cf:deploy` on push to master/main using CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID secrets, replacing the Vercel action. - package.json: `deploy` script points at `opennextjs-cloudflare deploy`; the vercel dep was not in devDependencies so nothing to remove there. - CLAUDE.md: hosting section and commands list now reflect Cloudflare Workers + OpenNext as the production target. - wrangler.jsonc: add JSONC comment documenting the R2 bucket prereq on the NEXT_INC_CACHE_R2_BUCKET binding so new environments don't silently fail when the bucket is missing. Note: CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID must be configured as GitHub Actions repository secrets before the next push to master. https://claude.ai/code/session_01EtZ6fVjJdyAqDR3WbiAqm3 --- .github/workflows/deploy.yml | 21 +++++++++++++-------- CLAUDE.md | 6 ++++-- package.json | 2 +- wrangler.jsonc | 2 ++ 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4c214ea..94153cf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Deploy to Vercel +name: Deploy to Cloudflare on: push: @@ -14,11 +14,16 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Deploy to Vercel - uses: amondnet/vercel-action@v25 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: - vercel-token: ${{ secrets.VERCEL_TOKEN }} - vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} - vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} - vercel-args: '--prod' + node-version: 22 + cache: "pnpm" + + - run: pnpm install --frozen-lockfile --strict-peer-dependencies + + - name: Deploy to Cloudflare Workers + run: pnpm cf:deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/CLAUDE.md b/CLAUDE.md index 88e98e2..69fc4d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ A Next.js site that renders content from Notion as a CMS, using react-notion-x. - **Framework:** Next.js (Pages Router, SSR), React 19 - **CMS:** Notion via react-notion-x -- **Hosting:** Vercel (auto-deploy on push to `master`) +- **Hosting:** Cloudflare Workers via OpenNext (auto-deploy on push to `master`) - **Styling:** CSS Modules + global CSS custom properties (warm earth-tone design system) - **Fonts:** Fraunces (display), DM Sans (body) - **Package manager:** pnpm (Node >= 20) @@ -26,7 +26,9 @@ pnpm install # install deps (runs patch-package postinstall) pnpm dev # dev server at localhost:3000 pnpm build # production build pnpm start # serve production build -npx vercel --prod --yes # manual deploy to Vercel +pnpm cf:build # build OpenNext Cloudflare worker bundle +pnpm cf:preview # preview worker locally via miniflare +pnpm cf:deploy # manual deploy to Cloudflare Workers ``` ## Key Files diff --git a/package.json b/package.json index 9974766..9727703 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,10 @@ "dev": "next dev", "build": "next build", "start": "next start", - "deploy": "vercel deploy", "cf:build": "opennextjs-cloudflare build", "cf:preview": "opennextjs-cloudflare preview", "cf:deploy": "opennextjs-cloudflare deploy", + "deploy": "opennextjs-cloudflare deploy", "deps:upgrade": "[ -z $GITHUB_ACTIONS ] && pnpm up -L notion-client notion-types notion-utils react-notion-x || echo 'Skipping deps:update on CI'", "deps:link": "[ -z $GITHUB_ACTIONS ] && run-s deps:link:* || echo 'Skipping deps:update on CI'", "deps:unlink": "[ -z $GITHUB_ACTIONS ] && pnpm add notion-client notion-types notion-utils react-notion-x || echo 'Skipping deps:update on CI'", diff --git a/wrangler.jsonc b/wrangler.jsonc index 6f770f7..604ffdf 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -18,6 +18,8 @@ }, "r2_buckets": [ { + // Bucket must be pre-created in the Cloudflare account before deploy: + // npx wrangler r2 bucket create techempower-cache "binding": "NEXT_INC_CACHE_R2_BUCKET", "bucket_name": "techempower-cache" } From 7dd232b49d2c44085be418884b16d1a874f42f5c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 03:52:57 +0000 Subject: [PATCH 3/4] Add Bitwarden-backed local deploy helper scripts/deploy.sh reads CLOUDFLARE_API_TOKEN (from the notes field) and CLOUDFLARE_ACCOUNT_ID (from the custom "id" field) out of the Bitwarden secure note "techempower cloudflare api", exports them, and runs pnpm cf:deploy. Requires the vault to be unlocked (BW_SESSION set) and bw + jq on PATH. New npm alias "deploy:local" wraps the script so the manual deploy flow from a dev machine is one command: `pnpm deploy:local`. CLAUDE.md gets a Deploying section documenting both the GitHub Actions auto-deploy path and the local Bitwarden-backed path, so Claude running in this repo knows which secrets live where. https://claude.ai/code/session_01EtZ6fVjJdyAqDR3WbiAqm3 --- CLAUDE.md | 24 +++++++++++++++++++++++- package.json | 1 + scripts/deploy.sh | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100755 scripts/deploy.sh diff --git a/CLAUDE.md b/CLAUDE.md index 69fc4d0..0d9ae0d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,9 +28,31 @@ pnpm build # production build pnpm start # serve production build pnpm cf:build # build OpenNext Cloudflare worker bundle pnpm cf:preview # preview worker locally via miniflare -pnpm cf:deploy # manual deploy to Cloudflare Workers +pnpm cf:deploy # manual deploy (requires CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID in env) +pnpm deploy:local # manual deploy with secrets pulled from Bitwarden (see Deploying) ``` +## Deploying + +CI auto-deploys on push to `master` via `.github/workflows/deploy.yml` using the +`CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` GitHub Actions secrets. + +For a manual deploy from a local machine, secrets live in Bitwarden: + +- Item: `techempower cloudflare api` (secure note) +- `notes` field → `CLOUDFLARE_API_TOKEN` +- custom field `id` → `CLOUDFLARE_ACCOUNT_ID` + +Unlock the vault once per shell, then run the helper script: + +```bash +export BW_SESSION=$(bw unlock --raw) +pnpm deploy:local +``` + +`scripts/deploy.sh` reads both values from Bitwarden, exports them, and runs +`pnpm cf:deploy`. Requires `bw` and `jq` on PATH. + ## Key Files | Path | Purpose | diff --git a/package.json b/package.json index 9727703..1bf2628 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "cf:preview": "opennextjs-cloudflare preview", "cf:deploy": "opennextjs-cloudflare deploy", "deploy": "opennextjs-cloudflare deploy", + "deploy:local": "bash scripts/deploy.sh", "deps:upgrade": "[ -z $GITHUB_ACTIONS ] && pnpm up -L notion-client notion-types notion-utils react-notion-x || echo 'Skipping deps:update on CI'", "deps:link": "[ -z $GITHUB_ACTIONS ] && run-s deps:link:* || echo 'Skipping deps:update on CI'", "deps:unlink": "[ -z $GITHUB_ACTIONS ] && pnpm add notion-client notion-types notion-utils react-notion-x || echo 'Skipping deps:update on CI'", diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..5f04d94 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Deploy to Cloudflare Workers using Cloudflare secrets pulled from Bitwarden. +# +# Prereqs: +# - Bitwarden CLI (`bw`) installed and logged in. +# - `jq` installed. +# - Vault unlocked: `export BW_SESSION=$(bw unlock --raw)` +# - Bitwarden item "techempower cloudflare api" (secure note) with: +# - notes -> CLOUDFLARE_API_TOKEN +# - custom "id" -> CLOUDFLARE_ACCOUNT_ID + +set -euo pipefail + +ITEM_NAME="techempower cloudflare api" + +if ! command -v bw >/dev/null 2>&1; then + echo "error: bw (Bitwarden CLI) not found on PATH" >&2 + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "error: jq not found on PATH" >&2 + exit 1 +fi + +bw_status="$(bw status 2>/dev/null || true)" +if ! printf '%s' "$bw_status" | grep -q '"status":"unlocked"'; then + echo "error: Bitwarden vault is locked (or bw is not logged in)." >&2 + echo " run: export BW_SESSION=\$(bw unlock --raw)" >&2 + exit 1 +fi + +item_json="$(bw get item "$ITEM_NAME")" +CLOUDFLARE_API_TOKEN="$(printf '%s' "$item_json" | jq -r '.notes // ""')" +CLOUDFLARE_ACCOUNT_ID="$(printf '%s' "$item_json" | jq -r '.fields[]? | select(.name=="id") | .value')" + +if [[ -z "$CLOUDFLARE_API_TOKEN" || -z "$CLOUDFLARE_ACCOUNT_ID" ]]; then + echo "error: could not read CLOUDFLARE_API_TOKEN (notes) or CLOUDFLARE_ACCOUNT_ID (custom field 'id')" >&2 + echo " from Bitwarden item '$ITEM_NAME'" >&2 + exit 1 +fi + +export CLOUDFLARE_API_TOKEN +export CLOUDFLARE_ACCOUNT_ID + +exec pnpm cf:deploy From 0fc68be2bfc738cbb170c039e7dcb6d8ae954a2f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 03:56:20 +0000 Subject: [PATCH 4/4] Update readme for Cloudflare Workers + R2 deploy - Tech Stack table: Vercel -> Cloudflare Workers via OpenNext; add R2 incremental cache row. - Useful commands table: drop vercel, add cf:build/cf:preview/cf:deploy and deploy:local. - Deployment section: document the GitHub Actions auto-deploy path (required secrets + token scopes), the Bitwarden-backed local deploy via `pnpm deploy:local`, and the `techempower-cache` R2 bucket prerequisite. - Environment Variables: split into app-level vs deploy-time tables; add CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID. https://claude.ai/code/session_01EtZ6fVjJdyAqDR3WbiAqm3 --- readme.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/readme.md b/readme.md index 5b450b1..272da07 100644 --- a/readme.md +++ b/readme.md @@ -29,7 +29,8 @@ This repository powers [techempower.org](https://techempower.org) — a Next.js |-------|-----------| | Framework | [Next.js](https://nextjs.org/) (Pages Router, SSR) | | CMS | [Notion](https://notion.so) via [react-notion-x](https://github.com/NotionX/react-notion-x) | -| Hosting | [Vercel](https://vercel.com) | +| Hosting | [Cloudflare Workers](https://workers.cloudflare.com/) via [OpenNext](https://opennext.js.org/cloudflare) | +| Incremental cache | [Cloudflare R2](https://developers.cloudflare.com/r2/) (bucket: `techempower-cache`) | | Styling | CSS Modules + global CSS custom properties | | Fonts | Fraunces (display), DM Sans (body) | | Analytics | Google Analytics, Fathom (optional), PostHog (optional) | @@ -104,25 +105,56 @@ site.config.ts Notion page IDs, navigation, site metadata | Command | Description | |---------|-------------| | `pnpm dev` | Start local dev server | -| `pnpm build` | Production build | +| `pnpm build` | Production build (Next.js) | | `pnpm start` | Serve production build locally | | `pnpm format` | Format code with Prettier | -| `pnpm deploy` | Deploy to Vercel | +| `pnpm cf:build` | Build the OpenNext Cloudflare worker bundle | +| `pnpm cf:preview` | Preview the worker locally via miniflare | +| `pnpm cf:deploy` | Deploy to Cloudflare Workers (requires env secrets) | +| `pnpm deploy:local` | Deploy with secrets pulled from Bitwarden (see [Deployment](#deployment)) | ## Deployment -The site deploys to **Vercel**. Push to `master` to trigger automatic deployment. +The site deploys to **Cloudflare Workers** via [OpenNext](https://opennext.js.org/cloudflare). Push to `master` to trigger automatic deployment through [`.github/workflows/deploy.yml`](./.github/workflows/deploy.yml). + +### GitHub Actions (auto-deploy) + +Requires two repository secrets: + +- `CLOUDFLARE_API_TOKEN` — scoped to `Workers Scripts: Edit` (account), `Workers R2 Storage Bucket Item Write` on `techempower-cache`, and `Workers Routes: Edit` on the `techempower.org` zone. +- `CLOUDFLARE_ACCOUNT_ID` + +### Manual deploy from a developer machine + +Secrets live in a Bitwarden secure note named `techempower cloudflare api`: + +- `notes` field → `CLOUDFLARE_API_TOKEN` +- custom field `id` → `CLOUDFLARE_ACCOUNT_ID` + +Unlock the vault once per shell, then run: + +```bash +export BW_SESSION=$(bw unlock --raw) +pnpm deploy:local +``` + +[`scripts/deploy.sh`](./scripts/deploy.sh) reads both values from Bitwarden, exports them, and runs `pnpm cf:deploy`. Requires `bw` and `jq` on PATH. + +### Prerequisite: R2 bucket + +The incremental cache binding points at an R2 bucket that must exist in the target account: -Manual deploy: ```bash -npx vercel --prod --yes +npx wrangler r2 bucket create techempower-cache ``` -SSR pages include CDN caching headers (`s-maxage=3600, stale-while-revalidate=86400`) for fast subsequent loads. +### Caching + +SSR pages include CDN caching headers (`s-maxage=3600, stale-while-revalidate=86400`). The Next.js incremental cache is backed by R2 (see [`open-next.config.ts`](./open-next.config.ts) and the `r2_buckets` binding in [`wrangler.jsonc`](./wrangler.jsonc)). ## Environment Variables -Set these in your Vercel project settings or in a local `.env` file: +App-level variables (set in a local `.env` file for development, or via `wrangler secret put` / Cloudflare dashboard for production): | Variable | Required | Description | |----------|----------|-------------| @@ -132,6 +164,13 @@ Set these in your Vercel project settings or in a local `.env` file: | `REDIS_HOST` | No | Redis host for preview image caching | | `REDIS_PASSWORD` | No | Redis password | +Deploy-time variables (GitHub Actions secrets / local shell): + +| Variable | Required | Description | +|----------|----------|-------------| +| `CLOUDFLARE_API_TOKEN` | Yes | Token with Workers Scripts Edit + R2 bucket write + Workers Routes Edit | +| `CLOUDFLARE_ACCOUNT_ID` | Yes | Cloudflare account ID | + ## Contributing See [contributing.md](./contributing.md) for development setup and guidelines.