Skip to content

feat: support Blocks v8 (content protocol) behind a flag - #7728

Draft
tlgimenes wants to merge 71 commits into
mainfrom
feat/blocks-v8-support
Draft

tlgimenes wants to merge 71 commits into
mainfrom
feat/blocks-v8-support

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

What is this contribution about?

The Studio site editor can now edit sites built on next-major Blocks (@decocms/blocks@8) through the Blocks content protocol. Editing a local deco serve (the account-less /site-editor route, and the Local draft environment when signed in) needs no org flag; the GitHub-backed protocol is behind the org flag below. The editor reads the committed schema and .deco/blocks and never runs the site's code. All of it sits behind the default-off org flag site_editor_content_protocol. With the flag off, Studio behaves as it does today.

What's in

  • Protocol from @decocms/blocks. Studio depends on the published @decocms/blocks, pinned exactly at 8.1.0-next.6, and imports its public @decocms/blocks/protocol, /protocol/server, /protocol/conformance, /protocol/storage/fs and /secrets subpaths. See "How Studio gets the protocol" below.

  • GitHub backend (API). It serves the protocol over a project's repository:

    • POST /api/:org/decofile/:virtualMcpId/:branch/rpc runs createContentHandler from @decocms/blocks/protocol/server over a storage built on RepoContentClient (apps/api/src/decofile/repo-content-storage.ts).
      • The revision is the branch head; block versions are git blob SHAs.
      • The schema comes from schema.gen.json, then meta.gen.json, then the default branch. It is served only when it says "blocksMajor": 8; a v7 site reads as schemaless and any write to it is refused (Unsupported), so even a direct API call can't write a v7 site.
      • A missing draft branch reads as the default branch, and the first write creates it.
      • Writes are guarded on the branch head. A provider conflict comes back as "stale", so the protocol core retries.
      • A tracked blocks.gen.json is regenerated in the same commit (gen-artifact.ts, shared with the existing commit coalescer).
    • GET .../changes answers what the draft branch changed against production, for the site's ?__draft= pointer. Like v7's read and save, every /rpc answer carries the pointer's token (X-Deco-Draft-Token, X-Deco-Api-Host headers, since the body is the protocol's). See "Draft previews" below.
    • These routes return 404 while the flag is off.
  • Which backend a project uses (web). One selector decides:

    • flag off → legacy (except a local deco serve connection, which is always v8);
    • connected to deco serve → protocol on the local server;
    • Local tunnel → legacy;
    • Fast Preview (cms) session whose branch has a committed schema → protocol on GitHub;
    • anything else → legacy.

    Sandbox sessions stay legacy. A failed GitHub check is never read as "no schema": the project shows as unavailable and the check retries.

  • Reading and writing (web).

    • One batched blocks.list + schema.get per poll, using ifNoneMatch, at describe.pollIntervalMs and on window focus.
    • Each save is one blocks.apply, and the client adopts the returned revision.
    • On GitHub, the ?__draft= link is the v7 Fast Preview pointer, naming the branch's changes (see "Draft previews" below). "Exit preview" still uses ?__draft=off.
  • Connecting to deco serve. No token anywhere: Studio never sends one, ignores token= in old links, remembers the endpoint per browser, reconnects on its own, accepts only 127.0.0.1/localhost/[::1] /rpc endpoints.

    • 7d1a2c79c: deco serve now answers every origin (blocks#619), so the empty state shows plain npx @decocms/blocks serve on any Studio origin; the --allow-origin guidance, its en/pt-br strings and the official-origin check are gone, and the e2e stub reflects any Origin.
    • c97b68ba3: deco serve is on localhost (blocks#619): the default endpoint is http://localhost:4545/rpc and every label shows localhost:<port>; links and remembered endpoints on 127.0.0.1 or [::1] still open and are read as localhost, so a returning user's stored server matches a new link.
    • The CLI prints <studio>/site-editor#endpoint=…&token=…. /site-editor takes the token out of the URL and keeps it for this tab only, so a reload keeps the connection. Signed in, the same link pasted into the draft selector's Local option connects a project.
    • Only servers on this machine are accepted (127.0.0.1, localhost, [::1], *.localhost).
    • The screen shows the server address and folder. The first request is what triggers Chrome's local-network prompt.
    • The editor shows a "Local server" chip (and "Read-only" when deco serve --read-only). There is no Disconnect: on /site-editor the connection follows one rule (see "Guided site editor empty state"); in a project, choosing a draft in the selector leaves Local.
    • Uploads go to PUT <server>/assets/<name> and respect describe.assets.maxBytes. On a deco serve connection, /assets/… thumbnails load from the dev app.
  • v8 content shapes.

    • A name in the manifest is a block type, not a saved block. Saved-block names can't reuse a block type's name.
    • The page list, "new page" and the URL field use the manifest's pages group, so the built-in page and route blocks such as post appear.
    • Lazy<T> fields edit T and save the {"__resolveType":"lazy","value":…} wrapper; lazy is never offered as a pick.
    • The built-in multivariate wraps each variant's value in lazy; the legacy multivariate names keep plain values.
    • Redirects read and write both the nested and the flat shape, each in the shape it was read. New redirects use the nested shape. Flat redirects get an optional status (301/302/307/308) and discardQueryParameters, and keep fields the editor doesn't know.
  • Secret widget. Write-only:

    • What the editor types is encrypted in the browser with describe.secrets.publicKey (RSA-OAEP/SHA-256 + AES-256-GCM, the protocol's ciphertext format). Only {"__resolveType":"secret","ciphertext":…} is sent.
    • With no public key, the field asks for .deco/secrets.pub.
    • v7 secrets say they can't be edited here.
    • While the backend is undecided or unreachable, the field shows a message instead of the legacy secret field, which would post plaintext to the site's encrypt action.
  • What isn't available on the protocol. Nothing renders in place, and the add-section gallery shows each block's name, description and schema @image. Loader-backed pickers fall back to the schema's options or free text. Run, runnable loaders and actions, and app install are hidden.

  • Settings (CMS settings block, v8). A "Settings" row in the Content sidebar opens the form of the site's well-known CMS block (type cms-settings: preview hosts, telemetry, analytics), from the schema. It knows only that name and type. It's offered only when the schema describes the type, so not on v7 sites or on 8.1.0-next.3, and a CMS block of another type gets an explanation instead of a form.

    • With no block yet, the form shows the defaults. The first change creates CMS.json with one blocks.apply guarded by ifMatch: { CMS: null } (b4bfc9124), so a CMS created meanwhile, of any type, is never replaced.
    • If the guard refuses the save, the editor reads the content again once the branch's writes settle, then shows the saved block or the explanation. After a guarded save lands, saves are unguarded like any other block's.
    • The guard goes through useSaveBlock / useDebouncedSaveBlock (SaveGuard, decided when the save runs) to applyProtocolPatch. Only the content-protocol backend sends it.
  • Other.

    • site-editor is now a reserved organization slug.
    • Docs: none in this repo any more. main retired the docs workspace ([chore]: retire legacy documentation workspace #7763), so the "Next-major Blocks sites" section this branch added to agentic-cms.mdx was dropped in the merge; see "Merged main" below for what still needs a home.

Flag: site_editor_content_protocol, off by default. It gates the GitHub-backed protocol only; /site-editor and the Local draft environment work without it. It is not in DEFAULT_ON_FLAGS and has no settings UI. Turn it on for an org with ORGANIZATION_SETTINGS_UPDATE, passing { "flags": { "site_editor_content_protocol": true } }.

How Studio gets the protocol

From npm: @decocms/[email protected] (exact pin, dist-tag next) is a dependency of apps/api, apps/web and packages/e2e. Nothing is vendored: the old packages/shared/src/vendor/blocks-protocol, its SYNC.md and scripts/sync-blocks-protocol.ts are gone. Every import maps to a public export:

Studio uses From
client, errors, types, storage contract, file-name rule @decocms/blocks/protocol
createContentHandler (GitHub endpoint, deco-serve e2e stub) @decocms/blocks/protocol/server
runConformance (GitHub e2e) @decocms/blocks/protocol/conformance
createFsStorage (deco-serve e2e stub) @decocms/blocks/protocol/storage/fs
encryptSecret (secret field) @decocms/blocks/secrets

Three pieces the vendored copy exposed have no public export, and the framework wasn't widened for them:

  • Ciphertext parser (ciphertext, secrets): @decocms/shared/secret-ciphertext is a small reading of the documented v1.<wrappedKey>.<iv>.<ciphertext> format. It checks that a Secret field holds a well-formed ciphertext, and gives tests what they need to decrypt one. Encryption itself uses encryptSecret.
  • In-memory storage (storage/memory): the deco-serve e2e stub now runs the public filesystem storage in a temp working tree, like deco serve, and reads the saved files from disk.
  • Asset handler (server/assets): dropped from the stub, since no spec uploads.

@decocms/blocks ships TypeScript sources. Bun (API) and Vite (web) load them as they are. Playwright runs on Node, which won't strip types under node_modules, so e2e imports the runtime pieces through packages/e2e/fixtures/blocks-protocol.ts. That file registers tsx first, the same way the package's own deco bin does. @decocms/blocks and tsx are on the e2e import allowlist.

Async rendering is off on v8. v8 has no async rendering out of the box, so on v8 sites the site editor offers no async-render control on section rows or page SEO, and never writes a Lazy wrapper. The v7→v8 migration strips Lazy wrappers, so v8 shows no async-render UI at all. The controls appear only once Studio knows the site is v7, not while the backend is still being detected. v7 is unchanged.

Out of scope

  • The hosted control plane: managed drafts, sync, releases, channels, rollback and site tokens.
  • Durable write receipts, ifSchemaMatch, ref/refs and read limits: removed from the protocol (v8 N-16: drafts are the fast-preview pointer, with only the branch's changes over the local release blocks#619). The GitHub storage is bound to the branch in the URL; since 8.1.0-next.6 its describe() no longer returns refs/idempotency, and a ref param is rejected as an unknown parameter (Invalid params).
  • ifMatch on ordinary saves (autosave is last writer wins). The one exception is Settings' first save, which is create-only.
  • Running deco serve inside sandbox pods.
  • The account-less workspace.
  • Uploads to Deco's hosted asset storage: on GitHub, uploads still go to Studio's file storage.

Production safety

Verdict: safe to merge with site_editor_content_protocol off everywhere. An independent re-audit at 7bd82f5b0 found nothing that changes v7 sites; its two remaining code items are fixed in a01bb071f (see below). v7 behaviour equals main.

Which sites are v8 (the blocksMajor rule). The schema's file name (schema.gen.json / meta.gen.json) does not tell the version. deco schema writes an explicit top-level "blocksMajor": 8, and deco check rejects a v8 schema without it. Studio treats a site as v8 only when its committed schema has blocksMajor === 8 (strict equality). Everything else is v7: a missing field, 7, "8", 9, null, a schema that doesn't parse, no schema file, or a failed GitHub read (retried every 5 s; a site already known to be v8 stays v8). The web (isV8Schema) and the API's /rpc storage apply the same rule: on a v7 site /rpc reads as schemaless and refuses every write.

Gating matrix

Surface v7 site / flag off Gate
/rpc, /changes 404 org flag site_editor_content_protocol (a failed settings read answers 404)
/rpc with the flag on, v7 site schema reads as missing; writes refused (Unsupported) blocksMajor === 8 at the branch head
Decofile GET/PATCH (classic editor, v7 draft links) unchanged, no org-settings read none needed
Editor backend choice, new blocks editor, v8 badge classic editor, new_blocks_editor flag as today, no badge flag and blocksMajor === 8
Preview toolbar, visual edit, variant tabs as on main v8 only
Schema form (resolve-schema) identical to main on every block of two real v7 metas (76 and 226 blocks) recursion cap only bounds self-referencing types
deco serve discovery, no-schema editor not reachable /site-editor route and the protocol path only
Local draft URL save public URLs saved as on main; localhost probed as deco serve (3 s) then saved as the tunnel —
/site-editor public by design (no login); site-editor is a reserved org slug pre-deploy slug check below
Docker / Helm deco @decocms/blocks is a devDependency only; .bin/deco is Studio's CLI exact pin 8.1.0-next.6 in api, web and e2e

Fixed after the re-audit (a01bb071f)

  • /rpc now enforces blocksMajor server-side (repo-content-storage.ts): readSchema returns no schema unless it is v8, and commit throws Unsupported: not a Blocks v8 site. New e2e: a v7 schema over /rpc with the flag on is not served and not written.
  • resolve-schema.ts: array items whose $ref is a union (a matcher inside Multi) stay resolved as on main, so nested matchers keep their branch schema and defaults. Re-run over the two real v7 metas: 0 of 76 and 0 of 226 blocks differ from main (previously multi.ts/negate.ts differed).

Release order

  1. Merge with the flag off everywhere. Pre-deploy: run the slug check below; docker build the API image and confirm /health answers (CI's docker-smoke builds the sandbox image, not the API image).
  2. Before enabling anywhere: release @decocms/blocks with BLOCKS_MAJOR (deco schema writing "blocksMajor": 8), bump Studio's exact pin, and re-run deco schema and commit in each v8 site (storefront-tanstack, blog-tanstack, faststore). Until then no site is recognized as v8 (the safe failure: they stay v7).
  3. Enable the flag for one internal, v8-only org.
  4. Watch API errors (/rpc, /changes) and GitHub API usage / rate limiting for that org.
  5. Widen to more orgs.

How did you verify your code works?

Run on the final tree (5d1756b52):

Check Result
bun run fmt:check passed
bun run lint (oxlint) passed
bun run knip passed (two configuration hints about packages/runtime, a package this PR doesn't touch)
bun run check (typecheck of every workspace) passed
bun run test 10170 pass, 0 fail
tsc --noEmit for web, api, shared and e2e on every commit of the branch 0 errors on each

New or updated unit tests:

  • the backend selection rules, including a failed GitHub check;
  • the poll merge;
  • loopback-only site editor links;
  • manifest-driven pages and the page type for new pages;
  • flat redirects keeping unknown fields;
  • Lazy<T> and multivariate values;
  • the secret encrypt round trip;
  • reserved slugs.

Upstream (Blocks protocol) tests: vitest packages/blocks/src/protocol, 492 pass, plus a new test that a legacy secret loader block passes the secret guard.

E2E. content-protocol-deco-serve.spec.ts passes locally against the published package (4/4, and 8/8 with --repeat-each=2). With the Settings tests (b4bfc9124): 7/7 against the local framework link. Under a heavily loaded machine, refuses a link to a server off this machine once missed its 5 s wait; it passed on every rerun. content-protocol-github.spec.ts loads and runs, but it needs the e2e API environment (the GitHub stub and its env). Against a plain dev server it fails on sign-up rate limits and on the missing stub, so CI is the signal for it.

  • content-protocol-github.spec.ts:
    • the full protocol conformance suite against the GitHub endpoint;
    • flag off gives 404; anonymous callers get 401;
    • a branch other than the bound one is refused;
    • the first write creates the branch;
    • a tracked blocks.gen.json is regenerated;
    • a legacy site's secret loader block saves.
  • content-protocol-deco-serve.spec.ts:
    • Settings: the defaults show, and the first save creates CMS.json with ifMatch: { CMS: null }; the second save carries no guard;
    • a CMS of another type written behind the editor's back survives the first save, and the editor explains it;
    • the /site-editor link flow, including that the server address is shown;
    • a link to a server off this machine is refused;
    • editing a plain field, a Lazy<T> field (stored wrapped) and a secret: the stored ciphertext decrypts with the test's key, and the plaintext never appears in storage or in a request.

Run them with bun run --cwd=packages/e2e test:e2e content-protocol. The existing decofile-api.spec.ts and fast-preview-git-sync.spec.ts are unchanged.

Screenshots/Demonstration

To add: the /site-editor screen (server address and folder), the "Local server" chip, the secret widget, and the page list on a v8 site.

How to Test

  1. Turn on site_editor_content_protocol for a test org.
  2. GitHub: open a Fast Preview project whose branch has .deco/schema.gen.json.
    • The blocks list and the schema load without the site running.
    • Edit a field: each save is one commit on the draft branch.
    • The preview opens the real site with ?__draft=….
  3. deco serve: in a v8 site run npx @decocms/blocks serve --preview localhost:<app port> (with the app's dev server running) and open the /site-editor#endpoint=… link it prints (no token: deco serve has none).
    • Allow Chrome's local-network prompt.
    • Edits are written to the working tree, and uploads go to the server's assets.
    • Stop the server: the chip says to start it. Restart it with a new token: the chip asks you to reconnect.
  4. Open /site-editor#endpoint=https://example.com/rpc&token=x: you should see "This site editor link is incomplete", in the New Layout whatever the preference says.
  5. Turn the flag off: the editor is back on the legacy path, and /rpc and /changes return 404.

Migration Notes

  • No database migrations.
  • Pre-deploy step: site-editor is now a reserved org slug. Before deploying, confirm no existing organization uses it (SELECT id, slug FROM organization WHERE slug = 'site-editor'; must return no rows); I couldn't check production data. The check is also documented next to the route in apps/web/src/router.tsx.

Follow-ups

  • Move the exact pin from 8.1.0-next.6 to the stable @decocms/blocks@8 once it's released.
  • Durable write receipts and ifMatch on every save, with managed drafts.
  • deco serve in sandbox pods, so sandbox sessions on v8 sites can use the protocol.
  • Blog tab detection from the manifest, and the page types each section is allowed on.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working (unit, typecheck, lint, format, knip and web build green; deco-serve e2e green locally; GitHub e2e runs in CI)
  • Documentation is updated
  • No breaking changes (the GitHub-backed protocol is behind a default-off flag; /site-editor is reachable only by URL; the Local option keeps today's tunnel behaviour for any non-deco serve URL)

🤖 Generated with Claude Code

https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig

Draft previews: the Fast Preview pointer, changes only

v8 keeps v7's Fast Preview protocol: the site gets a ?__draft= pointer and fetches it. The only change is what the pointer answers: what the draft branch changed against production, not the whole decofile. The site's cms.forDraft(pointer) layers it over the production content it already has (blocks docs: /next/content-delivery#draft-previews; SDK side: decocms/blocks#619).

Pointer (same shape as v7, with a /changes suffix)

  • <apiHost>/api/<org>/decofile/<vmcp>/<branch>/changes?token=<draft token>@<revision>.
  • The token is the existing v7 draft token (signDraftToken): HMAC-signed, scoped to org, project and branch, 6 hours.
  • The version is the last revision the editor saw. Studio doesn't read it; a new save gives the pointer a new address, so the preview refetches.
  • The token comes the v7 way: v7's read and save return it in their body, and every /rpc answer (session only, so a draft token can never mint another) carries it in the X-Deco-Draft-Token and X-Deco-Api-Host headers. The editor's protocol client stashes it, and replaces it only an hour before its 6-hour expiry, so polling doesn't reload the preview iframe. There is no separate token route.

Answer (GET .../changes, apps/api/src/decofile/draft-changes.ts)

  • {format: 1, set: {name: block JSON}, delete: [names]}, computed from Git on every request. Nothing is prepared or stored.
  • Studio diffs the branch head against its merge base with the production branch, file by file. The merge base comes from a dedicated RepoContentClient.mergeBase (GitHub: compare?per_page=1, reading only merge_base_commit; GitLab/Bitbucket: their merge-base endpoints), never the full compare payload, so a branch far from production doesn't time out:
    • an edited block file wins whole;
    • a deleted file becomes a tombstone;
    • aliases resolve by the shared key rule;
    • only changed bodies are read, through the blob cache.
  • A computed answer is reused for up to 60 s per branch head (at most 100 heads), so repeated preview requests cost one getBranch call. A new save is a new head.
  • A branch that doesn't exist yet changed nothing ({format: 1, set: {}, delete: []}), so a pointer minted before the first save works.
  • Cache-Control: no-store, Access-Control-Allow-Origin: *.
  • Errors: 401 without a valid token or session, 404 with the flag off, 409 on a provider conflict, 413 over 8 MiB of changed blocks, 422 when a changed block isn't valid JSON ({error, file} names it), 502 for the Git provider.
  • Known gap: unscheduled blog planning posts aren't projected into /changes the way the v7 anonymous read projects them (projectPlanningPostsForPreview), so they don't show in v8 previews yet. Documented; blog-tanstack doesn't use drafts today.

v7 unchanged

  • The plain GET .../:branch and PATCH behave exactly as on main: whole decofile for the anonymous ?token= read, and a fresh token on every editor read and write.

Editor

  • useProtocolDraft builds the pointer from the stashed token and the current revision. There is no readiness state: until the first /rpc answer there is no pointer, as with v7 before its first read.

Tests

  • draft-changes.test.ts: merge base, aliases, __proto__, app roots, missing branch, size limit, delete-only branch, rename (delete + set), deleting the winning spelling of an aliased name, malformed block JSON, per-head reuse.
  • e2e (content-protocol-github.spec.ts): flag off and anonymous access, /rpc answers carry the token and a token can't call /rpc, a save shows only its changes in /changes, the token is scoped to its branch, and the v7 read of the same branch still returns the whole decofile. decofile-api.spec.ts (v7) is unchanged.
  • The GitHub e2e stub compares commit shas, as GitHub does.

Replaced the overlay delivery with the changes pointer

  • Removed POST .../preview, revisionOnBranch, GET .../site-connection, the /rpc post-commit overlay hook, overlay uploads to object storage, /api/_delivery (draft-delivery.ts), overlay grants and site tokens, the "Preparing preview" state and draft-preview-loop.test.ts (it ran the overlay SDK; the changes SDK isn't published yet).
  • draft-token.ts, paths.ts, key-utils.ts and org-fs.ts are back to main.

Testing v8 locally

  • /site-editor (no account): a top-level route that is not gated: no org flag and no preference. It always renders in the New Layout (the project-first app takeover), even when the user's New Layout preference is off; a route-scoped override (ForceProjectFirstNav, set only by routes/site-editor.tsx) does that without a second copy of the layout. It opens the same Site Editor app a project launches (its Preview and Content tabs; Preview has the page picker; Content edits blocks and saves through deco serve), full screen with no sidebar. Hosted features are hidden: no chat, GitHub, drafts, publishing/releases or Code tab. It talks to a local deco serve over the content protocol. The connection arrives in the URL (/site-editor#endpoint=…&token=…); Studio removes the token from the address bar, keeps it for that tab only, and strips it from analytics. Only servers on this machine are accepted.
  • Site Editor active in the sidebar: in the New Layout rail, the Site Editor app entry is active at /:org/:project/site-editor (and its Content and Code tabs) and at /site-editor. Signed in on /site-editor, the rail lists the user's orgs, then the Site Editor entry, highlighted (aria-current="page", linking to /site-editor); signed out there is no rail. There is no org there, so no recent app is recorded.
  • Verified against a migrated storefront: storefront-tanstack (feat/next-major, @decocms/[email protected]) with deco serve --preview localhost:5190, driven with Playwright, signed out at /site-editor and signed in through a project's Local environment: the schema loads (recursive commerce Product types no longer freeze the tab), the preview frames the real storefront, a text edit lands in .deco/blocks/pages-home.json and shows in the preview about 2s later, and Replace image uploads through deco serve (/assets/<name>). Site-side fixes that made this work: frame loads get frame-ancestors for Studio instead of X-Frame-Options: SAMEORIGIN, vite dev skips the edge cache, and the dev server reloads open pages when the content module changes.
  • Preview URL: deco serve reports the app URL in the content protocol's describe (describe.preview.url). Set it with deco serve --preview localhost:8001 (host:port or a full URL); by default it reads server.port from the Vite config, else http://localhost:5173. Studio loads it in the Preview tab only if it is a loopback URL. The signed-in Local draft environment uses the same value.
  • Signed in: the Local draft environment. In the draft environment selector, Local replaces the old localhost option. Paste a deco serve link and Studio checks the server answers, then switches the project to v8 (inline errors when the server stopped or restarted with a new token). Any other URL is a tunnel, handled exactly as today (v7). Works for every org without a flag; the GitHub-backed protocol stays behind site_editor_content_protocol.
  • Variant tabs on v8: a v8 site has no x-deco-matchers-override, so a variant tab forces its variant in the preview's ?__draft= pointer instead. It adds one reserved __variant=<block>@<path>=<index> parameter per forced variant, and cms.forDraft applies them (v8 N-15: preview a forced variant through the draft pointer; dev reload recipes blocks#618). For local deco serve it is a pointer to the serve endpoint carrying only the forced variants, never the token. On the GitHub backend they go into the Fast Preview pointer. v7 projects keep the header. Verified live against storefront-tanstack: variant 2 shows on its tab, variant 1 comes back on its tab, and a forced variant survives the reload after a save.
  • v8 notice: a small v8 badge with a tooltip in the header of /site-editor and the signed-in site editor, on v8 sites only. v7 sites show nothing new.
  • New blocks editor on v8: every content-protocol (v8) site gets the redesigned blocks editor, on /site-editor (no org) and signed in, whatever the org's new_blocks_editor flag; v7 sites keep following the flag. While the site's version is detected the editor stays in its loading state instead of flashing the old one (bbf06e2).

Guided site editor empty state

/site-editor follows one rule, with no way to disconnect:

  1. Find the server: the endpoint in the link's hash (#endpoint=…, which wins), or else the last one used in this browser, then the default 127.0.0.1:4545.
  2. If it answers, the editor opens.
  3. If not, the empty state shows and keeps looking, backing off from 1s to 15s and pausing while the tab is hidden. The editor opens as soon as the server answers.
  4. If the server stops while editing, the page goes back to the empty state and reopens the editor on its own when the server is back.

The empty state explains the site editor, shows the deco serve command to copy (with --allow-origin <origin> when Studio isn't on an official address), the "Looking for deco serve on …" line, and docs links. deco serve on another port prints a link with #endpoint=…; opening it is how you reach that port (there is no address field). On a deployed Studio that doesn't have Chrome's local-network permission yet, a single "Look for deco serve on this computer" button starts the search, so Chrome's prompt comes after a click; a denied permission is explained.

Guided messages remain only where the server answered but can't be used: out of date (asks for a token), version mismatch, another program on the port, an error, and read-only (chip). Verified headless with Playwright: a fresh context with nothing on 4545 shows the empty state; /site-editor#endpoint=http%3A%2F%2F127.0.0.1%3A4547%2Frpc opens the storefront editor; stopping the server returns to the empty state and restarting it reopens the editor; no disconnect control anywhere.

No schema yet (72ce0c33c): without .deco/schema.gen.json, the editor (/site-editor and the Local draft) still opens. Content lists every saved block grouped by __resolveType; a block opens as plain fields inferred from its JSON plus the raw JSON editor, and a save keeps every untouched value exactly; Preview's Blocks panel shows the current page the same way. One banner (npx @decocms/blocks schema with copy, docs link) goes away on its own when the schema appears. Reads schema.get's new schema: null (decocms/blocks#619 766b0ff7) and an older server's NotFound alike. Verified headless with Playwright on a storefront-tanstack copy with the schema removed: list, open, edit and save round-trip, then deco schema and the banner left.


Framework side: decocms/blocks v8 stack #602–#614 (spec: decocms/blocks#585). Studio consumes it as @decocms/[email protected] from npm.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig


Summary by cubic

Adds editing Blocks v8 sites through the Blocks content protocol, from published @decocms/[email protected]. A site is v8 only when its committed schema says "blocksMajor": 8; legacy projects are unchanged. A local deco serve needs no org flag; the GitHub-backed protocol sits behind the default-off site_editor_content_protocol flag.

What changes

  • POST /api/:org/decofile/:virtualMcpId/:branch/rpc serves the protocol over the repository (git blob shas as versions, branch head as revision), regenerates blocks.gen.json in the same commit, and refuses any read or write on a v7 site.
  • Drafts keep v7's Fast Preview pointer through GET .../changes, which answers {format:1, set, delete} — what the branch changed against production since their merge base, computed per request and cached briefly per head. The token comes from the v7 decofile read body, fetched on load and again after each save.
  • /site-editor and the signed-in Local draft connect to a loopback deco serve with no token; the GitHub-backed protocol needs the flag.
  • One content backend per project and branch; a failed GitHub probe leaves the project on v7 quietly. The editor polls one batched blocks.list + schema.get with ifNoneMatch; each save is one blocks.apply adopting the returned revision.
  • v8 shapes: manifest short block-type names (blocked as new block names), Lazy<T> wrappers, the built-in multivariate, flat redirects with optional status/discardQueryParameters, and write-only Secret fields encrypted in the browser.
  • A Settings row edits the site's cms-settings block; the first save is create-only, guarded with ifMatch {CMS: null}. With no schema yet, blocks open in plain fields inferred from their JSON plus the raw JSON editor.
  • No async rendering UI on v8; v8 sites always get the redesigned blocks editor. Variant tabs force their variant into the preview's ?__draft= pointer through reserved __variant parameters.
  • Uploads go to PUT <server>/assets/<name> within describe.assets.maxBytes; the preview loads describe.preview.url (loopback only); the add-section gallery shows schema images since site code never runs.
  • @decocms/blocks is a devDependency in the API, so the Docker image runs Studio's CLI by path and the Blocks deco bin never shadows it. Before deploying, confirm no existing org uses the now-reserved site-editor slug.

Since the merge with main, the studio docs moved to deco-sites/docs-tanstack (main retired the docs workspace) and the bun.lock is main's plus only the pinned @decocms/[email protected] and its nested zod.

Written for commit 55f6063. Summary will update on new commits.

Review in cubic Turn on auto-fix

Moved to published @decocms/[email protected]

  • apps/api, apps/web and packages/e2e now depend on @decocms/blocks@^8.1.0-next.3 from npm (compiled dist); bun.lock has no local paths.

  • Dropped the tsx raw-.ts loader (packages/e2e/fixtures/blocks-protocol.ts) and tsx from e2e devDependencies; e2e fixtures import @decocms/blocks/protocol/{server,storage/fs,conformance} directly.

  • Typecheck, lint, fmt, knip, unit tests (1391 pass) and both builds pass. Two GitHub content-protocol e2e failures predate this bump (spec expects preview.origin where the protocol returns preview.url; the legacy-secret fixture encrypted: "0a1b2c3d" is rejected as malformed ciphertext) and are tracked separately.

  • Fixed both GitHub content-protocol e2e failures (3332863): the spec now expects preview.url, and a bun patch carries the dropped upstream legacy website/loaders/secret.ts exemption (blocks ccdf6c25) into @decocms/[email protected] until a release includes it; v8 secrets stay strict.

  • Removed the v8 unwrap-only "Disable async rendering" control (2f3f994): the v7→v8 migration strips async-render wrappers, so v8 sites show no async-render UI; v7 is unchanged.

On published @decocms/[email protected]

  • apps/api, apps/web and packages/e2e now depend on @decocms/blocks@^8.1.0-next.4 (bc5b85e).
  • The bun secret-guard patch is gone: next.4 ships the legacy website/loaders/secret.ts exemption upstream, so patches/@decocms%[email protected] and its patchedDependencies entry are removed. legacy-secret-guard.test.ts passes without it; v8 Secret fields stay strict.
  • Typecheck (api, web) and unit tests (10295 pass, 0 fail) pass.

On published @decocms/[email protected]

  • apps/api (devDependency), apps/web and packages/e2e pin @decocms/blocks exactly at 8.1.0-next.6 (b163a2d). bun.lock changes only the @decocms/blocks entries (version + integrity; its deps and peers are identical to next.4), and bun install --frozen-lockfile passes, so every other package resolves as before.
  • The next.4 shim in repo-content-storage.ts (refs, idempotency: null in describe()) is gone; the e2e check that blocks.list rejects a ref param is back (now -32602 Invalid params: an unknown parameter).
  • next.6 answers schema.get without a schema as a result instead of NotFound; the v7-site e2e check now asserts it never reads as a v8 schema (4a1e921). next.6 also ships the SDK side of the draft changes pointer.
  • Typecheck, lint, format, unit tests (10466 pass, 0 fail) and the GitHub content-protocol e2e (10/10, one worker) pass.

Merged main (a702910, 55f6063)

  • Merged origin/main (no rebase; feat/blocks-v8-hosted stays stacked on this branch).
  • Docs workspace retired ([chore]: retire legacy documentation workspace #7763): accepted main's deletion of apps/docs, including agentic-cms.mdx (en and pt-br), which this branch had extended. The Blocks site-editor docs live in deco-sites/docs-tanstack (storefront/blocks/next/site-editor.mdx), which already covers deco serve, /site-editor, uploads to public/assets/, write-only secrets via .deco/secrets.pub, schema-only pickers and gallery cards. These Studio-side details from the dropped section have no home yet:
    • Studio treats a site as v8 only when its schema declares "blocksMajor": 8; any other site keeps the v7 editor, and v8 sites show a v8 badge.
    • Signed in: draft selector Advanced → Local takes the deco serve link, checks the server answers before switching, shows a Local server chip, reconnects on restart, and leaves Local when a draft is chosen. A tunnel URL there still works for v7 sites.
    • Studio accepts only links to a server on your machine; 127.0.0.1 and [::1] links open as localhost.
    • Fast Preview sessions use the protocol only when the branch's committed schema has "blocksMajor": 8 and the org has site_editor_content_protocol (off by default): each save is one commit on the draft branch, the schema comes from that branch (or the default branch), and the preview gets a ?__draft= link. Sessions with a sandbox stay on the existing path.
  • bun.lock: main's lockfile plus only @decocms/[email protected] (apps/api, apps/web, packages/e2e) and its nested [email protected]: a 7-line diff against main. bun install --frozen-lockfile accepts it unchanged, so every other package resolves exactly as on main.
  • No other conflicts; main's changes to apps/api/package.json, apps/web/src/router.tsx, query-keys.ts and tool-io.ts merged cleanly. v7 behaviour is main's.
  • Typecheck, lint, format, knip and unit tests (10470 pass, 0 fail) pass. Content-protocol e2e (one worker): 15/17 on the first run; the two failures (Local draft option, refuses an off-machine link) pass on a rerun of just those tests (2/2).

@github-actions github-actions Bot added the claude PR authored by a coding agent label Oct 3, 2026
Comment thread packages/shared/src/vendor/blocks-protocol/ciphertext.ts Fixed
Comment thread packages/e2e/fixtures/deco-serve-stub.ts Fixed
tlgimenes and others added 24 commits October 6, 2026 15:30
Copies @decocms/blocks/protocol (unpublished v8) into
packages/shared/src/vendor/blocks-protocol with a sync script and a
SYNC.md note, and exposes it as @decocms/shared/blocks-protocol*.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Default off. Gates the content-protocol editing path for next-major
Blocks sites.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Adds a ContentStorage over RepoContentClient (one repo, app root and
branch; blob shas as versions, the branch head as the revision) behind
the vendored createContentHandler, at

  POST /api/:org/decofile/:virtualMcpId/:branch/rpc
  GET  /api/:org/decofile/:virtualMcpId/:branch/draft-grant

Both sit under the existing scope middleware (session, cms plan,
repository binding) and answer 404 unless the org has the
site_editor_content_protocol flag. They don't need a preview server.
A missing branch reads as the default branch until the first write
creates it. The tracked blocks.gen.json regeneration moves to
gen-artifact.ts, shared with the commit coalescer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Runs the protocol's black-box conformance suite against the new
endpoint over the GitHub stub, plus the flag gate, session auth, the
bound branch, branch creation on first write and the tracked
blocks.gen.json. The stub now hashes blobs the way git does, so
versions computed from content agree with its tree.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
useContentBackend decides, per project and branch, whether the editor
reads and writes the legacy way or through the Blocks content protocol:
a connected deco serve, or Studio's GitHub backend when a Fast Preview
session's branch has a committed schema. Behind the org flag; pending
while the flag or the probe loads, so a project never switches backends
mid-read.

On the protocol, useDecofile and useLiveMeta poll blocks.list and
schema.get with ifNoneMatch at describe.pollIntervalMs and on focus, the
write hooks send one blocks.apply each and adopt its revision, and a
poll keeps entries still being saved. On GitHub the draft pointer comes
from the new draft-grant route plus the list revision.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
/connect#endpoint=…&token=… (the link deco serve prints) takes the
connection out of the fragment, keeps it for the tab across the login
redirect and hands over to /$org/connect, which calls describe (Chrome's
Local Network Access prompt), lets the editor pick the project and
opens its site editor. The connection is stored per project in this
browser.

The site editor shows a Local server chip with Disconnect, says when
the server restarted with a new token or isn't running, and hides the
publish and terminal surfaces: the developer commits. Image and file
uploads go to PUT <server>/assets/<name> when describe.assets allows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
- A key in the manifest is a block type, so short names like `hero`
  aren't read as saved blocks, are offered in block pickers, and can't
  be taken by a new saved block. Without the manifest, the module-path
  heuristic is unchanged.
- Lazy<T> fields render the form of T and store the lazy block around
  the value; `lazy` is never offered as a pick. The built-in
  `multivariate` keeps each variant's value in a lazy block; the legacy
  names keep plain values.
- Redirects list and edit the flat `redirect` shape next to the nested
  one, keep the shape they were read in, and edit its optional status
  (301/302/307/308). New redirects stay nested.
- On the protocol, Secret fields are write-only: what an editor types is
  encrypted in the browser with describe.secrets.publicKey and only the
  secret block with its ciphertext is saved. v7 secrets say they can't
  be edited there.

The content protocol never runs site code, so on it:
- the preview is the real app (the draft on GitHub, the dev app for a
  connected deco serve) with no in-place /live/previews renders;
- the add-section gallery shows each block's name, description and
  schema @image instead of rendered thumbnails;
- loader-backed pickers fall back to the schema's options or free text;
- Run, runnable loaders and actions, and app install are hidden.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
A stand-in deco serve (the protocol's handler over an in-memory working
tree, on 127.0.0.1 with a token) is connected through /connect; the spec
edits a field, a Lazy<T> field (stored wrapped) and a Secret field, and
checks the stored ciphertext decrypts with the test's private key while
the plaintext appears in no stored file and no request.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Documents the site_editor_content_protocol flag, when a project uses
the content protocol (a Fast Preview branch with a committed schema, or
a connected deco serve), connecting deco serve, and what works
differently because the site's code never runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
The SEO autosave read the latest page block from a hard-coded cache key,
which a connected deco serve doesn't use. Also drops exports nothing
imports any more.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…ew server

The content-protocol storage and the commit coalescer share the commit
message builder. Only the rpc route skips the preview-server check: a
draft grant has nothing to point at without one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
/connect is now a root route (the deco serve connect link), so an org
with that slug would be shadowed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Fixes:
- Pages come from the manifest's pages group (the built-in page and
  route blocks like post), and new pages use the built-in page type.
- Connect links accept only a server on this machine, and the connect
  screen shows the server and folder before a project is picked.
- The legacy secret field (which posts plaintext to the site) renders
  only on the legacy backend; undecided or unreachable shows a message.
- A failed GitHub probe makes the backend unavailable and retries,
  instead of routing a protocol site to the legacy path for good.
- A deco serve that restarted or stopped is detected on the next read
  or write, so the chip says why.
- Uploads respect describe.assets.maxBytes; /assets thumbnails load from
  the dev app on a deco serve connection.

Cuts:
- One batched blocks.list + schema.get per poll, shared by the decofile
  and schema hooks (as the protocol docs describe), replacing two
  pollers and the probe's schema reuse.
- The draft grant is a cached query; saves no longer fetch it.
- ContentCapabilities becomes isProtocolProject; the git-status
  invalidation, cache-key choice, unavailable error and localStorage
  mirroring each live in one place; the connect routes share the default
  org resolution and layout with the home route.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
/site-editor is now a root route (the account-less site editor over
deco serve), so an org with that slug would be shadowed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
The link deco serve prints becomes /site-editor#endpoint=…&token=…: just
the site editor, talking the content protocol to the server on this
machine, with no sign-in, org, project or GitHub. The connection leaves
the URL at once and is kept for the tab (sessionStorage); only loopback
servers are accepted. /connect redirects there for older CLIs, and the
signed-in /$org/connect project picker is gone.

The editor gets a placeholder project whose org has no id: useVirtualMCP
and the org-settings read skip themselves without one, and
TabDecoServeConnectionContext hands every surface the tab's connection.
The header shows a v8/v7 badge (ContentVersionBadge).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Pasting a deco serve link into Advanced → Local checks the server
answers describe, then edits the project over the content protocol (v8);
any other URL keeps today's tunnel behaviour (v7). The local path no
longer needs the site_editor_content_protocol flag: a connection exists
only once its link was pasted, so v7 sites are untouched. The GitHub
backend stays behind the flag. The logged-in editor header shows the
v8/v7 badge.

The e2e spec covers the account-less /site-editor (through a /connect
link) and the signed-in Local option.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
PostHog captures the first pageview before the router strips the
fragment, so /connect#…&token=… reached $current_url. Drop token from
the hash in sanitizeAnalyticsUrl.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Signed in, the user pastes the link rather than opening it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes and others added 19 commits October 6, 2026 15:32
Docs say the site editor reconnects on its own and remembers the server;
the analytics token= redaction and the storage-key comment go; connect
links must be loopback hosts deco serve accepts (no *.localhost) and the
/rpc path; the waiting screen tells a non-default origin to pass
--allow-origin.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
A "Settings" row in the Content sidebar opens the form of the site's `CMS`
block (type `cms-settings`: preview hosts, telemetry, analytics), from the
schema. With no block yet the form shows the defaults and the first change
creates CMS.json through blocks.apply. Offered only on content-protocol
sites whose schema describes the type (published 8.1.0-next.3 does not);
v7 sites are unchanged. A block named CMS of another type is not edited.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
The first save of the CMS settings block while it's absent sends
blocks.apply with ifMatch { CMS: null }, so a CMS block created meanwhile
(of any type) is never replaced. A refused save re-reads the content once the
branch's writes settle, so the editor shows the saved block or explains one
of another type; after a guarded save lands, saves are unguarded like any
other block. The guard threads through useSaveBlock / useDebouncedSaveBlock
(SaveGuard, decided when the save runs) to applyProtocolPatch; only the
content-protocol backend sends it.

e2e: the first save's request carries the guard and the second doesn't; a
CMS of another type written behind the editor's back survives the first save
and the editor explains it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
/site-editor with no link now shows a guide inside the app shell instead of
"This site editor link is incomplete": what the editor is, the command to copy
(with --allow-origin only off the official Studio origins), a status line while
Studio looks for deco serve on its own (remembered server, then 127.0.0.1:4545;
one probe at a time, 3s timeout, 1s->15s backoff, paused while the tab is
hidden, asks first where Chrome would prompt for local network access), and a
"Using another port?" field that takes the printed link, an address or a port.
Docs links come from one constant (BLOCKS_DOCS_URL).

Probe failures are classified (not answering, out of date, version mismatch,
another program on the port, server error) and every connection, schema,
read-only, save and upload state says what happened and what to do next, in
en and pt-br. Disconnect keeps that server out of discovery for the session.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
- Show the guide inside the same framed Site Editor panel as the editor.
- Command box: copy button sits inside it and the command wraps only
  between words (it broke mid-flag and squeezed into a column on phones).
- Keep inline code on one line; shorten the --allow-origin note.
- Show a found-but-unusable server inside step 2, where detection lives,
  so step 1 stays first.
- Steps: number beside the heading, full-width body on phones.
- Docs links: label above, links on one row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
/site-editor looks for deco serve at the link's endpoint, or else the
last one used and the default port. If it answers, the editor opens; if
not, the guide shows and keeps looking (backing off, paused while the tab
is hidden) and opens the editor as soon as it answers. When the server
stops answering while editing, the guide comes back until it does.

Removed: the chip's Disconnect, the session list of disconnected
servers, the disconnected/Reconnect notice, the "Using another port?"
field, the gate's "Try now"/"Use a different server" and its
not-answering guidance, and the "paste another address to switch" copy.
Guidance stays only for a server that answered but can't be used (out of
date, version mismatch, another program, error, no schema, read-only).
The Chrome local-network button is a single plain button, shown only
where Chrome would ask first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
The redesigned blocks editor is on for every content-protocol (v8) site:
`deco serve` on /site-editor, the signed-in Local draft option and the
GitHub-backed protocol. v7 sites keep following the org's
`new_blocks_editor` flag.

One rule decides (`newBlocksEditorEnabled` in content-backend): protocol or
unavailable-protocol backend -> on; legacy -> the org flag; no site (org
settings) -> the org flag; no org (/site-editor) -> no flag to read, so v8
is on and nothing waits on org settings. While the backend is still being
detected it is undecided (unless the flag is on), and the Blocks panel,
the Content browser and the page JSON panel keep their loading state
instead of flashing the old editor and swapping.

`NewBlocksEditorProvider`, mounted by the Content and Preview tabs, probes
the site's backend once for everything below it; `useNewBlocksEditor()`
reads it with the flag. The org settings switch reads the flag itself and
now says it applies to v7 sites. Forms carry `data-blocks-editor` so the
deco serve e2e asserts the new editor with no org and with the flag off;
its selectors follow the guided-first-run copy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
deco serve now answers every origin, so the guided empty state shows
plain `npx @decocms/blocks serve` on any Studio origin. Removed the
--allow-origin command variant, the "You're using Studio at…" paragraph
and its en/pt-br strings, and the official-origin check
(needsAllowOrigin/serveCommand). The e2e deco serve stub reflects any
Origin like deco serve does, instead of taking an allowed one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…ard patch

next.4 ships the legacy website/loaders/secret.ts exemption, so the bun
patch for next.3 is gone; legacy-secret-guard.test.ts passes without it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
The default endpoint is http://localhost:4545/rpc and every label shows
localhost:<port>. Links and remembered endpoints on 127.0.0.1 or [::1]
still open and are read as localhost, so a stored 127.0.0.1 server and a
new localhost link are the same server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
With no .deco/schema.gen.json yet, the v8 site editor (/site-editor and the
Local draft) no longer stops at a "forms haven't been generated" gate or a
"Blocks unavailable" error:

- The poll reads "no schema" (schema.get's schema: null, or NotFound from an
  older deco serve) as an empty, marked schema, so the blocks still load and
  the next poll asks again until the real schema replaces it.
- Content lists every saved block, grouped by __resolveType and sorted by
  name, with a search. A block opens as plain fields inferred from its JSON
  (text, numbers, switches, nested groups and lists) plus the existing raw
  JSON editor. An edit replaces only the value it touched, so a save keeps
  every other value, its type and the key order. Autosave as usual; pending
  edits are saved when the schema arrives and the regular editor takes over.
- Preview's Blocks panel shows the current page the same way.
- One calm banner on top: forms get proper fields once the schema exists,
  `npx @decocms/blocks schema` with a copy button, and a docs link. It goes
  away on its own once the schema appears.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
The docs moved from decocms/blocks to deco-sites/docs-tanstack (published
at docs.decocms.com/storefront/blocks).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Drop the overlay preview protocol (POST /preview, revisionOnBranch,
site-connection, overlay uploads, /api/_delivery, overlay grants and site
tokens, the "Preparing preview" state). v8 drafts use v7's Fast Preview
pointer; the only change is what it answers:

- GET .../:branch/changes (draft token or session, org flag) returns
  {format: 1, set, delete}: what the branch changed against production
  since their merge base, computed from Git per request (draft wins per
  file). A branch that doesn't exist yet changed nothing; over 8 MiB is 413.
- GET .../:branch/draft-token (session only) mints the pointer's token,
  since saves over /rpc carry none. A token can't mint another.
- The legacy GET/PATCH are back to main: v7 sites are unchanged.
- The editor builds <apiHost>/api/<org>/decofile/<vmcp>/<branch>/changes
  ?token=…@<revision>; a failed token fetch shows "Preview unavailable".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
- Read only the merge base (compare?per_page=1 on GitHub, merge_base on
  GitLab/Bitbucket) instead of the full compare payload, which a branch far
  from production could time out on.
- Reuse a computed answer for up to a minute per branch head.
- A saved block that isn't valid JSON answers 422 naming its file.
- Refresh the draft token an hour before it expires instead of every 30
  minutes, so the preview iframe doesn't reload on every refresh.
- Tests: delete-only branch, rename, deleting the winning spelling, malformed
  block, reuse.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…Major

- api: @decocms/blocks is a devDependency (the server bundle inlines it), so
  `bun add decocms` no longer links the Blocks CLI as `.bin/deco`; the Docker
  image runs Studio's CLI by path. smoke-tarball checks the bin resolves to
  Studio's cli.js and boots the Dockerfile command; deco-bin.test.ts guards
  the package.json/Dockerfile.
- Pin @decocms/blocks to exactly 8.1.0-next.4 (api, web, e2e). bun.lock is
  origin/main's plus only @decocms/blocks and its nested zod (bun 1.3.14).
- resolve-schema: a self-referencing type keeps its fields for 3 levels
  (MenuItem.children: MenuItem[]) instead of none; still bounded for Product.
- v8 detection: a site is v8 only when its committed schema says
  "blocksMajor": 8 (isV8Schema). A meta.gen.json/schema.gen.json without it,
  a failed GitHub probe, or a form with no project id are v7 (legacy); the
  probe retries quietly. No more "unavailable" GitHub state.
- Content-protocol routes read the org flag before anything else; a failed
  read is off (404), never an error after a commit.
- Local option: a localhost/127.0.0.1/port whose deco serve probe fails is
  saved as the v7 tunnel URL, as on main.
- v7-visible changes reverted: no v7 badge (v8 only), main's Local picker and
  blocks-editor-setting copy, preview toolbar only for a deco serve preview,
  visual edit unchanged on v7, recent apps as on main for projects,
  async-render controls shown unless v8, no "backend unavailable" text while
  the backend is being decided.
- e2e: GitHub protocol specs use a v8 schema ("blocksMajor": 8); new specs
  show a flag-on org keeps meta.gen.json sites on the v7 editor, a
  blocksMajor 8 site gets the v8 badge, and v7 decofile read/save still mint
  draft tokens.
- Unexport 5 in-file-only symbols knip flagged; document the pre-deploy
  `site-editor` org slug check at the route.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…e rebase

- Image field: main moved uploads into useImageUpload; the deco serve
  upload (v8 only) now goes through it as an optional serveUpload, and the
  /assets thumbnail retry sits on main's safeImageSrc. With no deco serve
  connection the field is main's, unchanged.
- Delete/move keep main's Local-tunnel cache key and no-op persistence;
  the content-protocol path still runs first.
- Drop the deno.json/package.json query keys main removed with
  use-blog-support.
- bun.lock: main's lockfile plus only @decocms/[email protected] and its
  nested zod (bun 1.3.14, --frozen-lockfile passes).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
The branch's "keep key-utils at the merge base" commit removed main's
.avif content type once replayed on top of main; fs-bytes.test failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
The fields now ask useContentBackend (deco serve uploads, protocol
secrets), which reads useProjectContext(); the bare CT mount has no
provider. The stub answers legacy, which is what v7 sites see.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
@tlgimenes
tlgimenes force-pushed the feat/blocks-v8-support branch from 24a7219 to 47ead55 Compare October 6, 2026 18:50
tlgimenes and others added 10 commits October 6, 2026 15:57
Studio's CORS reflects its own and localhost origins with credentials,
so the shared localhost API context never saw the pointer's `*`. A
storefront on its own origin does, which is what the test means to check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…olved

- repo-content-storage: serve the schema only when it says
  "blocksMajor": 8, and refuse any commit to a site whose schema at the
  base isn't v8, so a flag-on org can't write a v7 site over /rpc.
- resolve-schema: array items whose $ref is a union (a matcher inside
  Multi) stay resolved as on main, so nested matchers keep their branch
  schema and defaults. resolveSchema over two real v7 metas now matches
  main on every block (76/76, 226/226).
- e2e: a v7 schema over /rpc with the flag on is neither served nor written.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…tions

- /rpc answers carry the draft token and API host (X-Deco-Draft-Token,
  X-Deco-Api-Host), as v7's decofile read/save do; the separate
  GET .../draft-token route, its client and the failed-token preview card go.
- The GitHub content storage no longer advertises refs or idempotency and
  ignores ref, matching the protocol that dropped them (the two description
  fields stay only for 8.1.0-next.4's types until the next bump).
- e2e: describe without refs, tokens read from the rpc answers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…nce passes

8.1.0-next.4's conformance requires resolvedRef null without refs; the
field is only a shim until the next @decocms/blocks bump.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A content-protocol (v8) project on GitHub now gets its draft token and API
host exactly as v7 does: from the body of the session-authenticated
GET /api/:org/decofile/:vmcp/:branch read, made on load and again after each
save. The /rpc answers no longer carry X-Deco-Draft-Token / X-Deco-Api-Host,
and the protocolDraftGrant cache with its 5-hour refresh rule is gone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e shim

- apps/api (dev), apps/web and packages/e2e pin 8.1.0-next.6 exactly;
  bun.lock changes only the @decocms/blocks entries (frozen install passes).
- The GitHub content storage no longer describes refs/idempotency, which
  next.6 removed from the protocol.
- e2e: a `ref` param on blocks.list is rejected again (Invalid params,
  an unknown parameter in next.6).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
8.1.0-next.6 answers schema.get without a schema as a result, not
NotFound; the check is that it never reads as a v8 schema.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Accept main's retirement of the docs workspace (#7763): drop this
  branch's "Next-major Blocks sites" section in agentic-cms.mdx (en,
  pt-br); the Blocks/site-editor docs live in deco-sites/docs-tanstack.
- bun.lock: main's lockfile plus only @decocms/[email protected] (and
  its nested [email protected]) for apps/api, apps/web and packages/e2e; every
  other package resolves exactly as on main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The merge staged main's bun.lock verbatim; add back only the
@decocms/[email protected] entries (apps/api, apps/web, packages/e2e)
and its nested [email protected]. `bun install --frozen-lockfile` accepts it
unchanged; every other package resolves exactly as on main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

This branch has not been deployed

No deployments
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.

2 participants