feat(web): adopt next.js 16 cache components#54
Merged
Conversation
Enable Partial Prerendering across every route. Cached shell HTML is
produced at build time; the dynamic regions — auth state, per-user
progress, admin data — stream in from a Suspense boundary at request
time. Tag-based invalidation replaces the hourly ISR window so admin
content edits propagate to public pages in seconds instead of up to
an hour.
The migration is wide because `cacheComponents: true` enforces that
every uncached runtime data access (cookies, headers, awaited params/
searchParams, Supabase auth/queries that read cookies) sits inside a
Suspense boundary during static prerender. One earlier attempt was
reverted for exactly this reason; the fix is the mechanical extraction
done in this PR.
Structural changes:
- next.config.ts: `cacheComponents: true`.
- Layouts: root + admin + (auth) now have a static chrome shell plus
a Suspense-wrapped async child that reads `headers()` / does the
auth check. Redirect semantics unchanged — proxy.ts is still the
primary gate; layout Suspense is the belt-and-suspenders path.
- Pages: every page that awaits params/searchParams or reads cookies
now has a sync default export wrapping an async `PageBody` child
in `<Suspense>`. Applies to all 4 public pages, 7 admin pages, 6
auth pages, the landing page, and the login page.
- Public pages: static data fetches (`getProblemBySlug`,
`getListBySlug`, `getListProblems`, `getFilteredProblems`,
`getFilteredLists`, sitemap data) converted from `react.cache` to
`'use cache'` + `cacheLife('hours')` + `cacheTag(...)`. Tags use
`problems`, `problem:<slug>`, `lists`, `list:<slug>`,
`list:<id>:problems` so admin mutations can target precisely.
- Admin mutations: `_admin-helpers.ts` gains an optional `tags` array.
`deleteProblem` revalidates `['problems', 'lists']`;
`flag`/`unflagRegeneration` revalidate `['problems']`. Pair with
existing `revalidatePath` so admin pages still re-render in-place.
- API routes: `dynamic = 'force-dynamic'` removed from
`api/cron/push` and `api/health` — incompatible with
cacheComponents; route handlers default to dynamic anyway.
- Request-scoped time: admin pages that use `new Date()`/`Date.now()`
to construct queries now `await connection()` first (Next.js
requires this signal so the value isn't frozen into the prerender).
- Page-level `revalidate = 3600` + `dynamicParams = true` removed
from the four public pages; those behaviours are now governed by
the cached helpers' `cacheLife` + `cacheTag`.
Testing:
- Added `revalidateTag` to the `next/cache` mock in admin-action
tests so `tags: [...]` paths don't fail.
- Exposed `LoginPageBody` as a named export so the existing direct-
invoke tests can keep exercising the async body.
- `pnpm build`: all 32 routes compile; 18 are Partial Prerender,
the rest static or API-dynamic.
- `pnpm test`: 186 shared + 571 web all green.
- `tsc --noEmit`, `eslint`: clean.
Docs:
- `.claude/rules/web-patterns.md`: new Cache Components section
documenting the page pattern, `use cache` helpers, cacheTag
vocabulary, and the `await connection()` requirement.
- `.claude/rules/push-pipeline.md`: cron route no longer mandates
`force-dynamic`.
- `docs/deferred-improvements.md`: Cache Components section removed
(done); Branded channel-identifier types rewritten as an explicit
"decided to skip" with revisit triggers.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📊 Coverage Report
🤖 Generated by CI · thresholds enforced via vitest |
bolin8017
added a commit
that referenced
this pull request
Apr 18, 2026
Audit pass over every .md after merging 17 PRs (#38-#54) in the review cycle. Nothing behavioural changed; this PR just retires claims that no longer match the repo. Corrections: - README.md: bumped prerequisite from Node 22+/pnpm 9 to Node 22+/pnpm 10 (CI and Vercel pinned to Node 24 via .nvmrc). Updated test counts 185 -> 186 (shared) and 566 -> 571 (web) for a new TS total of 757; matches the counts verified at the end of the Cache Components PR. - CLAUDE.md: same test count update; replaced "Web + Worker" in the architecture + deployment tables with "Web (includes cron API route)" since apps/worker/ was deleted in PR #33 and the push pipeline now runs as /api/cron/push inside the web app. Dropped `worker` from the scope enum. Added a short Cache Components note pointing at the rule file instead of duplicating the pattern here. - .claude/rules/git-conventions.md: same scope enum fix, with a note explaining why `worker` was retired. Replaced the `fix(worker): ...` example with `fix(web): ...`. - .claude/rules/deployment.md: renamed "Worker Cron" section to "Cron Entry" (matches push-pipeline.md terminology), rewrote the deploy step and the Web+Worker cloud-services entry, and added Upstash Redis as an optional service now that PR #49 uses it for the cross-instance webhook rate limiter. - .claude/rules/notifications.md: removed the stale `apps/worker/src/channels/**` glob; clarified that the email renderer lives in packages/shared. - docs/staging-setup.md: every "Web + Worker" phrasing updated to reflect the single Vercel deployment that now hosts the cron route. No production references remain to `apps/worker/`, `pnpm@9.15.0`, or outdated test counts.
bolin8017
added a commit
that referenced
this pull request
Apr 18, 2026
## Summary Audit pass over every `.md` after merging 17 PRs (#38–#54) in the review cycle. Nothing behavioural changed; just retire claims that no longer match the repo. ## Corrections - **`README.md`**: Node 22 → Node 22+ with CI/Vercel on Node 24 (via `.nvmrc`); pnpm 9 → pnpm 10.33.0. Test counts: shared 185 → 186, web 566 → 571, TS total 751 → 757. - **`CLAUDE.md`**: same test count update; "Web + Worker" → "Web (includes cron API route)" everywhere (apps/worker/ deleted in PR #33 — push pipeline now runs as `/api/cron/push` inside the web app). Dropped `worker` from the commit scope enum. Added a short Cache Components note pointing at `.claude/rules/web-patterns.md` instead of duplicating the pattern. - **`.claude/rules/git-conventions.md`**: same scope enum fix with explanation. Replaced `fix(worker): ...` example with `fix(web): ...`. - **`.claude/rules/deployment.md`**: "Worker Cron" section → "Cron Entry" (matches push-pipeline.md terminology). Added Upstash Redis as an optional cloud service now that PR #49 uses it for the cross-instance webhook rate limiter. - **`.claude/rules/notifications.md`**: removed the stale `apps/worker/src/channels/**` path glob; clarified that the email renderer lives in `packages/shared`. - **`docs/staging-setup.md`**: every "Web + Worker" phrasing updated to reflect the single Vercel deployment. ## Test plan - `grep` sweep confirms no remaining references to `apps/worker/`, `pnpm@9.15.0`, `751 TS`, `185 tests`, `566 tests`, `Node.js 22+` (without the updated Node 24 context), or "Web + Worker" as a claim about current state. - Only remaining mentions of `apps/worker/` are in explanatory sentences describing the removal (CLAUDE.md, deployment.md, git-conventions.md).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
cacheComponents: trueand migrates the whole app to Next.js 16 Partial Prerendering: static shells + cached data + dynamic Suspense regions in every route.deleteProblem,flagForRegeneration,unflagRegeneration) callrevalidateTag(..., 'max')for tag-scoped cache invalidation, replacing the hourly ISR window — admin edits propagate in seconds.docs/deferred-improvements.md) failed becausecacheComponents: truerequires every runtime data access (cookies, headers, awaited params/searchParams, Supabase auth) to sit inside a<Suspense>boundary; that's what this PR methodically fixes across ~20 files.What changed structurally
cacheComponents: trueinapps/web/next.config.ts.app/layout.tsx), admin, and(auth)now have a synchronous static chrome shell. Auth/header reads live in a Suspense-wrapped async child. Redirect semantics unchanged — proxy.ts still does the primary gate.PageBodychild in<Suspense>. Covers all 4 public pages, 7 admin pages, 6 auth pages, landing, login.react.cacheto'use cache'+cacheLife('hours')+cacheTag(...). Cached helpers:getProblemBySlug,getListBySlug,getListProblems,getFilteredProblems,getFilteredLists,getSitemapData.problems,problem:<slug>,lists,list:<slug>,list:<id>:problems._admin-helpers.tsgains atags: string[]option; affected actions pass the right tags.dynamic = 'force-dynamic'removed fromapi/cron/pushandapi/health(incompatible with cacheComponents; handlers default to dynamic anyway).new Date()/Date.now()for query construction nowawait connection()first.Test plan
pnpm build: all 32 routes compile. 18 are Partial Prerender (◐), the rest static (○) or API-dynamic (ƒ).pnpm --filter @caffecode/shared test: 186 passed.pnpm --filter @caffecode/web test: 571 passed. AddedrevalidateTagto thenext/cachemock; exposedLoginPageBodyas a named export for direct test invocation.tsc --noEmitfor shared + web: clean.pnpm --filter @caffecode/web lint: clean.Follow-up
/admin/contentedits → confirm public/problems/[slug]reflects changes within seconds (no longer bounded by the 1h ISR).