Skip to content

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

Merged
tlgimenes merged 1 commit into
mainfrom
feat/html-swap-page-refresh
Nov 10, 2026
Merged

tlgimenes merged 1 commit into
mainfrom
feat/html-swap-page-refresh

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

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 feat(nextjs,tanstack): let Studio's HTML swap re-render the page through its router blocks#808.

Written for commit cf095e5. Summary will update on new commits.

View guided diff Turn on auto-fix

…ers one

Studio's HTML swap replaced the document of a Blocks v8 page after each save.
React kept the old nodes and the new scripts never ran, so the page's client
components stopped at their server markup (a footer revealed on scroll stayed
hidden).

When the page sets window.__decoStudioRefresh (STUDIO_PAGE_REFRESH, from the
@decocms/nextjs and @decocms/tanstack bindings), html-swap hands it the URL
and reports start and end around it; a failure reports an error, so Studio
reloads the frame. Pages without it keep the document swap. channel.ts is the
new byte-identical copy of @decocms/blocks/protocol's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the claude PR authored by a coding agent label Nov 10, 2026
@tlgimenes
tlgimenes enabled auto-merge (squash) November 10, 2026 18:25
tlgimenes added a commit to decocms/blocks that referenced this pull request Nov 10, 2026
…ugh its router (#808)

## 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"`) and `StudioPageRefresh`, a `window` hook a
binding sets so Studio shows a save through the page's router. Studio
vendors `channel.ts` byte for byte.
- **`@decocms/nextjs`**: `StudioBridge` renders `StudioRefresh` (`"use
client"`). It calls `router.refresh()` for the page's own URL, or
`router.replace()` for a moved draft pointer (the proxy reads
`?__draft=` on the RSC request), and resolves when the transition is
over.
- **`@decocms/tanstack`**: `DecoDocument` renders `StudioRefresh`, which
reruns the loaders with `router.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 `__draft` already 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/nextjs` imports `next/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 check` passes (typecheck, lint, knip). The only warning is
the existing `biome.json` deprecation.
- `bun run test`: 3246 passed. One failure, `tel-23` in
`observability.test.ts`, passed when run alone. It's timing-based and
unrelated to this change.
- New tests: `packages/nextjs/src/StudioRefresh.test.tsx` and
`packages/tanstack/src/react/StudioRefresh.test.tsx` cover refresh vs.
replace/navigate, ordering, the error and other-origin cases, and
unmount. `studio.test.ts` in both bindings checks that the name matches
the protocol's.
- Spike on a TanStack storefront: with `__draft` in 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
to `data-footer-reveal="hidden"` and stayed there.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@tlgimenes
tlgimenes merged commit 7749a82 into main Nov 10, 2026
24 checks passed
@tlgimenes
tlgimenes deleted the feat/html-swap-page-refresh branch November 10, 2026 18:32
decocms Bot pushed a commit that referenced this pull request Nov 10, 2026
PR: #8435 feat(web): show a save through the page's router when its binding offers one
Bump type: minor

- decocms (apps/api/package.json): 4.567.0 -> 4.568.0

Deploy-Scope: web
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude PR authored by a coding agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant