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_compatand 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/blogis mostly aligned with the current guidance: it builds for OpenNext, usesnodejs_compat, disablesworkers_dev, bindsNEXT_INC_CACHE_KV, and is exposed through thewwwPages service binding for/blog/*.apps/academystill has one blocking deployment issue: OpenNext is configured to use the KV incremental cache, but Wrangler does not bindNEXT_INC_CACHE_KV.- I applied two tiny low-risk fixes:
- Fixed
apps/academy/package.json:12soOPEN_NEXT=1is present on the actualnext buildand OpenNext build commands. - Fixed the blog OpenNext comment at
apps/blog/open-next.config.ts:9to point to the real.docs/developer.mdfile.
- Fixed
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_NEXTis set (apps/academy/next.config.cjs:29-34). - Root Cloudflare docs say Workers apps set
OPEN_NEXT=1for the OpenNext build path (.docs/cloudflare-deployment.md:58-60). - The root
miseCloudflare build delegates to the app script (.mise.toml:1462-1464). - Current fixed script now runs
content:build:fast, thenOPEN_NEXT=1 next build, thenOPEN_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 nokv_namespacessection (apps/academy/wrangler.jsonc:3-10). - Repository deployment docs show Workers apps are expected to include
kv_namespaceswithNEXT_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 updateapps/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:
- Provision a production KV namespace with Wrangler using a least-privilege account token; do not print the token.
- Add the returned namespace id to
apps/academy/wrangler.jsoncas:jsonc1"kv_namespaces": [2{ "binding": "NEXT_INC_CACHE_KV", "id": "<production-kv-namespace-id>" }3] - If preview/staging deploys are required, add separate preview/staging namespaces instead of reusing production state.
- 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
_headersidentifieshttps://academy.assistance.prod.assistance.bg/*as the production domain, notacademy.assistance.bg(apps/academy/public/_headers:1-5). - The historical migration docs describe
academy.assistance.prod.assistance.bgas 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_devand has no routes (apps/blog/wrangler.jsonc:3-18). - The
wwwPages project bindsBLOG_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
_headerstreatshttps://www.assistance.bg/blog/*andhttps://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.mdatapps/blog/open-next.config.ts:9.
Impact: small operator-discoverability issue only.
Positive checks#
- Blog has
nodejs_compat, compatibility date2025-04-01,.open-next/assets,workers_dev: false, andNEXT_INC_CACHE_KV(apps/blog/wrangler.jsonc:3-18). - Blog uses the documented
cloudflare.useWorkerdCondition=falseworkaround 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#
1mise 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:
1pnpm run content:build:fast && OPEN_NEXT=1 next build && OPEN_NEXT=1 npx opennextjs-cloudflare build --skipNextBuild --dangerouslyUseUnsupportedNextVersion