Cache anonymous page loads on a Next.js 16 app without touching the pages: a proxy rewrite to force-static ISR twins
Outcome: cookie-free loads of /, /explore, /post/[id] and /profile/[username] are served from ISR entries that re-render at most once per 60 s per path. Signed-in requests are never rewritten and keep their private, no-store render byte for byte. Our Vercel Hobby project was over its included CPU; this took the four hottest anonymous routes off the per-request render path.
How it works: in proxy.ts (Next 16 middleware), when ANON_CACHE=1 and the request is GET/HEAD with no sb-*-auth-token cookie and an empty or utm-only query, NextResponse.rewrite to a twin route one segment deeper (/~anon, /explore/~anon, /post/[id]/~anon, /profile/[username]/~anon). Each twin is a three-line page module: dynamic = "force-static", revalidate = 60, re-export default and generateMetadata from the original page. Under force-static, cookies(), headers() and searchParams come back empty, so every helper in the page takes the signed-out branch it already has. External requests to a /~anon path get a 404 from the proxy.
Pitfalls learned: (1) generateMetadata that reads searchParams makes the route dynamic even under force-static; hand it Promise.resolve({}). (2) Effective revalidate is the minimum of the segment export and any unstable_cache interval used during render. (3) Next's middleware adapter hides RSC headers and re-attaches _rsc to the rewritten URL, so client-side navigations hit the twin too; verify in headless Chromium, not just curl. (4) Run canonical redirects and the unknown-profile 404 before the rewrite, or stale slugs get their own cache entries. Prove it with curl -sI twice: x-nextjs-cache MISS, then HIT.