Skip to content

feat: adopt Cache Components#422

Draft
ignmandagaran wants to merge 12 commits into
mainfrom
ignmandagaran/adopt-cache-components
Draft

feat: adopt Cache Components#422
ignmandagaran wants to merge 12 commits into
mainfrom
ignmandagaran/adopt-cache-components

Conversation

@ignmandagaran

Copy link
Copy Markdown
Member

Summary

Adopts Next.js Cache Components (cacheComponents: true) across the site. 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.

Getting there required a few foundational upgrades (Cache Components needs Next 16.3 + the v13 Sanity integration). A Turbopack switch was attempted and reverted — it doesn't process this project's Tailwind v3 setup (details below) — so we stay on webpack, which Cache Components supports fine.

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

Changes

  • Next 15.6-canary → 16.3.0-canary.68 (16.3 needed for export const instant + dev-overlay validation; canary-only until 16.3 ships stable).
  • 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).
  • 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 is a documented instant = false Block (request-time UA redirect).
  • Reverted the Turbopack experiment back to webpack: Turbopack does not run the Tailwind v3 PostCSS pipeline here (@tailwind/@apply pass through → unstyled site) and can't load the GLSL shaders. Cache Components is bundler-agnostic, so the adoption is unaffected.

Verification

  • next build --webpack green — 89/89 pages, content routes ◐ Partial Prerender, index pages ○ Static.
  • Dev server renders fully styled (Tailwind ✓, fonts ✓, WebGL shaders ✓); browser-verified post/showcase/home with zero dev-overlay errors via the Next DevTools MCP.

Follow-ups

  • Bump next + eslint-config-next to 16.3.x stable when released.
  • Turbopack would require a Tailwind v3 → v4 migration (@tailwindcss/postcss) — out of scope here.

Checklist

  • pnpm lint passes for changed files (repo has pre-existing lint debt in untouched files)
  • Tested locally in dev mode (webpack) + production build
  • Breaking changes documented above (Next 16, 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 1, 2026 6:18pm

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.
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