Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5020aea
feat(blocks): hosted drafts are overlays of changed blocks over local…
tlgimenes Oct 4, 2026
32650f6
fix(blocks): draft overlays honor grant expiry; drafts schedule relea…
tlgimenes Oct 4, 2026
7f1fe37
fix(cli): keep v7 form fidelity for literal order, @format datetime a…
tlgimenes Oct 4, 2026
966b65b
docs(skill): migrating an Eitri app
tlgimenes Oct 4, 2026
548ac19
docs(skill): lessons from a Next.js + VTEX storefront
tlgimenes Oct 5, 2026
f12417b
docs: use a placeholder site name in the deployment skill
tlgimenes Oct 5, 2026
b69e763
fix(blocks): no preview-server concept; who may preview is the app's …
tlgimenes Oct 5, 2026
0141f5a
feat(cli): @format on a type alias reaches every field of that type
tlgimenes Oct 5, 2026
3a98553
feat(skills): v7-to-v8 migration unwraps v7 async-rendering wrappers
tlgimenes Oct 5, 2026
63b0719
fix(blocks): leave legacy secret loader blocks out of the secret guard
tlgimenes Oct 3, 2026
9a45c8c
feat(cli): deco serve has no token
tlgimenes Oct 5, 2026
dddefd5
feat(blocks): one CMS settings block for preview hosts, telemetry and…
tlgimenes Oct 5, 2026
85bf711
fix(cli): deco serve link on a wildcard host, case-insensitive --host…
tlgimenes Oct 5, 2026
e7a1d1d
fix(blocks): per-call block maps, settings held across updates, stric…
tlgimenes Oct 5, 2026
963fd2b
fix(blocks): replace a stored CMS instance from an older package copy
tlgimenes Oct 5, 2026
46c0ea4
feat(cli): deco serve answers every origin
tlgimenes Oct 5, 2026
c606a45
feat(cli): deco serve shows and links localhost, listening on 127.0.0…
tlgimenes Oct 5, 2026
766b0ff
feat(protocol): schema.get answers "no schema yet" with schema: null
tlgimenes Oct 5, 2026
a875539
test(conformance): the docs now live in deco-sites/docs-tanstack
tlgimenes Oct 6, 2026
bbc8656
docs(skill): lessons from another TanStack + VTEX storefront
tlgimenes Oct 6, 2026
06a0a0b
docs(skill): lessons from a FastStore storefront
tlgimenes Oct 6, 2026
5f9dd07
feat(blocks)!: drafts are the fast-preview pointer, returning only th…
tlgimenes Oct 6, 2026
e07ae05
fix(blocks): refuse redirected draft responses and encode the pointer…
tlgimenes Oct 6, 2026
a8467dd
feat(blocks): schema declares "blocksMajor": 8; deco check requires it
tlgimenes Oct 6, 2026
773229c
fix(blocks)!: drop protocol options nobody uses; v7 draft cookie, hos…
tlgimenes Oct 6, 2026
2c378ad
fix(blocks): drop [::1] from the default draft-pointer hosts, as v7
tlgimenes Oct 7, 2026
73bc4f3
Merge branch 'v8-15-variant-preview' into v8-16-draft-overlays
tlgimenes Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions .agents/skills/deco-v7-to-v8-migration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Moves a v7 Deco site (@decocms/blocks 7.x with @decocms/tanstack or

# Deco v7 → v8 Migration

Moves a v7 site onto the next major in two parts: a script that does the mechanical, content-safe part in one pass, and a list of manual steps it prints. Proven on `deco-sites/storefront-tanstack` (Shopify, TanStack Start) and `deco-sites/blog-tanstack` (TanStack Start).
Moves a v7 site onto the next major in two parts: a script that does the mechanical, content-safe part in one pass, and a list of manual steps it prints. Proven on `deco-sites/storefront-tanstack` (Shopify, TanStack Start), `deco-sites/blog-tanstack` (TanStack Start), a Next.js App Router storefront on VTEX, a TanStack Start storefront on VTEX and a non-ejected FastStore storefront on VTEX (hundreds of saved blocks, private).

The spec is the docs page **Migrating from v7** (`/next/renames-and-migrations`). When this skill and the docs disagree, the docs win.

Expand All @@ -14,6 +14,10 @@ The spec is the docs page **Migrating from v7** (`/next/renames-and-migrations`)
- `package.json` depends on `@decocms/blocks` `^7`, and on `@decocms/tanstack` or `@decocms/nextjs`, `@decocms/blocks-admin`, `@decocms/blocks-cli` or v7 `@decocms/apps-*`.
- Saved content lives in `.deco/blocks/*.json`, or the site serves it at `/.decofile`.

**FastStore storefronts that aren't ejected** (FastStore owns the Next.js app; v7 monkey-patched its content requests) keep that seam with v8 behind it: follow `reference/faststore.md` for steps 2, 3 and 6.

**Native or mobile apps** whose v7 package only generated Studio's files (bundled JSON, native rendering) also apply. The script still runs, but most manual steps don't: follow `reference/native-apps.md`.

Not for sites still on `@decocms/[email protected]` (upgrade to 7.x first with `decocms-v6-to-v7-upgrade`) or on Fresh/Deno (migrate to TanStack first with `deco-to-tanstack-migration`; both live on the 7.x branch).

## Prerequisites
Expand Down Expand Up @@ -50,7 +54,8 @@ rm -rf .deco-migrate

What it does, in order (`scripts/migrate.ts`):

1. **content**: saved blocks into `.deco/blocks`, v7 generated files removed, the seven legacy names outside the alias table rewritten (`reference/legacy-names.md`), A/B tests keyed on a random matcher get its saved-block name as `experiment`;
1. **content**: saved blocks into `.deco/blocks`, v7 generated files removed, v7 async-rendering wrappers (`website/sections/Rendering/Lazy.tsx`, `SingleDeferred.tsx`, `Deferred.tsx`) unwrapped to the sections they held (v8 has no async rendering), the seven legacy names outside the alias table rewritten (`reference/legacy-names.md`), A/B tests keyed on a random matcher get its saved-block name as `experiment`;
**settings**: site settings folded into the `CMS` block (`.deco/blocks/CMS.json`, type `cms-settings`): the v7 Site block's (`Site`/`site`) `previewHosts` → `preview.hosts` (the field moves, ports kept, each entry trimmed and lowercased as v7 compared it; an entry that isn't a host is reported; on TanStack Start the `<site>.deco.site` and `<site>.deco-cx.workers.dev` hosts v7 always added for `DECO_SITE_NAME` are added too, or reported when the name isn't found), a literal `collectorAddress` on `OneDollarStats` → `analytics.collector`, and an earlier prerelease's `Telemetry.json`/`Analytics.json` → their sections, field for field and variants included (the old files are deleted). It keeps what `CMS.json` already has, writes nothing when there's nothing to fold, and a second run changes nothing. The report lists the environment-only settings to move by hand (`DECO_ALLOWED_PREVIEW_HOSTS`, `DECO_OTEL_*` sampling, `DECO_ANALYTICS_ENABLED`/`ONEDOLLAR_ENABLED`/`ONEDOLLAR_COLLECTOR`);
2. **secrets**: v7 secrets re-encrypted with `.deco/secrets.pub`;
3. **block map**: `.deco/index.ts` with each block under a short name plus an alias under its v7 name, after vendoring the app loaders and actions the content calls into `src/vendor`;
4. **imports**: the codemod over `src/` (`reference/import-map.md`);
Expand All @@ -64,12 +69,12 @@ The script prints **Done** and **Left to do**, grouped by step. Every "Left to d

## Manual steps (what the report leaves)

1. **Dependencies.** Depend on `@decocms/blocks@^8.1` and the v8 `@decocms/apps-<platform>` client. Remove `@decocms/tanstack`/`@decocms/nextjs`, `@decocms/blocks-admin`, `@decocms/blocks-cli`, `@decocms/apps-commerce`, `@decocms/apps-website` and `@decocms/apps-blog` once nothing imports them, and drop the v7 codegen from `build`.
1. **Dependencies.** Depend on `@decocms/blocks@^8.1` and the v8 `@decocms/apps-<platform>` client. Remove `@decocms/tanstack`/`@decocms/nextjs`, `@decocms/blocks-admin`, `@decocms/blocks-cli`, `@decocms/apps-commerce`, `@decocms/apps-website` and `@decocms/apps-blog` once nothing imports them, and drop the v7 codegen from `build`. On Next.js, add `@decocms/blocks` to `transpilePackages` in `next.config`: the `8.1.0-next.*` prereleases publish TypeScript source (`exports` → `src/*.ts`), v7's `withDeco` wrapper used to add it, and without it a clean install fails `next build` with `Module parse failed: Unexpected token`.
2. **Render pages with `createCMS`.** Follow the framework guide (`/next/tanstack-start-descriptors`, `/next/nextjs`): `createCMS` over the content, `matchRoute`, one promise per block, a view registry. Delete the v7 setup files, admin routes and `/deco/*` handlers.
3. **Move framework code into the site** (`reference/gotchas.md`): edge cache, image, SEO/head, device detection, cookies, cart/user/wishlist flows, commerce loaders and converters.
4. **Replace `/deco/invoke`** with server functions (TanStack `createServerFn`) or Next server actions/route handlers.
4. **Replace `/deco/invoke`** with server functions (TanStack `createServerFn`) or Next server actions/route handlers. One exported server function per loader or action the browser called, and each call site imports the one it calls: `invoke.site.loaders.spin(props)` becomes `$siteLoadersSpin({ data: props })`. Don't rebuild the `invoke.x.y` tree or a string-keyed dispatcher over the commerce-loader map (see DO NOT); the server function calls its handler directly. Details in `reference/gotchas.md` (`/deco/invoke` → server functions).
5. **Fix the content `deco check` rejects**: fields no type declares, `.tsx`-named preview blocks, v7 app blocks (apps are code now), `requestToParam` blocks in string fields.
6. **Telemetry and analytics**: the `telemetry` option of `createCMS` (`/next/telemetry`); the built-in analytics block with `AnalyticsScript`/`track` from `@decocms/blocks/analytics` (the report carries GTM/GA4 IDs over).
6. **Telemetry, analytics and previews**: the `telemetry` option of `createCMS` (`/next/telemetry`), with switches and rates in the `telemetry` section of `CMS.json`; `AnalyticsScript` with `const { analytics } = await cms.settings()` and `track` from `@decocms/blocks/analytics`; drafts through `await cms.draftPointer(request)` / `cms.draftCookie(request)`, which serve the release on hosts outside `preview.hosts` (list the dev host, `localhost`, too: v8 ignores `DECO_ALLOWED_PREVIEW_HOSTS`) (with no `previewHosts`, v7 allowed previews only on a TanStack site's `<site>.deco.site` and `<site>.deco-cx.workers.dev`, or nowhere; here every host may preview unless you list some, `/next/releases-and-drafts#allow-previews-per-host`). The report carries GTM/GA4 IDs over).

## Verify

Expand All @@ -78,11 +83,19 @@ bunx deco schema && bunx deco content && bunx deco check # 0 errors
bun run typecheck && bun run build
```

Then compare the migrated site with the v7 one page by page (a parity harness: SSR HTML, JSON-LD, analytics calls, cache headers, third-party requests). Encode every difference the product owner approves as an explicit rule, never a blanket ignore (`reference/parity.md`).
The committed `.deco/schema.gen.json` must carry the top-level `"blocksMajor": 8` that `deco schema` writes: Studio treats a site as v8 **only** when its schema has `blocksMajor === 8` (the file name doesn't count; anything else edits as v7), and `deco check` fails without it. A schema generated by a CLI that predates the field must be regenerated with one that writes it, then committed.

A v7 site often carries type errors of its own. Count them on the v7 commit first, and report the v8 count against it per file: name every remaining error in a file the migration touched instead of claiming there are none. Fix them with type-only changes, and rerun `deco schema` + `deco check` after each: a section's `Props` type *is* its editor form, so repairing a broken type import there (one that resolved to `any`) changes the schema and can make `deco check` reject saved content. Leave such an error, or fix the content with it, never silently. Don't fix an error by giving code that never ran (an undefined variable in a component) real values: that changes what renders.

Run it once more **without any local link** (a `decocms/blocks` checkout linked into `node_modules` ships compiled `dist/`, the npm package may not): a clean `install --frozen-lockfile && build` from the committed lockfile, the way CI and the deploy run it. Then `git diff .deco/schema.gen.json`: the build regenerates it, so commit what the committed dependency's CLI writes, or every clean build leaves the tree dirty. If it differs from the linked checkout's, check both against v7's forms; when the published one is the worse, list a follow-up to bump once the fix is released.

**Probe drafts by hand** on the dev server and on a production build: `?__draft=<pointer>` on an allowed host renders the draft and sets the cookie, the cookie alone keeps the next page in the draft, a host outside `preview.hosts` and a malformed pointer get the release, `?__draft=off` clears the cookie. A parity harness can't catch a dead draft path when v7's was dead too: both sides render the release.

Then compare the migrated site with the v7 one page by page (a parity harness: SSR HTML, JSON-LD, analytics calls, cache headers, third-party requests). Compare the editor forms as well (v7 `meta.gen.json` vs v8 `schema.gen.json`): many differences are stale v7 files or v7 heuristics v8 drops on purpose, a few are CLI bugs to fix. Encode every difference the product owner approves as an explicit rule, never a blanket ignore. Keep explained-but-unapproved ones as `pending`, and have a strict compare fail on them (`reference/parity.md`).

## DO NOT

- Add compat shims that re-create v7 APIs (`useCart` from a package, `invoke`, `RequestContext`, a fake `@decocms/tanstack`). What v7's packages did is site code now; copy it in and own it.
- Add compat shims that re-create v7 APIs (`useCart` from a package, `invoke`, `RequestContext`, a fake `@decocms/tanstack`). What v7's packages did is site code now; copy it in and own it. A site-local `invoke` object with v7's paths, or a `runInvoke("site/loaders/…")` dispatcher, is the same shim.
- Strip `"use client"` from components that need hooks, events or browser APIs to make them render.
- Rename saved type names. Content stores them; keep the v7 name as an alias (`/next/renames-and-migrations#rename-a-type-with-an-alias`).
- Hand-edit the script's output in the same commit as the run.
Expand All @@ -92,5 +105,7 @@ Then compare the migrated site with the v7 one page by page (a parity harness: S
- `reference/import-map.md`: every v7 import and its v8 replacement.
- `reference/legacy-names.md`: the alias table and the seven rewritten names.
- `reference/gotchas.md`: what the site migrations taught.
- `reference/parity.md`: approved v7 → v8 differences.
- `reference/parity.md`: how to approve differences, the pending state, editor-form causes, approved differences so far.
- `reference/native-apps.md`: apps that bundle content and render natively.
- `reference/faststore.md`: FastStore storefronts that aren't ejected (vendored runtime, content-client patch, drafts on Pages Router SSG).
- `scripts/`: the migration (`main.ts` entry). Its tests run with the repo's `bun run test`.
40 changes: 40 additions & 0 deletions .agents/skills/deco-v7-to-v8-migration/reference/faststore.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# FastStore storefronts that aren't ejected

Learned on a VTEX FastStore storefront (`@faststore/cli` 4.x, Next.js 16 Pages Router, private) whose v7 integration monkey-patched FastStore's content requests. FastStore owns the Next.js app (it generates it into `.faststore/` at build) and only compiles some of the store's folders, so there is no place for a framework guide's `createCMS` page. v8 keeps the same seam; only what sits behind it changes. The patches are site-owned glue: say so in the code and the README.

## The seam

- **Vendored runtime.** FastStore won't compile `@decocms/blocks` or `.deco/index.ts` from the store, so bundle them (esbuild, CommonJS) into a file under a folder FastStore copies (e.g. `src/server/vendor/deco-cms.cjs`), exporting `createCMS`, `parseDraftPointer` and the block map. Build it in `dev`, `test` and `build`, commit it, and build the committed copy from the **published** package: a bundle built from a linked checkout ships unpublished code. Say in the PR which version it was built from.
- **Content module.** Bundle what `deco content` writes (the blocks, revision and alias table) as JSON next to the runtime, and import it statically so the server build traces it. Don't read `.deco/` from disk at runtime: FastStore's deploy doesn't ship it.
- **Content client patch.** Patch the VTEX Content Platform client FastStore's content service calls (`@vtex/client-cp`) so it answers from `cms.forRelease()` / `cms.forDraft(pointer)`, in FastStore's `{ $componentKey, data, name }` section shape. Section blocks return a descriptor (`{ component, props }`) and FastStore renders the component.
- **Schema-derived defaults by block map, not by name.** A rule that reads a section's schema (a `default` that forces server rendering, say) must map `deco schema`'s `base64(<block name>)@Props` keys to sections through the block map's registrations. Parsing a v7 alias (`site/sections/X.tsx`) works only until the aliases go, and then every section silently changes behavior.

## Drafts on Pages Router SSG pages

FastStore's CMS pages are SSG and its CLI forbids `middleware`/`proxy` and `/api` routes, so the only point where a request can turn a static page into a per-request `getStaticProps` is Next's `tryGetPreviewData` (a non-`false` return is draft mode: no SSG cache, `Cache-Control: private, no-cache, no-store`, the value as `context.previewData`).

- **Patch the compiled runtimes, not `next/dist/server`.** Next 16 serves pages from `next/dist/compiled/next-server/pages.runtime.{dev,prod}.js`, and each inlines its own `tryGetPreviewData`. Replacing `next/dist/server/api-utils/node/try-get-preview-data` in `require.cache` reaches neither, so drafts never run, in dev or production. Add one statement after the function's per-request memo in both runtimes (webpack builds; FastStore builds with `--webpack`, so the turbo runtimes are unused) that calls a global hook and returns its answer unless it's `undefined`. A `patch-package` file of a minified runtime is hundreds of KB; an idempotent install/build script that fails when the anchor is missing is reviewable. Re-check it on every Next upgrade.
- **The hook.** Answer synchronously (Next calls it twice per request, from the route's `prepare` and from the renderer, so memoize on the request). Only a `?__draft=` or draft cookie that `parseDraftPointer` accepts is a signal: junk on a production host must not switch a page to per-request rendering. `cms.draftPointer`/`cms.draftCookie` are async, so return a token as preview data, let the content patch await the CMS's answer for it, and remember hosts the CMS refused so they go back to the SSG cache.
- **`x-nextjs-cache: HIT` is no evidence.** Next stamps it on draft-mode responses too. The draft ran when the response is `Cache-Control: private, no-cache, no-store, …` and shows the draft's content.
- **Password-protected preview domains.** FastStore protects `*.vtex.app` with a password gate that runs before the page. A store that patched it to let the Studio preview through must read the draft cookie (`__deco_draft`, the same name in v7 and v8), and accept only a value shaped like a pointer, never any `?__draft=`.
- **Prove drafts end to end; parity can't.** If the v7 draft path was dead (a `require.cache` patch is), the v7 baseline recorded the published page for the draft cases and a dead v8 path passes the compare. Probe the dev server and a production build (`next start`) by hand: `?__draft=<pointer>` on an allowed host shows the draft and sets the cookie; the cookie alone keeps the next page in the draft; a host outside `preview.hosts` and `?__draft=<junk>` get the published page; `?__draft=off` expires the cookie. Then let the draft cases differ from the baseline and list that for approval.

## Preview hosts and draft sources

- `preview.hosts` in `CMS.json` must list the dev host (`localhost`) as well as the store's hosts, within the code cap in `createCMS({ preview })`. v8 ignores v7's `DECO_ALLOWED_PREVIEW_HOSTS`: remove it from env files and harness env too.
- Drafts need no site loader: `cms.forDraft` fetches the pointer itself, only from v7's preview API domains (`*.decocms.com` and the loopback hosts by default; `DECO_PREVIEW_API_DOMAINS` replaces the list, as in v7). Delete any draft-fetching loader the site kept from v7.
- If the site keeps v7's policy that a draft that fails to load falls back to the release, write that down: the docs say a failed draft is an error.

## Content imported from FastStore's CMS

- Pages imported from FastStore's Headless CMS carry FastStore's settings (`__settings`) next to the SEO object the editor's form edits (`seo`). Merge the form's `seo` over the imported one, or an SEO edit never renders.
- Don't route on undeclared `__`-prefixed metadata (`__cms.contentType`). The docs say the site editor keeps only declared fields, and whether `_`-keys survive a save isn't documented. Encode what routing needs in a declared field (the template's synthetic `path`, say) or declare it.
- The store's build runs `deco check`, so an importer that writes content check rejects breaks the next deploy. Make the importer apply the same cleanup rules as the migration's content commit, and run `deco check` at its end.

## Saved-block names that differ only by letter case

A case-insensitive disk (macOS) checks out one of the two files, so v7 silently bundled one of them on macOS while Linux had both. Before migrating, find which one content references (grep every saved block for both names), compare the two, and keep the referenced one; record the decision in its own commit. Make the build fail on such a pair, reading the git index too (the disk can't show it). v8's write path refuses a new name that differs from an existing one only by case, but `deco content` and `deco check` don't flag a pair that is already there.

## Global sections with per-URL matchers

v7 resolved a store-wide "global sections" list per request, so a matcher in it could show a section on some URLs only. In v8 a matcher reads the request only through the site's own request-scoped storage (`/next/blocks#reading-the-request`), and FastStore fetches global sections apart from the page, for SSG pages at build time. An adapter that resolves the list once per revision drops per-URL matching. If content uses it, keep it (resolve the list per page path, passing the path to the matcher through the site's storage); if it doesn't, list the loss for the product owner anyway, since editors could rely on it.
Loading
Loading