diff --git a/.agents/skills/deco-nextjs-site/references/studio-bridge.md b/.agents/skills/deco-nextjs-site/references/studio-bridge.md
index 05b971e2..45929dc3 100644
--- a/.agents/skills/deco-nextjs-site/references/studio-bridge.md
+++ b/.agents/skills/deco-nextjs-site/references/studio-bridge.md
@@ -37,9 +37,11 @@ layout from being cached.
## What it does
-- **The bridge script** answers Studio's scroll-to-section and rerender
- messages, only from a parent window at one of Studio's origins. Outside a
- frame it answers no message and only listens for the "." shortcut.
+- **The shortcut script** listens for the "." shortcut, only outside a frame.
+ It answers no message: v8 only, v7's framed handler (scroll-to-section,
+ rerender) is gone, so no script on the page compares the window with `top`
+ or `parent`, and a test runner that rewrites "frame-busting" code (Cypress)
+ serves the page as is.
- **The Studio channel** runs on every page, production included (about
0.7 kB gzipped). Framed by a trusted origin, it posts `{ type: "deco:ready" }`
and imports a module only when the answer `{ type: "deco:load", url }`
diff --git a/.agents/skills/deco-tanstack-site/references/document-and-security.md b/.agents/skills/deco-tanstack-site/references/document-and-security.md
index e3b4c0bf..fbc9c7b4 100644
--- a/.agents/skills/deco-tanstack-site/references/document-and-security.md
+++ b/.agents/skills/deco-tanstack-site/references/document-and-security.md
@@ -50,7 +50,7 @@ What else it mounts:
after hydration. Work that must follow hydration waits for it.
- `NavigationProgress` and `StableOutlet` ([`router-and-navigation.md`](./router-and-navigation.md)).
- An inline script that publishes the request's draft and CDN segment token to `window`.
-- `StudioBridge`, `StudioChannel` and `DraftBadge` from `@decocms/blocks/preview` (below).
+- The "." edit shortcut, and `StudioChannel` and `DraftBadge` from `@decocms/blocks/preview` (below).
## The head
@@ -76,18 +76,18 @@ import { DecoPreview } from "@decocms/tanstack/react";
```
`DecoPreview` takes `site`, `editShortcut`, `nonce` and `studioOrigins`. It renders the draft
-script, `StudioBridge`, `StudioChannel` and `DraftBadge`. `StudioBridge` alone leaves draft pages
-without the badge. You then own ``, ``, `lang`, the events bus and the
+script, the "." shortcut, `StudioChannel` and `DraftBadge`. You then own ``, ``, `lang`, the events bus and the
progress bar.
## Studio inside the page
-- **`StudioBridge`**: in Studio's frame it talks to Studio. Outside it, "." opens the current page
- in Studio's editor: the page whose route returns `pathTemplate` (and optionally `pageId`) in its
- loader data, else Studio picks it from the URL.
+- **The "." shortcut**: outside Studio's frame, "." opens the current page in Studio's editor: the
+ page whose route returns `pathTemplate` (and optionally `pageId`) in its loader data, else Studio
+ picks it from the URL. In a frame it does nothing (v8 only: no v7 `scrollToComponent` /
+ `editor::rerender` handler).
- **`StudioChannel`**: on every page, production included, about 0.7 kB gzipped. When a trusted
Studio origin frames the page, it loads Studio's editor modules from that origin. Otherwise it
- does nothing. The bridge runs no script Studio sends.
+ does nothing. It is the only way Studio's editor reaches the page.
- **`DraftBadge`**: shown on a draft page outside Studio's frame. A draft link that can't be read
anymore shows a notice and the release.
@@ -98,8 +98,7 @@ from `@decocms/tanstack`). Each entry must be an exact `http(s)` origin; anythin
The worker puts the same list into the CSP. If you set `studioOrigins` on `DecoDocument`, set the
same list on `createDecoWorker`; `DECO_STUDIO_ORIGIN` sets both.
-When Studio asks the page to scroll to a section, `StudioBridge` looks for the element whose
-`data-manifest-key` equals the id Studio sends, else the element with that `id`. How to render
+Studio's editor modules find the page's sections by their `data-manifest-key`. How to render
sections: `deco-site-authoring`.
## Security headers
diff --git a/CLAUDE.md b/CLAUDE.md
index 8a12a222..7cc66dfe 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -105,7 +105,7 @@ Opinionated by design: the behaviour every Deco TanStack site should have is fix
| `@decocms/tanstack/server` | `src/server/index.ts` — `contentClient(cms, request?)` (`cms.client({ version, visitor, experiments, campaigns })` from the worker's `x-deco-version`, `x-deco-visitor` and `x-deco-assignments`: the page decides nothing again), `requestVersion`, `requestVisitor`, `requestAssignments`, `markPrivate`, `isServerFnCall`. Server only |
| `@decocms/tanstack/start` | `src/start/index.ts` — `decoServerFnFetch` for `createStart({ serverFns: { fetch } })`. Browser-safe |
| `@decocms/tanstack/router` | `src/router/index.ts` — `createDecoRouter`, `decoParseSearch`/`decoStringifySearch` (keep `?q=`, v7 #646), `PAGE_STATUS_HEADER`. Isomorphic |
-| `@decocms/tanstack/react` | `src/react/index.ts` — `DecoDocument` (mounts `StudioBridge`, `StudioChannel` (its `studioOrigins` prop, else `studioOrigins()` of `src/studio.ts`: `DECO_STUDIO_ORIGIN` comma-separated, else `DEFAULT_STUDIO_ORIGINS`) and `DraftBadge` from `@decocms/blocks/preview`, publishes the `__cseg` token and the draft), `NavigationProgress`, `StableOutlet`; `Image`, `Picture`, `Source` (v7's props and markup) with `ImageUrlProvider` for the site's `ImageUrl` from `@decocms/blocks/image`; no platform knowledge (without a provider, `DECO_IMAGE_URL`: every source through the Deco CDN), the site lists its platform's helper (`@decocms/apps-vtex/image`, …) itself. `tests/tanstack-image-markup.golden.test.ts` holds v7's markup byte for byte (250 cases: no width and Nuvemshop images included, the latter with a store's own provider) with those helpers, outside the package since it depends on no `@decocms/apps-*`. Browser-safe |
+| `@decocms/tanstack/react` | `src/react/index.ts` — `DecoDocument` (mounts the "." shortcut (`src/studioShortcut.ts`, the same file as `@decocms/nextjs`'s; v8 only, no v7 framed handler, Cypress-safe: `tests/studio-shortcut.test.ts`), `StudioChannel` (its `studioOrigins` prop, else `studioOrigins()` of `src/studio.ts`: `DECO_STUDIO_ORIGIN` comma-separated, else `DEFAULT_STUDIO_ORIGINS`) and `DraftBadge` from `@decocms/blocks/preview`, publishes the `__cseg` token and the draft), `NavigationProgress`, `StableOutlet`; `Image`, `Picture`, `Source` (v7's props and markup) with `ImageUrlProvider` for the site's `ImageUrl` from `@decocms/blocks/image`; no platform knowledge (without a provider, `DECO_IMAGE_URL`: every source through the Deco CDN), the site lists its platform's helper (`@decocms/apps-vtex/image`, …) itself. `tests/tanstack-image-markup.golden.test.ts` holds v7's markup byte for byte (250 cases: no width and Nuvemshop images included, the latter with a store's own provider) with those helpers, outside the package since it depends on no `@decocms/apps-*`. Browser-safe |
| `@decocms/tanstack/vite` | `src/vite/index.ts` — `decoTanstack({ buildHash })` (= `decoContent()` from `@decocms/blocks/vite`, `__BUILD_HASH__`, a browser stub for `node:async_hooks`, `resolve.dedupe`), `gitBuildHash` |
| `@decocms/tanstack/scope` | `src/scope/server.ts` / `browser.ts` — the request scope the worker hands the render (`AsyncLocalStorage`). Conditional export: `workerd` and `node` before `browser`, because a Workers build turns `browser` on too; `src/__tests__/exports.test.ts` resolves it under those conditions |
@@ -117,7 +117,7 @@ Small helpers for any Next.js App Router app, with no hosting assumptions (no CD
|---|---|
| `@decocms/nextjs/proxy` | `src/proxy/index.ts` — `decideDraft(cms, request)` (the CMS's preview hosts, `cms.readVersion`, `src/drafts.ts`), `draftRequestHeaders` (`x-deco-version`; forged `x-deco-version`/`x-deco-visitor`/`x-deco-assignments` removed), `rewriteToDraftRoute` (`/_draft`, with the visitor and the assignments when given), `applyDraft` (cookie, `draftResponseHeaders`), `isDraftRoutePath`, `decideVisitor` (`readVisitor`; the cookie on a first visit only, never from a preview; reads no content), `visitorRequestHeaders` (`x-deco-visitor`), `decideAssignments` (every test and campaign, decided once: `cms.client({ visitor, ...version })`), `assignmentsRequestHeaders` (`x-deco-assignments`), `applyVisitor`, the assignments route (`rewriteToAssignmentsRoute`, `assignmentsSegment`: every name with its state), `draftProxy` (all of it). `DraftDecision.version` is `cms.readVersion`'s shape (blocks' `parseVersion` after the binding's host check). No React, no `next/headers`, no block map (`src/__tests__/exports.test.ts`) |
| `@decocms/nextjs/server` | `src/server/index.ts` — `bindDraft(cms, { searchParams })` (param, then `x-deco-version`, then cookie; the same host check), `activeDraftPointer`, `contentClient(cms)` (`cms.client({ version, experiments, campaigns })` with what the proxy decided), `assignedClient(cms, segment)` (async: the assignments route's fully decided client, a test at 0% kept off); `bindDraft` also binds the proxy's `x-deco-visitor` and `x-deco-assignments` (for a preview always; on the current release only when it saves an `experiments` or `campaigns` block, so a static page stays static, and then a test at 0% stays off whatever the header says). The draft lives in React's `cache()` (`server/slot.ts`), so no conditional export is needed |
-| `@decocms/nextjs` | `src/index.ts` — `DraftPreviewIndicator` (renders `DraftBadge` for the bound draft), `DraftBadge`, `StudioBridge` (`src/StudioBridge.ts`: blocks' bridge with `trustedOrigins` set to the package's `STUDIO_ORIGINS`, followed by blocks' `StudioChannel` for `studioOrigins(props.studioOrigins)`), `withStudioCsp` (`src/csp.ts`: the site's own CSP with Studio's origins in `script-src` and `frame-ancestors`; the binding sends no CSP itself), and the package's own draft names and Studio origins: `DRAFT_PARAM`/`DRAFT_COOKIE`/`VERSION_HEADER`, `draftResponseHeaders` (`src/draft.ts`), `VISITOR_HEADER` (`src/visitor.ts`), `ASSIGNMENTS_HEADER` (`src/assignments.ts`), `STUDIO_ORIGINS`/`STUDIO_FRAME_ANCESTORS`/`isStudioOrigin`/`STUDIO_ORIGIN`/`DEFAULT_STUDIO_ORIGINS`/`studioOrigins` (`src/studio.ts`). The same values as `@decocms/tanstack`'s, never imported from it (`exports.test.ts`) |
+| `@decocms/nextjs` | `src/index.ts` — `DraftPreviewIndicator` (renders `DraftBadge` for the bound draft), `DraftBadge`, `StudioBridge` (`src/StudioBridge.ts`, v8 only: the "." shortcut, `src/studioShortcut.ts` (no v7 framed handler, so nothing a Cypress frame-busting rewrite matches: `tests/studio-shortcut.test.ts`), followed by blocks' `StudioChannel` for `studioOrigins(props.studioOrigins)`), `withStudioCsp` (`src/csp.ts`: the site's own CSP with Studio's origins in `script-src` and `frame-ancestors`; the binding sends no CSP itself), and the package's own draft names and Studio origins: `DRAFT_PARAM`/`DRAFT_COOKIE`/`VERSION_HEADER`, `draftResponseHeaders` (`src/draft.ts`), `VISITOR_HEADER` (`src/visitor.ts`), `ASSIGNMENTS_HEADER` (`src/assignments.ts`), `STUDIO_ORIGINS`/`STUDIO_FRAME_ANCESTORS`/`isStudioOrigin`/`STUDIO_ORIGIN`/`DEFAULT_STUDIO_ORIGINS`/`studioOrigins` (`src/studio.ts`). The same values as `@decocms/tanstack`'s, never imported from it (`exports.test.ts`) |
| `@decocms/nextjs/config` | `src/config/index.cjs` (+ `index.d.cts`), plain CommonJS shipped from `src` — `withDeco(nextConfig, { draftHeaders, studioOrigins })`: v7's draft header rules first, `STUDIO_ORIGINS` in `allowedDevOrigins`; no rewrites, no image rules. It repeats the package's names (`src/draft.ts`, `src/studio.ts`), held equal by `config.test.ts` |
| `@decocms/nextjs/instrumentation` | `src/instrumentation/index.ts` — `register()` (the site's `instrumentation.ts`: a minimal global tracer provider whose only job is Next's root `BaseServer.handleRequest` span, recorded with `recordServerRequest` from `@decocms/blocks/metrics`; exports no span, non-recording for every other span; skips the proxy's own bubbled `handleRequest`; no-op on Edge, without `@opentelemetry/api` (the app's, else Next's compiled copy) or when another provider is registered) and `serverRequestSpanProcessor()` for a site with its own OpenTelemetry SDK. `@opentelemetry/api` is an optional peer |
| `@decocms/nextjs/image-loader` | `src/image-loader/index.ts` — `createDecoImageLoader(imageUrl, { fallback })`, `nextImageOptimizer`. Platform-neutral: the site composes its platform's helpers (`@decocms/apps-vtex/image`, …) into the `ImageUrl`; the package depends on no `@decocms/apps-*`. `tests/nextjs-image-loader.golden.test.ts` runs the next/image golden URLs of blocks and apps-vtex through that composition (no changed case but the PO-approved fix-2 ones, marked `before`/`reason` in the fixture) |
diff --git a/examples/nextjs-binding-smoke/smoke.mjs b/examples/nextjs-binding-smoke/smoke.mjs
index 3a2f6f51..2af06a69 100644
--- a/examples/nextjs-binding-smoke/smoke.mjs
+++ b/examples/nextjs-binding-smoke/smoke.mjs
@@ -100,10 +100,10 @@ try {
check("a published page is not noindex", !page.res.headers.has("x-robots-tag"));
check("the layout mounts the Studio bridge", page.body.includes('data-site="binding-smoke"'));
check(
- "the bridge answers Studio's origins (the package's STUDIO_ORIGINS)",
- page.body.includes(
- 'data-origins="["https://studio.decocms.com","https://studio-stg.decocms.com","https://decocms.com","https://*.decocms.com"]"',
- ),
+ "the bridge is v8 only: the Studio channel, and no v7 framed handler",
+ page.body.includes("data-deco-studio-channel") &&
+ !page.body.includes("scrollToComponent") &&
+ !page.body.includes("window.self!==window.top"),
page.body.match(/data-origins="[^"]*"/)?.[0],
);
check(
diff --git a/packages/nextjs/README.md b/packages/nextjs/README.md
index 306ee4a7..dacc1e7f 100644
--- a/packages/nextjs/README.md
+++ b/packages/nextjs/README.md
@@ -130,7 +130,7 @@ import { StudioBridge } from "@decocms/nextjs";