Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Deploy to Vercel
name: Deploy to Cloudflare

on:
push:
Expand All @@ -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 }}
28 changes: 26 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -26,9 +26,33 @@ 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 (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 |
Expand Down
4 changes: 2 additions & 2 deletions open-next.config.ts
Original file line number Diff line number Diff line change
@@ -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
})
Comment on lines 4 to 6
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in 71399b4.github/workflows/deploy.yml now deploys to Cloudflare Workers via pnpm cf:deploy on push to master/main, using CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID secrets. package.json's deploy script and the hosting section of CLAUDE.md were updated to match.


Generated by Claude Code

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
"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",
"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'",
Expand Down
55 changes: 47 additions & 8 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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 |
|----------|----------|-------------|
Expand All @@ -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.
Expand Down
46 changes: 46 additions & 0 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
@@ -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
8 changes: 5 additions & 3 deletions wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
"observability": {
"enabled": true
},
"kv_namespaces": [
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_KV",
"id": "77055535c51e442193e5dad69ceb5075"
// 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"
}
Comment on lines +19 to 25
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in 71399b4 — added a JSONC comment on the NEXT_INC_CACHE_R2_BUCKET binding documenting the bucket prereq (npx wrangler r2 bucket create techempower-cache). Skipping preview_bucket_name for now since pnpm cf:preview uses miniflare's local R2 emulator.


Generated by Claude Code

]
}
Loading