feat: migrate to Deco CMS v8 (Blocks next major)

Merged
TL
2d ago
+21389-16490

Parity and approvals (2026-10-17)

Parity: zero-2: 75/75 cases, 0 differing pixels in 77 screenshots. The 47 PENDING cases are snapshot-only.
Approved by the product owner (2026-10-17), "Match prod now, fix right after": the migration keeps production's broken listings: the Shopify listing loader is called without the page URL, as on v7. This is recorded as approvedDecisions listing-match-prod (03e9a27). The follow-up that passes the URL again, right after the cutover, is the draft #136.

Summary

Moves demo-storefront from @decocms/* 7.64 (tanstack, blocks-admin, blocks-cli, apps-commerce, apps-website, apps-vtex, apps-shopify 7) to the next major of Deco CMS: @decocms/blocks@^8.1.0 and the thin @decocms/apps-shopify@^8.1.0 client, both from npm. No v7 package is left in package.json or imported from src/.

  • createCMS gets explicit params only (src/cms.ts): the site reads its own env (DECO_SITE, DECO_SITE_TOKEN, OTEL_EXPORTER_OTLP_*, DECO_SITE_NAME, DECO_ENV_NAME, the CF_VERSION_METADATA binding) and passes values. Hosted releases need both DECO_SITE and the DECO_SITE_TOKEN secret.
  • The worker's edge cache, security headers and CSP are the site's own code (src/server/edge-cache.ts), with v7's behaviour: profiles, segments (device, login, region), stale-while-revalidate/if-error, purge, drafts bypass only with a valid pointer, signed-in shoppers (the secure_customer_sig cookie) bypass, v7 admin paths answer 404.
  • Commerce loaders, cart and sign-in flows were vendored from apps-shopify 7.64.3 into src/vendor/ over the v8 client. A failed Storefront API call on a listing now shows the empty listing (breadcrumb, filters, sort, 0 results) instead of dropping the section.
  • Telemetry keeps v7's identity (service.name, deployment.environment.name, service.version) and error sampling (0.1, in the CMS block). Drafts keep v7's preview hosts.
  • The stylesheet is byte-identical to v7's (app-CIckAda9.css).
  • v7 leftovers removed: the @decocms/start migration text in the README, the dead Deno Dockerfile and Argo CD deploy/, migration-script TODOs.

The branch includes chore/parity-baseline (the parity harness, recorded on v7 main).

On @decocms/* 8.1.1 (2026-10-12, d21e0d9): @decocms/blocks and @decocms/apps-shopify ^8.1.0 โ†’ ^8.1.1; bun.lock resolves 8.1.1 (bun install --frozen-lockfile clean). .deco/schema.gen.json regenerated by the 8.1.1 CLI: only the version stamp changes (checked structurally; this site had no __@ symbol-keyed fields, the bug 8.1.1 fixes). No site code changed. deco check 39 blocks, 0 errors, 0 warnings; tests 11/11. Workers Builds passes.

Branch status (2026-10-12): @decocms/blocks and @decocms/apps-shopify bumped from ^8.1.0-next.7 to ^8.1.0 (6edda3a); bun.lock resolves exactly 8.1.0, with no prerelease left. 0 commits behind main. deco check (0/0), tsc --noEmit and tests (11/11) pass locally; Workers Builds passes. The parity table below was run on 5861b1a (next.7) and hasn't been re-run on 8.1.0. 8.1.0 needs no code change here: src/cms.ts already passes dev: import.meta.env.DEV, which 8.1.0 now reads instead of NODE_ENV (only its comment changed). The regenerated .deco/schema.gen.json changed, see PENDING item 9.

Parity (v7 main vs this branch, threshold 0)

Record/replay of Shopify upstream and per-case HARs, frozen clock and randomness, pinned Chromium; see parity/README.md.

runwhatokPENDINGDIFFupstream misses
v7-self-3v7 vs its own baseline, --no-pending --strict --strict-upstream75000
v7-self-4same, again75000
v8-9this branch vs the v7 baseline254732
v8-10this branch vs the v7 baseline (final)255002
  • PENDING means identical only once a difference the product owner hasn't approved is applied. It is never counted as ok, and --strict fails on it.
  • The v7 self-compares ran on the current harness. The baseline was re-captured from its fixtures because harMisses now carry full URLs, and it is pixel- and snapshot-identical to the earlier one apart from that field.
  • v8-9's 3 DIFFs came from a pending rule that still named images by origin + path. It now names the same six images by full URL, and v8-10 has no DIFF.
  • The 2 upstream misses are one request in pdp-variant-select, mobile and desktop. After the Color click, v8 loads /products/insulated-tumbler-with-a-straw-โ€ฆ, a shelf card, most likely as a router intent preload while the harness scrolls under a resting cursor. v7's shelf was Lazy-rendered. The query was never recorded and answers 599. Pixels and snapshot still match. Details are in parity/README.md.
  • Covered: home, landing pages, PLP, sort, search, PDP (variants, out of stock, single variant), add to cart and minicart, login, account, mobile menu, mega menu, SPA navigations, 404 and catch-all pages, robots.txt, sitemap.xml, trailing-slash redirects, favicon.
  • Not available: an editor-forms pixel compare (forms are reviewed field by field), and the 3 flows blocked on v7 (filter, show more, listing โ†’ product), which can't run on v7.

Nothing is approved. Every PENDING item below waits for the product owner.

PENDING for the product owner

Each item is a "status": "pending" entry in parity/pages.json or a note in parity/README.md.

  1. listing-gets-page-url (pending baseline, 25 cases): category, search, catch-all and /sitemap.xml pages.
    • v7 showed "12 of 0 results" everywhere, live too, because its listing loader never got the page URL. v8 passes the URL.
    • The Shopify catalog was replaced on 2026-10-10, so the old collections are empty. The v8 capture pinned in parity/pending/listing-gets-page-url/ differs from v7 only in three things: the breadcrumb (it names the collection or query), the listing JSON-LD (title, description, URL), and view_item_list's list name and id. That folder's README has per-case pixel counts, and v7-vs-new/diff/ has the diffs.
    • parity/pending/listing-live-before-catalog-change/ has v8 captured live on 2026-10-09, listing products with filters and sort.
  2. ssr-jsonld-first-html (landing pages, PDPs): JSON-LD from sections v7 wrapped in Lazy is now in the first server HTML.
  3. plp-page-url (landing pages): JSON-LD and analytics URLs use the site's origin instead of https://localhost.
  4. home-mobile-no-cart-cookie and home-utm-mobile-from-cache: v7 created a cart on an isolate's first home SSR and served that page private. v8 doesn't, so the home is edge-cached.
  5. home-shelf-images-load (3 desktop flows from the home): the tabbed shelf's first-tab images load without scrolling.
  6. home-desktop-mobile-sources (volatile list): four mobile <source> images that a desktop full-page capture sometimes requests. It is reported only on a run where dropping them changed the result. It didn't trigger in v8-10.
  7. Clickjacking (your decision): neither v7 nor v8 sends frame-ancestors or X-Frame-Options when the site sets its own CSP. The baseline headers confirm this, even though a v7 comment says otherwise. Adding frame-ancestors 'self' https://studio.decocms.com https://*.deco.studio would close it and keep the editor preview. It changes the CSP header of 55 cases, so it isn't done without your call.
  8. Behaviour changes outside the compare, all listed in parity/README.md:
    • The A/B worker split is gone.
    • Telemetry loses debug/info logs and per-signal endpoints.
    • Drafts add localhost:5173 to v7's two preview hosts.
    • Fast deploy becomes hosted releases, which need DECO_SITE_TOKEN.
    • Signed-in shoppers are detected by secure_customer_sig.
    • LiveControls is kept as a vendored copy.
    • Editor-form differences are listed in "Editor forms".
  9. Editor forms after the 8.1.0 bump (.deco/schema.gen.json, not yet checked in the site editor):
    • The recursive image/video arrays of the vendored ImageObject/VideoObject (reached from the Header's navItems and from the product types) were a bare { "type": "object" }. They're now $refs to the shared ImageObject/VideoObject definitions, so the editor shows typed fields there instead of a free-form object.
    • ProductDetails โ†’ variantSelectorConfig.preloadStrategy: the "intent"/"viewport"/"render" literals now carry an equal default.

How to run parity

npx playwright install chromium        # once (playwright 1.59.0)
# v7 baseline: on a checkout of chore/parity-baseline (v7 code + harness)
npm run parity:build && npm run parity:record
# this branch, with the baseline copied into parity/baseline:
npm run parity:build
npm run parity:compare -- --label v8                  # ok / PENDING / DIFF counts
npm run parity:compare -- --strict --strict-upstream  # fails on anything pending or unrecorded
npm run parity:compare -- --no-pending                # every case against v7, pending baselines off
npm run parity:record-pending -- --rule listing-gets-page-url   # re-pin the v8 listing capture

On the v7 checkout, node parity/run.mjs compare --no-pending --strict --strict-upstream is the self-compare.

Site editor check

parity/editor-check.mjs drives deco serve --port 4653 --preview http://localhost:5340 the way the site editor does, against vite dev on 5340. All 12 steps passed:

  • describe: working tree, writable, preview URL.
  • schema.get: 131 definitions, then notModified when sent with ifNoneMatch.
  • blocks.list: 39 blocks, 0 diagnostics.
  • A stale ifMatch is refused and the error names the current version.
  • An edit of the home's Newsletter title rendered by vite dev without a restart.
  • Restoring the original brought back the same block version and content revision, with a byte-identical file and a clean git status.

Editor forms (v7 /live/_meta vs v8 .deco/schema.gen.json) are reviewed in parity/README.md ("Editor forms"); every difference there is listed for approval.

Not covered by the compare (check by hand)

  • Hosted releases and drafts through a real ?__draft= pointer (needs DECO_SITE_TOKEN and Studio).
  • Signed-in shoppers' pages (edge-cache bypass on secure_customer_sig).
  • Telemetry delivery to the collector (parity runs send nothing).
  • The A/B worker split (withABTesting over SITES_KV) is gone; v8 has no equivalent.

๐Ÿค– Generated with Claude Code

https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig