feat: migrate to Deco CMS v8 (Blocks next major)
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/.
createCMSgets 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, theCF_VERSION_METADATAbinding) and passes values. Hosted releases need bothDECO_SITEand theDECO_SITE_TOKENsecret.- 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 (thesecure_customer_sigcookie) 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 theCMSblock). Drafts keep v7's preview hosts. - The stylesheet is byte-identical to v7's (
app-CIckAda9.css). - v7 leftovers removed: the
@decocms/startmigration text in the README, the dead DenoDockerfileand Argo CDdeploy/, 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.
| run | what | ok | PENDING | DIFF | upstream misses |
|---|---|---|---|---|---|
v7-self-3 | v7 vs its own baseline, --no-pending --strict --strict-upstream | 75 | 0 | 0 | 0 |
v7-self-4 | same, again | 75 | 0 | 0 | 0 |
v8-9 | this branch vs the v7 baseline | 25 | 47 | 3 | 2 |
v8-10 | this branch vs the v7 baseline (final) | 25 | 50 | 0 | 2 |
- PENDING means identical only once a difference the product owner hasn't approved is applied. It is never counted as ok, and
--strictfails on it. - The v7 self-compares ran on the current harness. The baseline was re-captured from its fixtures because
harMissesnow 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, andv8-10has 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 routerintentpreload 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 inparity/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.
listing-gets-page-url(pending baseline, 25 cases): category, search, catch-all and/sitemap.xmlpages.- 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), andview_item_list's list name and id. That folder's README has per-case pixel counts, andv7-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.
ssr-jsonld-first-html(landing pages, PDPs): JSON-LD from sections v7 wrapped in Lazy is now in the first server HTML.plp-page-url(landing pages): JSON-LD and analytics URLs use the site's origin instead ofhttps://localhost.home-mobile-no-cart-cookieandhome-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.home-shelf-images-load(3 desktop flows from the home): the tabbed shelf's first-tab images load without scrolling.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 inv8-10.- Clickjacking (your decision): neither v7 nor v8 sends
frame-ancestorsorX-Frame-Optionswhen the site sets its own CSP. The baseline headers confirm this, even though a v7 comment says otherwise. Addingframe-ancestors 'self' https://studio.decocms.com https://*.deco.studiowould close it and keep the editor preview. It changes the CSP header of 55 cases, so it isn't done without your call. - 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:5173to v7's two preview hosts. - Fast deploy becomes hosted releases, which need
DECO_SITE_TOKEN. - Signed-in shoppers are detected by
secure_customer_sig. LiveControlsis kept as a vendored copy.- Editor-form differences are listed in "Editor forms".
- Editor forms after the 8.1.0 bump (
.deco/schema.gen.json, not yet checked in the site editor):- The recursive
image/videoarrays of the vendoredImageObject/VideoObject(reached from the Header'snavItemsand from the product types) were a bare{ "type": "object" }. They're now$refs to the sharedImageObject/VideoObjectdefinitions, so the editor shows typed fields there instead of a free-form object. ProductDetailsโvariantSelectorConfig.preloadStrategy: the"intent"/"viewport"/"render"literals now carry an equaldefault.
- The recursive
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, thennotModifiedwhen sent withifNoneMatch.blocks.list: 39 blocks, 0 diagnostics.- A stale
ifMatchis refused and the error names the current version. - An edit of the home's Newsletter title rendered by
vite devwithout 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 (needsDECO_SITE_TOKENand 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 (
withABTestingoverSITES_KV) is gone; v8 has no equivalent.
๐ค Generated with Claude Code