feat(nextjs,tanstack): let Studio's HTML swap re-render the page through its router
Merged
TL
8h ago+383+383+012345678901234567890123456789-3-3-0123456789
Why
After a save, Studio's HTML swap replaces the whole document of a v8 page. React keeps the old nodes and the new HTML's scripts never run, so every client component stops at its server markup. Seen on a Next.js storefront: the footer is revealed on scroll by a client component, and after any edit in the site editor it stayed hidden for good.
What
- Protocol (
@decocms/blocks/protocol):STUDIO_PAGE_REFRESH("__decoStudioRefresh") andStudioPageRefresh, awindowhook a binding sets so Studio shows a save through the page's router. Studio vendorschannel.tsbyte for byte. @decocms/nextjs:StudioBridgerendersStudioRefresh("use client"). It callsrouter.refresh()for the page's own URL, orrouter.replace()for a moved draft pointer (the proxy reads?__draft=on the RSC request), and resolves when the transition is over.@decocms/tanstack:DecoDocumentrendersStudioRefresh, which reruns the loaders withrouter.invalidate(). For a moved draft pointer it first fetches the new link as a document, for the draft cookie that the loaders' server functions carry, then navigates. Only the matches that navigation didn't load are invalidated: a route whose loader deps include__draftalready reloaded, and invalidating it again rendered every save twice in the preview.- Both bindings keep their own copy of the name, held equal to the protocol's by a test, as with the Studio origins. The hook is set unframed too, so no code compares the window with its parent (the Cypress frame-busting rewrite).
@decocms/nextjsimportsnext/navigation.js, because the build's NodeNext resolution rejects the extensionless specifier.- A page without the hook keeps the document swap. The Studio side is decocms/studio#8435.
Verification
bun run checkpasses (typecheck, lint, knip). The only warning is the existingbiome.jsondeprecation.bun run test: 3246 passed. One failure,tel-23inobservability.test.ts, passed when run alone. It's timing-based and unrelated to this change.- New tests:
packages/nextjs/src/StudioRefresh.test.tsxandpackages/tanstack/src/react/StudioRefresh.test.tsxcover refresh vs. replace/navigate, ordering, the error and other-origin cases, and unmount.studio.test.tsin both bindings checks that the name matches the protocol's. - Spike on a TanStack storefront: with
__draftin its loader deps, each save in a local Studio now runs the page's loader once (twice before). - Spike on a live Next.js storefront:
window.next.router.refresh()after scrolling to the footer kept the footer revealed, with the same DOM node. Scrolling up and down still hid and revealed it, so React was still attached. With Studio's document swap, the same page's footer went todata-footer-reveal="hidden"and stayed there.
š¤ Generated with Claude Code