Skip to content

feat: adopt Cache Components + Turbopack (Next 16.2.10 stable)#422

Open
ignmandagaran wants to merge 25 commits into
mainfrom
ignmandagaran/adopt-cache-components
Open

feat: adopt Cache Components + Turbopack (Next 16.2.10 stable)#422
ignmandagaran wants to merge 25 commits into
mainfrom
ignmandagaran/adopt-cache-components

Conversation

@ignmandagaran

@ignmandagaran ignmandagaran commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Adopts Next.js Cache Components (cacheComponents: true) and lands the app on stable Next 16.2.10 running on Turbopack. Content routes now Partial Prerender (static shell + streamed request-time data); index/landing pages prerender fully static. Sanity data is cached via next-sanity v13's Live integration, so cached content still revalidates on-demand through Sanity Live.

Cache Components, cacheLife, and PPR are no longer canary-gated — they ship in stable 16.2.x — so the app runs on a real release, not a nightly canary.

Built with the next-cache-components-adoption skill.

Changes

  • Next 15.6-canary → 16.2.10 (stable). next + eslint-config-next pinned to 16.2.10. No canary channel needed.
  • Turbopack for dev and build (next.config.ts turbopack.rules; the --webpack flag is gone). The earlier "styles break under Turbopack" was a stale postcss@8.0.0 lockfile pin, not a Tailwind-v3 incompatibility — bumped to postcss ^8.5.10 with postcss-import as an explicit dep, so the postcss.config.mjs chain (postcss-importtailwindcss/nestingtailwindcss) runs cleanly. GLSL shaders now load through raw-loader alone: the glslify #pragma requires were flattened into the .glsl files, so no glslify-loader is required.
  • next-sanity 12 → 13 (+ React 19.2.7, @sanity/client 7.23, sanity 5.31) — native Cache Components support; cacheLife profile wired into next.config so Sanity Live drives revalidation.
  • motion 12.0.0-alpha.2 → 12.42 stable — the alpha called Math.random() at render (SSR hazard under Cache Components); stable is prerender-safe.
  • Sanity fetch layer split into 3 modes: sanityFetch (Live, draft/preview) · sanityFetchCached (Live + "use cache", published render) · sanityFetchStatic (non-Live — generateStaticParams/generateMetadata, and any non-use cache context such as server actions).
  • Routes adopted: post/showcase/careers/blog [slug]<Suspense> for params + "use cache" content; home/showcase/people/services index → cached list fetchers; .md endpoints + sitemap cached.
  • /lab mobile redirect moved to proxy.ts so the page no longer reads headers() and prerenders static — this removed the only 16.3-canary-only dependency (export const instant), unblocking stable.
  • Fixed the arcade "Lab" experiments list — its server action called the Live sanityFetch, whose cacheTag() throws outside a "use cache" scope, so it 500'd and rendered no experiments. Switched to sanityFetchStatic.
  • sitemap.xml revalidates on publish — was sanityFetchStatic (no Sanity tags → redeploy-only); switched to sanityFetchCached so Sanity Live invalidates it, matching the pages and .md routes.
  • Fixed 404-scene wall inspectable frames mispositioning — the motion 12.42 upgrade changed the frames' transform MotionValues to new MotionValue(0); animate() tweens from the current value, so they grew in from origin/zero-scale and could be left mispositioned depending on load timing (intermittent). Init resting scale to 1 and snap the first placement; only real selections animate.
  • Removed the checked-in next-cache-components-adoption skill and trimmed the auto-generated AGENTS.md block, keeping a project-specifics section.

Verification

  • next build green on Next 16.2.10 + Turbopack — content routes ◐ Partial Prerender, index pages ○ Static, /lab ○ Static.
  • Route-table parity checked across the canary → stable move: the only classification that changed is /lab (ƒ → ○); every other page kept its class.
  • Dev + prod render fully styled (Tailwind ✓, fonts ✓, WebGL shaders ✓). Arcade "Lab" experiments + cover images load; /sitemap.xml prerenders with sanity:* cache-tags; the 404 scene's wall frames render correctly across repeated reloads.

Follow-ups

  • Optionally bump to 16.3.x stable when it ships (16.2.10 is the current stable and fully supports this migration).

Checklist

  • Tested locally in dev + production build (Next 16.2.10, Turbopack)
  • Breaking changes documented above (Next 16, Turbopack, dep upgrades)

- Bump next + eslint-config-next to 16.3.0-canary.60 line (16.3 needed for Cache Components: instant export + codemod + dev-overlay validation)
- Switch dev/build scripts to Turbopack (Next 16 default); drop --webpack
- Remove webpack() glsl loader config (mirrored by existing turbopack.rules)
- Remove experimental.ppr (flag removed in Next 16; superseded by cacheComponents)
- Remove experimental_ppr segment config from blog route
- next-sanity 12.1.6 -> 13.1.1 (native Cache Components support: cacheLife profile, 'use cache'-aware fetches, tag-based Live invalidation)
- react/react-dom 19.0.1 -> 19.2.7 (satisfies next-sanity peer ^19.2.3; React 19.2 is what Next 16 targets)
- @sanity/client ^7.23.0, sanity/@sanity/vision ^5.29.0 (v13 peer floors)
- @types/react(-dom) ^19.2

Build verified green; no behavior change (cacheComponents not yet enabled).
Enable Next.js Cache Components (cacheComponents: true) with next-sanity v13's
Live integration, and walk every route to a passing build. Content routes now
Partial-Prerender (static shell + streamed data); index/landing pages prerender
static.

Config
- next.config: cacheComponents: true + Sanity cacheLife profile (Sanity Live
  drives on-demand revalidation, so the default 15min TTL is replaced).

Sanity data layer (src/service/sanity)
- sanityFetch (Live), sanityFetchCached (Live wrapped in "use cache" — cached
  published reads, still Live-invalidated via cacheTag), sanityFetchStatic
  (non-Live client.fetch for generateStaticParams/generateMetadata).
- Translate removed route-segment config: drop force-static (post/showcase/
  careers [slug]) and dynamic/fetchCache/revalidate (scores API).

Shared shell
- ContentWrapper: isolate usePathname() behind <Suspense>; dynamic-param routes
  (all canvas-blacklisted) fall back to page content in the plain container.
- Navbar/Footer: stream interactive (motion) content via <Suspense>; cache the
  shared layout fetches (counts, company info).
- Root layout: draftMode + SanityLive behind Suspense.

Routes
- post/showcase/careers/blog [slug]: split static vs Live fetch, defer params
  behind <Suspense>, cache content with "use cache".
- home/showcase/people/services index: cache published list fetchers.
- .md endpoints + sitemap: cache their data so the handlers prerender.
- brands: deterministic mobile trim (was Math.random) + stream motion title.
- lab: instant = false (request-time user-agent redirect, documented Block).

AGENTS.md/CLAUDE.md regenerated by next dev for Next 16.
… workarounds

motion@12.0.0-alpha.2 evaluated Math.random() during render (an SSR/prerender
hazard under Cache Components); the stable line is prerender-safe. Upgrading lets
the navbar, footer, and brands render back in the static shell instead of being
streamed behind <Suspense> to dodge the hazard.

- motion 12.0.0-alpha.2 -> ^12.42.0
- MotionValue constructor now requires an initial value: new MotionValue() ->
  new MotionValue(0) (postprocessing, inspectables, use-fade-animation)
- locked-door: guard lockedDoor before animate() (stricter v12 overloads reject
  the possibly-undefined target)
- Remove the navbar/footer/brands <Suspense> motion workarounds
- Copyright year now resolves on the client (reading the current year at render
  is unstable IO; was previously masked by the footer Suspense wrap)

Build green (89/89); content routes still Partial Prerender.
Turbopack does not run this project's Tailwind v3 PostCSS pipeline (verified with
minimal and full postcss configs, ESM and CJS): `@tailwind`/`@apply` pass through
unprocessed, so the site renders with ~85 CSS rules, no custom fonts, no brand
styling. Turbopack also can't load the GLSL shaders (no loader equivalent).

Cache Components is bundler-agnostic, so reverting the bundler keeps the entire
adoption intact — content routes still Partial Prerender, index pages static.

- dev/build scripts back to --webpack
- restore the webpack() GLSL raw-loader/glslify-loader config
- restore postcss.config.mjs (postcss-import + tailwindcss/nesting + tailwindcss)

Verified: dev renders fully styled (819 CSS rules, Geist font, black bg, shaders
load); `next build --webpack` green with cacheComponents (◐ routes preserved).

Revisiting Turbopack later would require migrating Tailwind v3 -> v4 (native
Turbopack support via @tailwindcss/postcss).
Trim the explanatory comments added during adoption to concise one-liners
(why, not what), matching surrounding comment density.
@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
website-2k25 Ready Ready Preview, Comment Jul 20, 2026 5:57pm

The blog index only awaits params and renders cached fetchers ("use cache"),
so the params read doesn't gate any uncached work — the route still prerenders
(`/blog` and category pages stay static with the full post list in the HTML;
the optional catch-all stays Partial Prerender). Verified via prod build.
careers/[slug] has no unstable client components, so the wrap was unnecessary —
its pages now prerender static. post/[slug] and showcase/[slug] keep theirs (a
verified build fails without them: client embeds/gallery read Date.now()/runtime
data at prerender); added a comment noting why each is load-bearing.
The Mux player reads Date.now() at render, which can't run during prerender
under Cache Components. Load it via dynamic(ssr: false) wrapped in <Suspense>
with a skeleton that renders the Mux poster (the video's first frame, so no
layout shift). This makes <Video> self-contained, so showcase/[slug] no longer
needs a page-level Suspense (its only prerender blocker was the gallery video).

- video-mux.tsx: the Mux renderer, lazy-loaded client-only
- video.tsx: dynamic(ssr:false) + Suspense + poster VideoSkeleton
- showcase/[slug]: drop the now-redundant page-level Suspense

Verified: build green (89/89), Mux player absent from prerendered HTML (no
hazard), poster in the static shell, videos load + play client-side.
post/[slug] had three client embeds blocking prerender. With all handled, the
route drops its page-level Suspense and prerenders.

- tweet: wrap getTweet() in "use cache" + cacheLife('max') (tweets are
  immutable) — fetched once and cached instead of hitting the syndication API
  per request
- sandbox: load Sandpack via dynamic(ssr: false) + Suspense with a skeleton
  (it reads Date.now() at render); split into sandbox-impl.tsx
- post/[slug]: drop the now-redundant page-level Suspense
…e) (#423)

* prototype(canvas-route-groups): scaffolding for structural canvas

Replaces the usePathname + BLACKLISTED_PATHS check with a route-group approach:
- CanvasLayer: the single persistent canvas, mounted once in the root layout,
  visibility driven by the isCanvasInPage store flag (no usePathname).
- SetCanvasMode: client effect setter a group layout renders to declare
  canvas on/off — runs client-side, so no prerender hazard, no <Suspense>.
- PageShell: the (pages) main+Footer chrome, extracted so both the canvas and
  plain groups can share it.

Wiring (route moves + root/layout swap) is the next step; see notes.

* prototype(canvas-route-groups): wire structural canvas via route groups

Replace ContentWrapper's usePathname + BLACKLISTED_PATHS with route groups:
- (canvas)/ and (plain)/ groups; (content)/ sub-layout shares PageShell chrome
- root layout mounts the persistent CanvasLayer (no usePathname, no Suspense)
- SetCanvasMode in each group layout toggles canvasVisible; isCanvasInPage stays
  sticky so the WebGL Scene persists across navigations
- showcase index (canvas) / [slug] (plain) split across groups
- not-found sets canvas mode; 21 cross-refs + contact path updated

Build green (89/89). Runtime canvas behavior pending browser QA.
The laboratory server action called the Live sanityFetch, whose cacheTag()
throws outside a "use cache" scope under Cache Components, so the action
500'd and the arcade rendered no experiments. Use sanityFetchStatic (non-Live,
published) which is safe in a server action.
sitemap.ts used sanityFetchStatic (non-Live), so the prerendered XML sitemap
had no Sanity cacheTags and only refreshed on redeploy or the 1y backstop.
Switch to sanityFetchCached so Sanity Live invalidates it on publish, matching
the .md routes.
The /lab page read headers() (user-agent) at the top to redirect mobile
visitors, which forces it dynamic under Cache Components and required the
canary-only `export const instant = false`. Move the redirect to proxy.ts
(scoped to /lab) so the page no longer reads request data and prerenders as
static. Behaviour is unchanged; the redirect now runs before render.
Cache Components, cacheLife, and PPR are no longer canary-gated — they ship in
stable 16.2.10. With the /lab instant=false opt-out removed (redirect moved to
proxy), the app builds clean on stable. Route classifications are unchanged
except /lab, which is now prerendered static. Pins next + eslint-config-next
to 16.2.10.
Document the stable-16.2.10/webpack setup, the three Sanity fetch modes and
when to use each, the proxy-not-pages rule for request-time logic, and
tag-based revalidation. Placed outside the next-dev-managed block so it
survives regeneration.
Remove the generated nextjs-agent-rules block from the committed file. Note:
next dev regenerates it on dev runs, so it may reappear as an uncommitted
change; the project-specific section is kept separate so it survives.
…GLSL

Turbopack officially supports raw-loader but not glslify-loader. The 5
shaders using #pragma glslify: require/export are flattened with the
glslify CLI (which resolves + dedups the ashima noise helpers), so plain
raw-loader can load every .glsl file. The now-inlined utils/value-remap
and utils/basic-light are deleted.
Drop the --webpack flag from dev/build (Turbopack is the Next 16
default), simplify turbopack.rules to raw-loader only, and remove the
now-dead webpack() loader function. Remove glslify-loader/glsl-noise/
glsl-constants; add postcss-import as an explicit dep so Turbopack's
resolver doesn't rely on a transitive hoist. Tailwind v3 PostCSS chain
verified working under Turbopack. AGENTS.md updated to match.
…urbopack

The lockfile had postcss pinned at 8.0.0 (the first 8.x release, 2020).
Turbopack's PostCSS transform silently no-ops plugins driven by that
version — Tailwind's @tailwind/@apply/theme() directives passed through
raw in both dev and production, with no error. Bumping to postcss@8.5.10
fixes it: verified in a worktree and the real tree, dev and build, that
every CSS chunk now has zero raw directives and full expanded output
(preflight, utilities, theme colors), confirmed visually in the browser.
No Tailwind v3->v4 migration was needed — this was never a Turbopack/
Tailwind-v3 incompatibility.
…cement

The motion 12.42 upgrade changed the inspectable transform MotionValues from
new MotionValue() to new MotionValue(0). motion's animate() tweens from the
value's current state, so the old undefined snapped instantly while 0 tweens
the frame in from origin/zero-scale. On the 404 security-cam view the frames
could be left mispositioned depending on load timing (intermittent, fixed on
refresh). Init resting scale to 1 and snap the first placement (duration 0);
only real selections animate.
@ignmandagaran
ignmandagaran marked this pull request as ready for review July 20, 2026 18:15
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown

Too many files changed for review. (146 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@ignmandagaran ignmandagaran changed the title feat: adopt Cache Components feat: adopt Cache Components + Turbopack (Next 16.2.10 stable) Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant