Services

2026 06 16 Opennext Pages Review


OpenNext / Cloudflare Pages review — academy and blog

Date: 2026-06-16 Scope: apps/academy and apps/blog OpenNext/Cloudflare implementation, plus the Pages service-binding surface that exposes blog under the www Pages app.

No Cloudflare APIs were called, no deployments were run, and no secrets were printed.

Guidance baseline#

  • Cloudflare Workers skill: Wrangler is the preferred deploy surface, routes should be declared in Wrangler config when Cloudflare owns the route, and DNS/hostnames must be coordinated with the registry when the registry owns the zone (cloudflare-workers/SKILL.md:30-32, :167-169, :313-335).
  • Cloudflare Storage skill: KV/D1/R2 bindings must be explicit in Wrangler config, tokens must not be printed, and storage mutations need appropriate least-privilege account scopes (cloudflare-storage/SKILL.md:31-39, :110-119, :304-318).
  • Cloudflare Pages skill: Pages/Workers deployment should prefer Wrangler, and OpenNext runtime errors should be handled by ensuring nodejs_compat and the correct KV/D1/R2 bindings are present (cloudflare-pages/SKILL.md:25-32, :328-330).
  • Inventory baseline: the prior inventory already called out that blog has a KV binding and academy configures a KV incremental cache override without a Wrangler KV binding (docs/cloudflare-review/2026-06-16-skill-inventory.md:72-79).

Summary#

  • apps/blog is mostly aligned with the current guidance: it builds for OpenNext, uses nodejs_compat, disables workers_dev, binds NEXT_INC_CACHE_KV, and is exposed through the www Pages service binding for /blog/*.
  • apps/academy still has one blocking deployment issue: OpenNext is configured to use the KV incremental cache, but Wrangler does not bind NEXT_INC_CACHE_KV.
  • I applied two tiny low-risk fixes:
    • Fixed apps/academy/package.json:12 so OPEN_NEXT=1 is present on the actual next build and OpenNext build commands.
    • Fixed the blog OpenNext comment at apps/blog/open-next.config.ts:9 to point to the real .docs/developer.md file.

Findings#

F1 — High, fixed in this change: academy build:cf did not apply OPEN_NEXT=1 to next build#

Evidence:

  • The academy Next config only disables the non-edge Redis cache handler when process.env.OPEN_NEXT is set (apps/academy/next.config.cjs:29-34).
  • Root Cloudflare docs say Workers apps set OPEN_NEXT=1 for the OpenNext build path (.docs/cloudflare-deployment.md:58-60).
  • The root mise Cloudflare build delegates to the app script (.mise.toml:1462-1464).
  • Current fixed script now runs content:build:fast, then OPEN_NEXT=1 next build, then OPEN_NEXT=1 npx opennextjs-cloudflare build ... (apps/academy/package.json:12).

Why it matters: with shell syntax like OPEN_NEXT=1 command_a && command_b, the environment assignment applies only to command_a. The previous academy script put OPEN_NEXT=1 on the content build step, leaving the actual Next build on the non-OpenNext cache-handler path.

Recommended follow-up: none for this specific issue. The narrow verification command below confirms the script now contains OPEN_NEXT=1 next build.

F2 — Blocker: academy OpenNext KV incremental cache has no NEXT_INC_CACHE_KV binding#

Evidence:

  • Academy explicitly enables the OpenNext KV incremental cache override (apps/academy/open-next.config.ts:5-7).
  • Academy Wrangler config has the worker entry, nodejs_compat, and assets binding, but no kv_namespaces section (apps/academy/wrangler.jsonc:3-10).
  • Repository deployment docs show Workers apps are expected to include kv_namespaces with NEXT_INC_CACHE_KV (.docs/cloudflare-deployment.md:92-100).
  • Historical academy migration docs record this exact blocker: No KV binding "NEXT_INC_CACHE_KV" found, with resolution to create a KV namespace and update apps/academy/wrangler.jsonc (docs/migrations/2026-04-05-learn-to-academy.md:215-229).
  • Storage guidance shows KV bindings belong in Wrangler config and require account-level KV/storage permissions (cloudflare-storage/SKILL.md:110-119, :304-318).

Impact: wrangler deploy for assistance-academy can build an OpenNext Worker that expects a KV binding which the runtime does not provide. The aggregate deploy preflight already anticipates OpenNext KV bulk upload requirements (.mise.toml:1477-1492), so academy is inconsistent with that deploy contract.

Recommended fix:

  1. Provision a production KV namespace with Wrangler using a least-privilege account token; do not print the token.
  2. Add the returned namespace id to apps/academy/wrangler.jsonc as:
    jsonc
    1
    "kv_namespaces": [
    2
    { "binding": "NEXT_INC_CACHE_KV", "id": "<production-kv-namespace-id>" }
    3
    ]
  3. If preview/staging deploys are required, add separate preview/staging namespaces instead of reusing production state.
  4. Run mise run cf:build:academy, then deploy only after token/binding setup is in place.

I did not apply this because the correct KV namespace id must come from Cloudflare and this task explicitly prohibited Cloudflare API calls/deploys.

F3 — Medium: academy production routing/exposure is not represented as desired state in Wrangler/docs#

Evidence:

  • Academy Wrangler config declares no workers_dev, routes, or custom-domain desired state (apps/academy/wrangler.jsonc:3-10).
  • The repo health task checks https://academy.assistance.bg/ (.mise.toml:1643-1649).
  • Academy _headers identifies https://academy.assistance.prod.assistance.bg/* as the production domain, not academy.assistance.bg (apps/academy/public/_headers:1-5).
  • The historical migration docs describe academy.assistance.prod.assistance.bg as a registry/Caddy-backed prod domain (docs/migrations/2026-04-05-learn-to-academy.md:41-46, :150-158).
  • Workers guidance says Cloudflare routes are preferably declared in Wrangler config, while registry-owned/internal hostnames need registry coordination (cloudflare-workers/SKILL.md:167-169, :313-335).

Impact: an operator reading the repo cannot tell whether academy is meant to be reached through a Cloudflare Worker route, a Cloudflare custom domain configured out-of-band, a workers.dev URL, or the registry/Caddy prod hostname. This makes deploy verification and rollback ambiguous.

Recommended fix: choose and document one source of truth. If academy is public on the Cloudflare-hosted assistance.bg zone, add the intended route/custom-domain desired state to Wrangler and align _headers/cf:health. If academy remains registry/Caddy-owned under prod.assistance.bg, update Cloudflare deployment docs and health checks to reflect that split.

F4 — Medium: blog route topology and health target disagree#

Evidence:

  • Blog is built with basePath: '/blog' (apps/blog/next.config.mjs:35).
  • Blog Wrangler disables workers_dev and has no routes (apps/blog/wrangler.jsonc:3-18).
  • The www Pages project binds BLOG_WORKER=assistance-blog (apps/www/wrangler.toml:11-15).
  • The Pages Function forwards /blog/* requests to that service binding as-is (apps/www/functions/blog/[[path]].ts:1-17).
  • Blog _headers treats https://www.assistance.bg/blog/* and https://assistance.bg/blog/* as production surfaces (apps/blog/public/_headers:1-10).
  • The root Cloudflare health task checks https://blog.assistance.bg/ instead (.mise.toml:1643-1649).

Impact: the implementation points to /blog under the main site as the canonical/blog production path, but the health task validates a different host and root path. If blog.assistance.bg exists out-of-band, it is not represented in repo config; if it does not, cf:health can report a false negative.

Recommended fix: either update cf:health and docs to check the canonical /blog/ route under www.assistance.bg / assistance.bg, or declare/document the blog.assistance.bg Cloudflare custom domain and decide whether the app should keep basePath: '/blog' on that host.

F5 — Low, fixed in this change: blog OpenNext comment referenced a non-existent docs file#

Evidence:

  • The actual follow-up note lives in .docs/developer.md:257.
  • The comment now references .docs/developer.md at apps/blog/open-next.config.ts:9.

Impact: small operator-discoverability issue only.

Positive checks#

  • Blog has nodejs_compat, compatibility date 2025-04-01, .open-next/assets, workers_dev: false, and NEXT_INC_CACHE_KV (apps/blog/wrangler.jsonc:3-18).
  • Blog uses the documented cloudflare.useWorkerdCondition=false workaround for the OpenNext copyWorkerdPackages issue (apps/blog/open-next.config.ts:9-15; .docs/developer.md:257).
  • Both apps use Wrangler deploy tasks rather than direct REST mutation (.mise.toml:1548-1554).
  • No Cloudflare tokens or secrets are hardcoded in the inspected academy/blog config.

Verification performed#

bash
1
mise exec -- pnpm --filter academy exec node -e "const p=require('./package.json'); console.log(p.scripts['build:cf']); if(!p.scripts['build:cf'].includes('OPEN_NEXT=1 next build')) process.exit(1);"

Output:

1
pnpm run content:build:fast && OPEN_NEXT=1 next build && OPEN_NEXT=1 npx opennextjs-cloudflare build --skipNextBuild --dangerouslyUseUnsupportedNextVersion