feat: @decocms/blocks/preview, the draft badge and the Studio bridge
TL
2h ago+1044+1044+0123456789012345678901234567890123456789-1-1-0123456789
Stacked on #694 (P2, feat/blocks-image). This is P3 of the v8 framework packages plan.
What it adds
A new subpath, @decocms/blocks/preview, with two components. Neither has framework code, and neither holds a list of Studio's origins: @decocms/blocks exports no Studio origins and no draft names (P1 was reverted after the PO's review). The bindings own those. @decocms/tanstack's document mounts both components and passes Studio's origins in (P4). A Next.js site renders both in its root layout with the origins from @decocms/nextjs (P5).
DraftBadge({ pointer, exitHref? })
- v7's
DraftPreviewBadge, unchanged on screen: the "Preview mode" pill with "Exit preview" and "Share preview". "use client". It renders nothing in three cases: whenpointeris empty, on the server, and inside a frame (Studio's site editor already shows its own chrome).exitHrefoverrides the default exit link, which is the current URL with?__draft=off.- The badge gets the
__draftparameter name fromdraft.ts. That module exports the name to the badge only; the package does not export it. - The inlined Deco mark is now a 36px PNG (about 2KB). v7 inlined the 720px original (about 22KB) into every client bundle that mounted the badge.
StudioBridge({ site, trustedOrigins, editShortcut?, nonce? })
- v7's
LiveControlsas one static inline<script>. Its settings travel indata-*attributes. - No hooks, so a Next.js Server Component can render it.
- Why a script and not an effect: Studio posts to the frame on its
loadevent, which can come before React hydrates. trustedOriginsis required, and it is the whole list the bridge answers. It uses the CSP source form:https://host, orhttps://*.hostfor any name under it, with the same scheme and port. The binding passes Studio's origins. An empty list answers nothing.- In a frame, it answers only messages from the parent window whose origin is in
trustedOrigins. It does not trust the page's own origin: under a test runner that proxies the site, the runner's own messages would navigate the page. - Messages, checked against what Studio v8 sends today:
editor::inject { args: { script } }is what Studio's Fast Preview and Local frames post. The injected script then handles thecms-editor::*messages itself.- It also keeps v7's
scrollToComponentandeditor::rerender(same-origin URLs only).
- v7's "." / Ctrl+Shift+E shortcut, which opens the page in Studio's
/choose-editoron a page that isn't framed, is on by default (PO, 2026-10-15, 280f1aa8), as in v7, on TanStack and Next alike.editShortcut={false}turns it off.
Tests
preview/preview.test.tsx(jsdom) covers the badge and the bridge.- Badge: its links, its SSR output, framed and unframed rendering, the menu, and copying the share link.
- Bridge: its script runs as a browser runs it. The cases are:
editor::injectfrom Studio;- a table of origins checked against Studio's list as a binding passes it;
- an empty list answering nothing, not even Studio or the page's own origin;
- any given origin, exact and wildcard;
- a message from a window other than the parent;
- an unframed page;
- malformed messages;
scrollToComponent;- running only once;
- the shortcut: on by default, off with an explicit
false(nodata-shortcut, "." and Ctrl+Shift+E ignored), never inside Studio's frame, never while typing in a field.
preview/exports.test.tschecks four things: theexportsentry, the exact runtime names, that the badge module starts with"use client", and that the bridge has no hooks.browserBundle.test.tsbundles/previewwith esbuild for a browser target and a workerd target.- Each bundle's inputs must be only
preview/*,draft.tsand React.studio.tsis no longer allowed, and it no longer exists. - The bundles must contain no
node:built-ins,createCMSorAsyncLocalStorage.
- Each bundle's inputs must be only
- Shortcut default (280f1aa8, on top of #694 merged in 16d05320):
bun run check,bun run build,skills:checkandskills:readme:checkpass; plain Node imports all 25 built exports anddeco --helpruns.bun run test(2 GB): 2,180 passed, 3 failed: DS-2 (fails on main too; fixed separately) and twoschema.test.tstimeouts under full-suite load (58/58 alone). Preview tests 29/29; reverting the default fails three of them. - Earlier round:
bun run check,bun run build,skills:checkandskills:readme:checkpass. bun run test: 2,153 passed and 4 failed.- One failure is DS-2 in
delivery.cli.test.ts(known). - The other three are timeouts under full-suite load: se-03 in
cli.test.tsand two inschema.test.ts. Run alone,cli.test.tsandschema.test.tspass (182/182).
- One failure is DS-2 in
For the PO
- The plan's signatures changed.
StudioBridgerenders a<script>; the plan had it returningnull.- The Next.js root does not need to re-export it as a
"use client"module. DraftBadgeaccepts anullpointer, so it can be mounted on every page.trustedOriginsis now required, because the origins belong to the binding.
- The "." shortcut is on by default (decided 2026-10-15). Follow-up above:
DecoDocumentin #696 still defaultseditShortcuttofalseand must pass the new default through;@decocms/nextjs'sStudioBridge(#697) passes props through, so it inherits it (its README line should say so). - v7's
*.deco.cxadmin origins are no longer trusted. The bindings pass Studio's origins only.
"." opens the page on screen (2026-10-16, 3d32c406, 32bdd30e)
- The shortcut sent
pathTemplate=/*on every page, so Studio's/choose-editorcould only guess the page. The bridge now sends the page'spathTemplate(andpageIdwhen known, as v7 did), read at the keypress so a client-side navigation sends the page then on screen. It comes from the bridge's newpageprop (a layout that re-renders per page:@decocms/tanstack's document) or, winning, from the lastStudioPagethe page rendered (a Next.js root layout doesn't re-render per page). StudioPage({ pathTemplate, id? })is new: a<meta name="deco:page">that React places in the<head>, so it adds nothing to the page's own markup.- The shortcut already ran on a page that isn't framed. The module comment said the bridge "does nothing unless the page is framed", which was true only of Studio's messages; it now says so. In a frame, it still answers only
trustedOrigins. - Signed-in check: v7's
LiveControlshad none on the store page, and neither does this. Studio's/choose-editoris auth-gated and sends a visitor who isn't signed in to its login and back. - Tests: a plain "." on the store page; the page's template and id from the prop; a
StudioPageon the page winning, read at the keypress; attributes changed after the script ran; "." typed in an input, textarea, select and contenteditable ignored;StudioPagehoisted into the head. Each fails without its change. - Checked in a browser on five locally built sites (three TanStack, two Next): the exact choose-editor URL with each page's template, "." in a field, the badge (shown, kept across client-side navigation and reloads, hidden in a frame, exit clears the cookie, absent on a host that may not preview), and Studio's injected editor script outlining, selecting and scrolling to sections.
Main refresh (2026-10-17, 4e244e17)
- Merged P2
0103f85finto this branch (normal merge, no conflicts), then merged up the stack to M2 #707. - Gates at the top of the stack (M2
5131a3c1):bun run check,bun run build,skills:check,skills:readme:checkpass.bun run test(2 GB memory cap): 122 files pass, 1 skipped; 2,745 tests pass, 95 skipped, 0 fail. DS-2 now passes (fixed on main by #709); no timeouts this run. - CI on this PR's new head: all checks pass.
🤖 Generated with Claude Code
https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu