Skip to content

feat: @decocms/tanstack for v8 (createDecoWorker, DecoDocument, router, decoTanstack) - #696

Draft
tlgimenes wants to merge 17 commits into
feat/blocks-previewfrom
feat/tanstack-v8
Draft

tlgimenes wants to merge 17 commits into
feat/blocks-previewfrom
feat/tanstack-v8

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Brings @decocms/tanstack back for v8 as packages/tanstack. It is for TanStack Start on Cloudflare Workers, deployed the Deco way, and it is opinionated: there is one createDecoWorker({...}), an option only where a current site needs a different value, and routes as the one escape hatch. It depends only on @decocms/blocks. Stacked on #695 (P3).

As you asked, the draft host allowlist lives here in the binding, not in the framework. @decocms/blocks keeps its host behaviour unchanged.

Per your review of #693, this package also owns what #693 had put in @decocms/blocks. That means the draft names, the draft response headers and Studio's origins. Their values are unchanged: this is a move. @decocms/blocks exports none of them now.

What's in it

Subpath What it does
. (root) DRAFT_PARAM (__draft), DRAFT_COOKIE (__deco_draft), DRAFT_HEADER (x-deco-draft), draftResponseHeaders(pointer) (no-store, private, Vary: Cookie, noindex), STUDIO_ORIGINS, STUDIO_FRAME_ANCESTORS, isStudioOrigin. No imports, browser-safe.
/worker createDecoWorker. Server only.
/server contentClient(cms, request?), requestDraft, markPrivate, isServerFnCall. Server only.
/start decoServerFnFetch, safe in the client bundle.
/router createDecoRouter, decoParseSearch / decoStringifySearch, PAGE_STATUS_HEADER.
/react DecoDocument, NavigationProgress, StableOutlet.
/vite decoTanstack({ buildHash }) and gitBuildHash().
/scope The request scope behind AsyncLocalStorage. It is a conditional export with workerd and node before browser.

Always on in createDecoWorker

  • /deco/_liveness answers OK. /deco/*, /live/* and /.decofile answer 404.
  • Studio can frame the site: frame-ancestors from this package's STUDIO_FRAME_ANCESTORS, and no X-Frame-Options. v7's security headers are sent too.
  • Drafts. v7's allowlist decides which hosts may preview. The worker sets the draft cookie and passes the pointer to the app in x-deco-draft, removing any copy that arrives from outside. Draft responses get no-store, private, Vary: Cookie and noindex, and only a real draft skips the cache.
  • CMS redirects run before any route. The query string is kept, as matchRoute does.
  • Edge cache. The key is the build, the content revision, the device and the segment parts. It does stale-while-revalidate and stale-if-error. Signed-in, private and degraded (x-deco-degraded) responses are never stored, and safe cookies are stripped from the stored copy.
  • __cseg. The page publishes a segment token. The CDN may cache a GET server function only when the token in its URL matches the one the worker recomputes; every other response gets CDN-Cache-Control: no-store, set at one exit.
  • Set-Cookie is deduplicated by name + Domain + Path (v7 fix(tanstack): dedupe Set-Cookie by name + Domain + Path, not name alone #642).
  • Fingerprinted assets and /fonts/ are immutable. cf-cache-status is dropped on a HIT. x-page-status sets the document's status.
  • onCache({ profile, decision, ms, status }) is the only metrics hook (D3).

Draft host allowlist (D1)

This ports v7's readAllowedHosts / isDraftHostAllowed as is.

  • The site's own hosts are the preview hosts it declares (the CMS block's, or createCMS's). A site that declares none previews only on its Deco hosts, as in v7; blocks' own default (every host, for cms.draftPointer) is unchanged and isn't used by the worker.
  • The Deco hosts named after site are always added, exactly: <site>.deco.site and <site>.deco-cx.workers.dev.
  • previewHosts: [...] is v7's DECO_ALLOWED_PREVIEW_HOSTS: it replaces the CMS block's list, and the Deco hosts stay allowed.
  • previewHosts: "none" is the kill switch: no host previews, the Deco hosts and "*" included.
  • Hosts compare exactly, lowercased, with the port.
  • ?__draft=off ends a preview on any host.

Every one of these cases is tested in worker.test.ts.

The rest

  • Build hash (D4). decoTanstack({ buildHash: process.env.WORKERS_CI_COMMIT_SHA }) uses the CI's sha (12 characters), then git rev-parse --short=12, then the time, as v7 did. vite dev gets "dev", and the package reads no env.
  • decoContent() stays in @decocms/blocks/vite; decoTanstack() only calls it.
  • DecoDocument renders data-theme="light", the DECO.events bus and the data-event observer, the progress bar and StableOutlet, the published __cseg token and draft, StudioBridge, and DraftBadge. It passes STUDIO_ORIGINS as the bridge's required trustedOrigins, which is the same list the bridge answered before feat: @decocms/blocks/preview, the draft badge and the Studio bridge #695 dropped its own copy. Its options are lang, bodyClassName, account, readyDelay, outlet (main or div), editShortcut and nonce. editShortcut is left to the bridge's default, so the "." edit shortcut is on unless a site passes editShortcut={false} (PO decision 4, 2026-10-15).

Tests

  • packages/tanstack: 123 tests.
    • The worker's header matrix: 60 tests, including every allowlist case and the kill switch.
    • Router round-trips (fix(tanstack): keep empty search values so value-less query params stop redirecting (307) #646).
    • decoServerFnFetch.
    • Build-hash order.
    • DecoDocument SSR, including the origins it passes to the Studio bridge.
    • The moved draft names, draftResponseHeaders and Studio's origins (values pinned, lists frozen, isStudioOrigin cases).
    • The package depends on no @decocms/apps-* package.
    • The conditional export, resolved with Node --conditions=workerd,worker,browser.
    • esbuild browser bundles of the root, /react, /start and /router: no node:, no AsyncLocalStorage, no CMS.
  • examples/tanstack-worker-smoke is the existing smoke app moved onto the package. bun run smoke checks 25 headers and markers on workerd (vite preview, Miniflare). A new CI job, tanstack-worker-smoke, builds it and runs it.
  • guides.cli.test.ts in-11 now allows the bindings. Both bindings and every apps-* package depend only on @decocms/blocks.
  • Commands run:
    • bun run check passes. The knip hint about esbuild is already on P3.
    • bun run build passes. Plain Node imports the built root export.
    • bun run skills:check and bun run skills:readme:check pass.
    • bun run test: 2277 passed, 3 failed.
      • One is DS-2 in delivery.cli.test.ts, the known failure.
      • The other two are timeouts in schema.test.ts under full-suite load. That file passes 58/58 when run alone.
    • The workerd smoke passes 25/25.

The four TanStack sites, rewritten on local branches

None of these are pushed. Each is on its own worktree, on branch feat/decocms-tanstack-worker from feat/next-major. Each site links tarballs of this branch's @decocms/blocks and @decocms/tanstack packed at 8.3.0, with an overrides entry so only one copy of blocks is installed.

Site Lines Built under 2 GB vite preview check
Shopify storefront A −1036 / +75 yes MISS then HIT; enforced CSP + frame-ancestors; the page loader's server function is CDN-cacheable with its token
Shopify storefront B −832 / +70 yes MISS then HIT; report-only CSP; ?q= answers 200
A blog −670 / +99 yes MISS then HIT; a CMS 404 answers 404; its tests pass (21/21)
A VTEX storefront with an A/B split −3519 / +286 RAM capped at 2 GB, with swap MISS then HIT; segment is device + geo + sales channel; IS cookies; ?asJson

All of the VTEX storefront's vendored v7 worker (src/vendor/tanstack/**) goes away.

For the PO

  • The draft allowlist default (changed in review, 0ca42fe). It is v7's: with no declared preview hosts only <site>.deco.site and <site>.deco-cx.workers.dev preview. Local development (Studio's local mode on localhost:5173) needs previewHosts: ["localhost:5173"] in dev or the host in the CMS block, as v7 needed DECO_ALLOWED_PREVIEW_HOSTS.
  • Drafts are decided once, by the worker. The app reads x-deco-draft through contentClient. The worker also sets the draft cookie, so the sites' draft-cookie middleware in start.ts goes away.
  • What the sites needed that the plan's API lacked. I added each of these because a site needs it:
    • site, for the Deco preview hosts.
    • previewHosts.
    • preload.fonts, for the VTEX storefront.
    • csp.frameAncestors, for the VTEX storefront's marketing-tool editors.
    • segment can return "private", for a signed-in shopper.
    • segment can override device: both Shopify storefronts merge tablets into mobile.
  • Kept as site code, through wrapping, with no new option. In the VTEX storefront: the location-matcher geo cookies, its vendored RequestContext and loader-cache scope, the VTEX middleware, and the A/B split. These are what a full S4 migration would move next.
  • What the package does not cover yet:
    1. The VTEX storefront's router needs TanStack's hydrate option, so it keeps its own createRouter with the exported search functions.
    2. The blog's layout has a skip link and <main id>, so it keeps its own RootLayout. It renders DecoPreview (2026-10-16, below), so it has the badge and __cseg.
    3. The blog falls back to the release when a draft can't be read. That stays in the site, and contentClient could adopt it.
    4. v7's body-hash caching of POST server functions is not ported, and no v8 site depends on it.
    5. CSP nonce generation is not ported; no site enforces one.
  • The blog's cache values moved to the shared defaults, as the plan decided. The blog's GET server functions are now cached only when they load a page.
  • The VTEX storefront's build. Its peak at "rendering chunks" exceeds 2 GB of RAM; its own build script sets a 3 GB heap. I built it with RAM capped at 2 GB and swap allowed.
  • Images. This package has no image code and depends on no @decocms/apps-* package. The Image components come in feat: the v7-to-v8 migrator and skill use @decocms/tanstack and @decocms/nextjs #700; there, a site wires in its platform's URL helper itself (@decocms/apps-vtex/image or @decocms/apps-shopify/image).
  • Not in this PR: createPageLoader/pageState, and the Image/Picture components. They are not needed by the four worker entries and are left for the site PRs. Release config and npm commands are for P6.

Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig

Preload hints only on the app's pages (2026-10-15, 68167723)

  • preload hints (Link: rel=preload for the site's CSS and fonts) now go only on HTML the app rendered. A routes response (a proxied checkout, an A/B test's legacy page) is another origin's page, so it gets none; v7's worker didn't send them there either. Security headers and CDN-Cache-Control still apply at the single exit. The handler's routes result is marked internally (routed), so the worker knows nothing about A/B or compat.
  • Test: a routes HTML response keeps its own Link and the security headers but none of the worker's hints, while the app's page has them. Reverting the check fails it.

"." opens the page on screen, and DecoPreview (2026-10-16, 8b568030, 6a3abe98)

  • DecoDocument hands the bridge the page on screen: pathTemplate (and optionally pageId) from the deepest matched route's loader data, as v7's document read its CMS route's path. A page route returns pathTemplate: match.entry.path.
  • DecoPreview({ site, editShortcut?, nonce? }) from /react is what DecoDocument mounts for Studio and drafts, for a site that keeps its own document: the published draft and __cseg, StudioBridge with the page, and DraftBadge. It closes follow-up 2 above: the blog renders it and now shows the badge, keeping its skip link.
  • Tests: the bridge carries the route's template and id, nothing without one, and DecoPreview in a site's own document publishes the draft and mounts the bridge after the site's markup.

🤖 Generated with Claude Code

https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu

tlgimenes and others added 2 commits October 14, 2026 12:55
…ent, the router and decoTanstack()

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…only on its Deco hosts, as in v7

The worker deferred to cms.draftPointer whenever previewHosts was unset, so a
site with no preview hosts in its CMS block previewed drafts on every host
(blocks' default). v7's allowlist had no such default: without a declared list
a site previewed only on <site>.deco.site and <site>.deco-cx.workers.dev.
declaresPreviewHosts() tells a declared list (including an explicit "*") from
blocks' default; blocks' own behaviour is unchanged.

The smoke app declares 127.0.0.1 in its CMS block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes and others added 2 commits October 14, 2026 15:48
Brings in the PO review of P1-P3: @decocms/blocks no longer exports the
draft names, draftResponseHeaders() or Studio's origins, /image is
platform-neutral, and StudioBridge takes trustedOrigins from the binding.
CLAUDE.md conflict: both sides' tests/ and examples/ lines kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…ponse headers and Studio's origins

Moved from @decocms/blocks, unchanged values (PO review of #693): a new
browser-safe root export with DRAFT_PARAM, DRAFT_COOKIE, DRAFT_HEADER,
draftResponseHeaders (src/draft.ts) and STUDIO_ORIGINS,
STUDIO_FRAME_ANCESTORS, isStudioOrigin (src/studio.ts). The worker,
/server and the frame-ancestors use them; DecoDocument passes
STUDIO_ORIGINS to StudioBridge's now required trustedOrigins, the list
the bridge answered before. Tests moved with them, plus one that the
bridge gets Studio's origins and one that the package depends on no
@decocms/apps-* package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes added a commit that referenced this pull request Oct 14, 2026
…draft names and Studio's origins

Brings in the PO review: @decocms/blocks no longer exports the draft names,
draftResponseHeaders or Studio's origins (#693), @decocms/blocks/image is
platform-neutral with the VTEX and Shopify helpers in @decocms/apps-vtex/image
and @decocms/apps-shopify/image (#694), StudioBridge takes its trusted origins
from the binding (#695), and @decocms/tanstack owns its own copy (#696).
CLAUDE.md conflict: tests/ (helper guardrails) and examples/ lines combined.

@decocms/nextjs, in the same commit so every package type-checks:
- src/draft.ts (DRAFT_PARAM/COOKIE/HEADER, draftResponseHeaders) and
  src/studio.ts (STUDIO_ORIGINS, STUDIO_FRAME_ANCESTORS, isStudioOrigin): the
  package's own copy, same values as @decocms/tanstack's, never imported from
  it. The proxy, drafts.ts, /server and config.test.ts use it; the root
  exports it.
- StudioBridge (src/StudioBridge.ts): blocks' bridge with trustedOrigins set
  to STUDIO_ORIGINS, so the layout still passes only the site.
- /image-loader stays platform-neutral; its next/image golden comparison
  moves to tests/nextjs-image-loader.golden.test.ts, which composes
  @decocms/apps-vtex/image with decoCdn as a Next storefront does: 1,212
  cases from blocks' and apps-vtex's fixtures, none changed. exports.test.ts
  pins that the package depends on no @decocms/tanstack or @decocms/apps-*.
- nextjs-binding-smoke: decoCdn({ fit: "contain" }) (blocks' default fit is
  now cover) and a check of the origins the bridge answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes added a commit that referenced this pull request Oct 14, 2026
Brings in the PO review down the stack: blocks without the draft names or
Studio's origins (#693), the platform-neutral @decocms/blocks/image with the
@decocms/apps-vtex/image and @decocms/apps-shopify/image helper subpaths
(#694), StudioBridge's required trustedOrigins (#695), and each binding's own
copy of the draft names and Studio's origins (#696, #697).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes and others added 3 commits October 14, 2026 17:27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes and others added 9 commits October 15, 2026 11:02
Brings P1-P3's PO decisions (NODE_ENV dev mode, first-match multivariate,
deco schema --check, decoCdn leading strip, edit shortcut on by default).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
… default

DecoDocument no longer defaults editShortcut to false, so the "." shortcut
follows @decocms/blocks/preview's default (on). An explicit false still
turns it off (PO decision 4, 2026-10-15).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
…utes responses

A `routes` response is another origin's page (a proxied checkout, an A/B
test's other site); the worker's `preload` hints name this app's assets, so
it no longer appends them there. Security headers and CDN-Cache-Control
still apply at the single exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
The "." shortcut now opens the page on screen in Studio: DecoDocument reads
`pathTemplate` (and optionally `pageId`) from the deepest matched route's
loader data, as v7's document read its CMS route's `path` and `blockKey`,
and passes it to StudioBridge. It re-renders with the router, so a
client-side navigation sends the next page. A route that returns neither
leaves Studio to pick the page from the URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
…e's own document

A site whose layout DecoDocument can't express (a skip link before <main>)
kept its own document and rendered only StudioBridge, so its draft pages
had no "Preview mode" badge and "." sent no page template. DecoPreview is
what DecoDocument mounts for Studio and drafts, on its own: the request's
draft (and CDN segment token) published to the browser, StudioBridge with
the page on screen, and DraftBadge. DecoDocument renders the same pieces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant