You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Blocked onnextjs-revalidatev1.7.0 (milestone) — specifically superhuit-agency/nextjs-revalidate#30, which provides the WordPress-side trigger. The Next.js work below is implementable and testable before that release; only the end-to-end acceptance criteria require it.
For the implementing session: this issue is the spec. Follow the next-wordpress-data-flow skill — this changes a data contract crossing the WordPress→Next.js boundary. When done, /code-review against this issue exercises the Spec axis.
Problem
The FSE snapshot — all templates with core/template-part blocks inlined and Polylang translation variants attached — is a build artifact. next/scripts/fetch-fse-templates-and-parts.ts runs via predev/prebuild and writes a JSON file that is imported statically:
Because the import is static, the JSON is inlined into the compiled server bundle. Rewriting the file at runtime does nothing for a running next start. Editing an FSE template or template part in WordPress therefore requires a full rebuild and redeploy — a CI run and a PM2 process bounce for what is a content edit.
The build also depends on WordPress being reachable: if it isn't, the script writes an empty-templates fallback and the site ships with no templates, silently.
Goal
Make the snapshot runtime cached data, invalidated on demand by a cache tag. Editing a template in WordPress updates the frontend without a rebuild.
Design decisions
These were settled in a design session; recorded here so they are not relitigated.
Snapshot becomes runtime cached data, not a build artifact seeded at build time and not a hybrid of both.
Caching primitive:experimental.useCache: true in next.config.ts + the "use cache" directive + cacheTag('fse-templates'). Not top-level cacheComponents — that changes route-segment caching semantics app-wide, interacts with export const revalidate = 3600 (next/src/app/[[...uri]]/page.tsx:19) and PPR on every route, and is a framework migration in its own right. Notunstable_cache — it works today but is the legacy path in Next 16 and would need migrating again.
Backstop TTL:cacheLife('hours') — stale 5 min / revalidate 1 h / expire 1 day. Chosen to match the page's existing revalidate = 3600 so the snapshot and the full-route cache share one cadence. Purely a safety net for a missed invalidation ping (plugin misconfigured, request failed, secret rotated).
Build hooks removed. The script survives only as a debug CLI writing to a gitignored path outsidesrc/lib/fse/, so it can never be imported again.
Endpoint: a dedicated /api/revalidate-fse route, matching the default path in the plugin's new settings.
Scope
next/next.config.ts
Add experimental: { useCache: true }. Narrowly scoped — it only enables the directive.
New: next/src/lib/fse/get-fse-snapshot.ts
Extract the body of next/scripts/fetch-fse-templates-and-parts.ts (the two GraphQL queries, formatBlocksJSON with skipGetData: true, template-part inlining, Polylang translations grafting) into an exported async function.
Wrap with 'use cache', cacheTag('fse-templates'), cacheLife('hours').
Behaviour on WordPress error must degrade the same way the script does today: return an empty-templates structure rather than throwing and taking pages down.
⚠️Landmine — cache the function, not the fetch.next/src/lib/fetch-api.ts:34 issues fetch(endpoint, { method: 'POST', … }), and Next.js never caches POST fetches. Relying on fetch-level caching would re-query WordPress on every ISR regeneration (~0.9 s measured) instead of once per invalidation. The cached unit must be the finished, template-part-inlined structure.
next/src/lib/get-node-by-uri.ts
Delete the static import at line 8 (and the @ts-ignore above it, now unnecessary).
getTemplateBlocks() (line 326) becomes async, reading from getFseSnapshot(). Its call site (lines 125–128) already sits inside a Promise.allSettled, so it becomes enrichTemplateBlocks(await getTemplateBlocks(...), lang).
The translations[lang] substitution and core/post-content injection are unchanged.
New: next/src/app/api/revalidate-fse/route.ts
Validate secret against process.env.REVALIDATE_SECRET, matching the existing next/src/app/api/revalidate/route.ts (same env var, same query-arg convention, same 401 on mismatch).
Remove predev and prebuild from next/package.json (lines 6, 8).
Remove the fse-templates-and-parts.json entry from next/.gitignore (lines 51–52); add the new debug-dump path instead.
Reduce next/scripts/fetch-fse-templates-and-parts.ts to a thin CLI calling getFseSnapshot() and dumping to the gitignored debug path.
Rewrite docs/fse-templating.md — its entire "Build time" phase, the architecture diagram, the mermaid schema, and the "Updating the JSON" section all become wrong.
Acceptance criteria
npm run build no longer contacts WordPress, and succeeds with WordPress unreachable
A page renders its FSE template correctly with no JSON file present anywhere
GET /api/revalidate-fse?secret=… returns 200; a wrong or missing secret returns 401
After hitting the endpoint, a template change in WordPress appears on the frontend without a rebuild
Polylang translated template parts still resolve per-language (regression check against the translations map)
core/navigation inner blocks are still fetched at request time via getData, not from the snapshot
Only one GraphQL round-trip pair occurs per invalidation, not one per ISR regeneration — verified by logging or network capture (this is the POST landmine above)
Verify and record: does revalidateTag('fse-templates') invalidate the full-route cache entries of already-prerendered pages, or only the data-cache entry? Answer belongs in Research: cheap cache invalidation vs selective warming for large sites #130 and determines whether path revalidation is additionally required
End-to-end against tipee.ch staging with nextjs-revalidate ≥ 1.7.0: edit a template part in the site editor → change appears on the frontend
network (2 GraphQL queries, parallel) = 889–1045 ms ← once per invalidation, site-wide
parse + format (formatBlocksJSON) = 12–31 ms
JSON.parse(702 KB) = 1.8–2.7 ms ← per ISR regeneration, cache hit
full build script, cold = 2.13 s wall ← removed from every build
Expected impact: visitor requests unchanged (ISR full-route cache); ISR regeneration gains ≤ 2.7 ms against a render that already makes several WordPress round-trips for getData; builds get ~2.1 s faster and lose their WordPress dependency.
Problem
The FSE snapshot — all templates with
core/template-partblocks inlined and Polylang translation variants attached — is a build artifact.next/scripts/fetch-fse-templates-and-parts.tsruns viapredev/prebuildand writes a JSON file that is imported statically:Because the import is static, the JSON is inlined into the compiled server bundle. Rewriting the file at runtime does nothing for a running
next start. Editing an FSE template or template part in WordPress therefore requires a full rebuild and redeploy — a CI run and a PM2 process bounce for what is a content edit.The build also depends on WordPress being reachable: if it isn't, the script writes an empty-templates fallback and the site ships with no templates, silently.
Goal
Make the snapshot runtime cached data, invalidated on demand by a cache tag. Editing a template in WordPress updates the frontend without a rebuild.
Design decisions
These were settled in a design session; recorded here so they are not relitigated.
experimental.useCache: trueinnext.config.ts+ the"use cache"directive +cacheTag('fse-templates'). Not top-levelcacheComponents— that changes route-segment caching semantics app-wide, interacts withexport const revalidate = 3600(next/src/app/[[...uri]]/page.tsx:19) and PPR on every route, and is a framework migration in its own right. Notunstable_cache— it works today but is the legacy path in Next 16 and would need migrating again.cacheLife('hours')— stale 5 min / revalidate 1 h / expire 1 day. Chosen to match the page's existingrevalidate = 3600so the snapshot and the full-route cache share one cadence. Purely a safety net for a missed invalidation ping (plugin misconfigured, request failed, secret rotated).src/lib/fse/, so it can never be imported again./api/revalidate-fseroute, matching the default path in the plugin's new settings.Scope
next/next.config.tsexperimental: { useCache: true }. Narrowly scoped — it only enables the directive.New:
next/src/lib/fse/get-fse-snapshot.tsnext/scripts/fetch-fse-templates-and-parts.ts(the two GraphQL queries,formatBlocksJSONwithskipGetData: true, template-part inlining, Polylangtranslationsgrafting) into an exported async function.'use cache',cacheTag('fse-templates'),cacheLife('hours').next/src/lib/get-node-by-uri.ts@ts-ignoreabove it, now unnecessary).getTemplateBlocks()(line 326) becomes async, reading fromgetFseSnapshot(). Its call site (lines 125–128) already sits inside aPromise.allSettled, so it becomesenrichTemplateBlocks(await getTemplateBlocks(...), lang).translations[lang]substitution andcore/post-contentinjection are unchanged.New:
next/src/app/api/revalidate-fse/route.tssecretagainstprocess.env.REVALIDATE_SECRET, matching the existingnext/src/app/api/revalidate/route.ts(same env var, same query-arg convention, same 401 on mismatch).revalidateTag('fse-templates').await fetch(...)fan-out — see Research: cheap cache invalidation vs selective warming for large sites #130. Return a small JSON body consistent in shape with the existing route.Cleanup
predevandprebuildfromnext/package.json(lines 6, 8).fse-templates-and-parts.jsonentry fromnext/.gitignore(lines 51–52); add the new debug-dump path instead.next/scripts/fetch-fse-templates-and-parts.tsto a thin CLI callinggetFseSnapshot()and dumping to the gitignored debug path.docs/fse-templating.md— its entire "Build time" phase, the architecture diagram, the mermaid schema, and the "Updating the JSON" section all become wrong.Acceptance criteria
npm run buildno longer contacts WordPress, and succeeds with WordPress unreachableGET /api/revalidate-fse?secret=…returns 200; a wrong or missing secret returns 401translationsmap)core/navigationinner blocks are still fetched at request time viagetData, not from the snapshotrevalidateTag('fse-templates')invalidate the full-route cache entries of already-prerendered pages, or only the data-cache entry? Answer belongs in Research: cheap cache invalidation vs selective warming for large sites #130 and determines whether path revalidation is additionally requirednextjs-revalidate≥ 1.7.0: edit a template part in the site editor → change appears on the frontendReference measurements
tipee.ch against
admin-staging.tipee.ch— 702 KB snapshot, 13 templates, 3 template parts, 1163 blocks:Expected impact: visitor requests unchanged (ISR full-route cache); ISR regeneration gains ≤ 2.7 ms against a render that already makes several WordPress round-trips for
getData; builds get ~2.1 s faster and lose their WordPress dependency.Related