Rendering.
Every route in anchor, the mode it picked, and why. The page reads top-to-bottom as a journey from most-static to most-dynamic; the same mental model to use when deciding how to render anything new.
Legend.
- ○Static
- ●SSG (parameter-expanded)
- ◐Partial Prerender
- ƒDynamic SSR
- ƒEdge
Routes.
- ○
/StaticPure SSG baseline. The catalog list comes from a TypeScript constant; no request data, no Suspense. Same HTML for every visitor, served from the edge CDN.
- ○
/agentsStaticReads from loadAgentsDescriptor() which is wrapped in 'use cache' + cacheLife('hours') + cacheTag('descriptor'). The page is purely a function of catalog data, so Next prerenders it once.
- ○
/.well-known/agents.jsonStaticMachine-readable capability descriptor. Same cached loader as /agents — one cache entry serves both surfaces. Rewrite maps the conventional URL to /agents-descriptor internally.
- ○
/llms.txtStaticMarkdown reading list, same cached loader. Rewrite handles the .txt suffix that Next's app router would otherwise treat as a private file.
- ○
/compareStaticThe form shell prerenders at build time. The AI call only fires when the user submits and the Server Action invokes the comparison agent — no model call happens during the static build.
- ○
/playgroundStaticStatic shell describing the eight-check pipeline plus a client component that fires scenarios via /api/playground/scenario. The runner endpoint mints the demo token server-side so the admin key never reaches the browser.
- ○
/askStaticFullscreen conversational surface backed by the AskAnchor client component. Static shell; the chat itself streams from /api/ask. Every product fetch the agent makes carries User-Agent: anchor-ask/1.0 so the proxy logs it under bot class 'anchor-ask' — visitors talking to the agent contribute to the AEO dashboard they can see.
- ƒ
/api/askDynamic SSRStreaming chat endpoint. Calls Anthropic via the AI SDK with five tools (list_products, get_product, compare_products, lookup_agents_json, propose_navigation). Returns a UIMessageStreamResponse the useChat hook on the client consumes.
- ƒ
/api/playground/scenarioDynamic SSRServer-side scenario runner. Mints a delegated-authority token with ANCHOR_ADMIN_KEY, calls /api/agent/checkout once per scenario step, returns the full request/response chain to the playground UI. Admin key stays server-side; the browser only ever sees the scenarioId it sent and the response chain it received back.
- ○
/docs/renderingStaticPure documentation page. ROUTES is a TypeScript constant, no request data, no Suspense. This page documents itself.
- ●
/products/[slug]/agentSSG (parameter-expanded)10 static redirects (one per slug, 308 to /agent/markdown) generated at build time via generateStaticParams. Keeps the canonical /agent URL working even though the body lives in three format-split sub-routes.
- ●
/products/[slug]/agent/markdownSSG (parameter-expanded)10 prerendered files — the citation-shaped LLM surface for each product. Body is a pure function of CATALOG, no request reads. Edge cache serves every fetch in ~5ms worldwide.
- ●
/products/[slug]/agent/jsonSSG (parameter-expanded)Same pattern as /markdown: 10 prerendered files containing only Schema.org Product/Offer JSON-LD. For crawlers that prefer pure structured data.
- ●
/products/[slug]/agent/plainSSG (parameter-expanded)Same pattern as /markdown: 10 prerendered files with citation opening + prose body, no JSON-LD fence. For crawlers that don't parse structured data.
- ◐
/products/[slug]Partial PrerenderMostly static product copy + specs + JSON-LD. Single dynamic hole: <Suspense> around <AgentTally>, which reads headers() inside to opt out of prerendering and streams in the live Redis fetch count at request time. Best of both worlds: instant shell + live data.
- ◐
/dashboardPartial PrerenderFunctionally force-dynamic. Every Suspense child (TopBar, BotMix, PerProduct, LiveTail) reads headers() at the top, which under cacheComponents marks them dynamic. Equivalent to old 'export const dynamic = force-dynamic' without the route segment export, which is disallowed in this mode.
- ƒ
/api/agent/issue-tokenDynamic SSRAdmin-only token minter. Each call signs a new token with a fresh jti — no caching possible. Default-deny: 404 (not 403) on a missing or wrong admin key so the endpoint can't be enumerated.
- ƒ
/api/agent/checkoutDynamic SSRThe agent-purchase endpoint. Output depends on the token contents, the Redis nonce + idempotency lookups, the cached product, and the current time. No amount of caching helps — every request must run the 8-check pipeline fresh.
- ƒ
proxy.tsEdgeCited-by attribution + AEO logging cross-cutting concerns. Runs at the edge on every /products/:slug and /products/:slug/agent* request, classifies User-Agent / Referer, attaches X-Anchor-* headers, queues Redis writes via after(). Telemetry survives even when the body is served from the static cache.
Primitives.
The Next 16 + AI SDK v6 features the build leans on, with file paths so you can grep for each one in the repo.
'use cache' directive
lib/product-loader.ts, lib/agents-descriptor.ts
Marks a function's output cacheable. Inputs become the cache key; the result is memoized across requests. Replaces the older unstable_cache wrapper.
cacheLife(profile)
lib/product-loader.ts ('hours' & 'days'), lib/agents-descriptor.ts ('hours')
Sets revalidation cadence on a cached function. anchor uses 'hours' for product reads and 'days' for the rarely-changing slug index.
cacheTag(name)
lib/product-loader.ts, lib/agents-descriptor.ts
Labels a cache entry so revalidateTag can invalidate it surgically. anchor tags 'product:<slug>', 'catalog:index', and 'descriptor' — selling a Moonshot Grinder invalidates exactly that one slug.
revalidateTag(tag, profile)
app/api/agent/checkout/route.ts
Fires after a successful sale. Invalidates the product cache entry so the next read recomputes with the decremented inventory. The other 9 products stay cached.
generateStaticParams()
app/products/[slug]/page.tsx + all four /agent/* routes
Tells Next which dynamic-segment values to prerender. anchor expands 10 slugs × 4 agent routes = 40 prerendered HTML files at build.
after(fn)
proxy.ts (telemetry write)
Vercel-specific lifecycle primitive. Runs fn after the response is sent, but before the function suspends. Lets us preserve sub-50ms TTFB while still writing AEO logs to Redis on every fetch. Without after(), an unawaited Promise in serverless would be killed mid-execution.
Server Action
app/compare/actions.ts
Type-safe client-to-server call. The comparison page's <CompareForm> imports compareProducts() directly and gets the exact return type. No route shape to maintain, no JSON serialization to design.
streamObject + Zod discriminated union
lib/compare-agent.ts
Generative UI in AI SDK v6. The model picks one of three shapes (specTable / prosCons / recommendation) and fills it. React switch-renders the matching component on result.kind. Type-safe at every boundary.
Proxy (formerly Middleware)
proxy.ts
Runs at the edge before any route handler. Two jobs: (1) classify Referer for cited-by attribution on human page visits, (2) classify User-Agent + log AEO fetches on /agent/* even when the body is served from the static cache. Renamed from middleware.ts in Next 16.
cacheComponents flag
next.config.ts
Enables BOTH 'use cache' AND Partial Prerendering. Default-static rendering: opting OUT of caching is the active gesture (via headers() / cookies() / non-cached reads). Disallows per-route runtime / dynamic / revalidate exports — those move into the component body via the data sources you read.
How to pick.
When you sit down to render anything new, the question to ask first is: what's the most static this can be without losing the dynamic behavior I actually need?
- Does the body depend on the visitor (auth, personalization, live data)? If no → static or SSG. Wrap data loaders in
'use cache'and you're done. - One small piece needs to be live? → PPR. Wrap that piece in
<Suspense>; the rest stays static. - The whole page depends on the request? → Dynamic. Read
headers()orcookies()at the top. - Cross-cutting concern that should run before any route? → Proxy. Geographic edge, no React render needed.
- Interactive UI (state, events, browser APIs)? →
'use client'on just the interactive component, not the parent.