feat(web): show a save through the page's router when its binding offers one

Merged
TL
7h ago
+113-7

What is this contribution about?

After each save in the site editor, Studio's HTML swap replaces the document of a Blocks v8 page. React keeps the old nodes and the new HTML's scripts never run, so the page's client components stop at their server markup. On a Next.js storefront, the footer (revealed on scroll by a client component) stayed hidden after any edit.

When the page sets window.__decoStudioRefresh (STUDIO_PAGE_REFRESH in @decocms/blocks/protocol, set by the @decocms/nextjs and @decocms/tanstack bindings), html-swap hands it the URL instead of swapping the document:

  • It still reports html-swap:start/end and the HTML_SWAP_EVENT phases, so the progress bar and the wireframe behave as before.
  • A rejection reports html-swap:error, so Studio reloads the frame.
  • The page's origin check runs before the hook is called.

Pages without the hook (bindings not upgraded yet) keep the document swap. channel.ts is the new byte-identical copy of @decocms/blocks/protocol's (cmp checked). The bindings' side is decocms/blocks#808.

How did you verify your code works?

  • bun run verify passes: 13276 tests, 0 failed. tsc --noEmit in apps/web is clean.
  • New tests in apps/web/src/studio-modules/html-swap.test.ts:
    • The hook gets the URL, nothing is fetched, the existing DOM nodes stay in place, and start/end are reported.
    • A hook that rejects ends in html-swap:error.
    • Another origin's URL never reaches the hook.
  • Spike on a live Next.js storefront page:
    • Running Studio's document swap left the footer at data-footer-reveal="hidden".
    • Running router.refresh() instead (what the binding's hook calls) kept it revealed, on the same node, and scroll still toggled it.

How to Test

  1. Once the bindings are released, upgrade a v8 Next.js or TanStack site and open it in the Site Editor.
  2. Scroll to a client-driven element (for example, a footer revealed on scroll), then edit and save a section.
  3. The section updates without a reload and the client-driven element keeps working.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Documentation is updated (if needed)
  • No breaking changes

šŸ¤– Generated with Claude Code


Summary by cubic

Fixes the site editor's save flow so a Blocks v8 page keeps its client components working after an edit. The previous document swap replaced the HTML but React kept the old nodes and the new scripts never ran, leaving server-rendered-only components (like a footer revealed on scroll) stuck at their initial markup.

Now, when the page's binding sets window.__decoStudioRefresh (STUDIO_PAGE_REFRESH, used by the Next.js and TanStack bindings), html-swap hands it the target URL so the page re-renders through its own router instead of swapping the document. Pages without the hook keep the old document swap.

Behavior details

  • Start/end events and the HTML_SWAP_EVENT phases still fire around the router refresh, so the progress bar and wireframe behave as before.
  • A rejected refresh reports html-swap:error and Studio reloads the frame; the page's origin check runs before the hook is invoked.
  • channel.ts now carries a byte-identical copy of STUDIO_PAGE_REFRESH from @decocms/blocks/protocol; the bindings' side ships in decocms/blocks#808.