diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a2f4827..e81e4e9 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "Claude skills for Dynamicweb 10 — organized by task domain, bundled by role.", - "version": "3.8.6" + "version": "3.9.0" }, "plugins": [ { @@ -87,6 +87,7 @@ "./skills/dw-demo-base", "./skills/dw-demo-pim", "./skills/dw-demo-swift", + "./skills/dw-demo-headless", "./skills/dw-demo-erp", "./skills/dw-integration-bc", "./skills/dw-integration-framework", diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f6b3bc..455739f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,39 @@ All notable changes to the Dynamicweb Skills plugin are recorded here. The `version` field in `.claude-plugin/marketplace.json` tracks these entries. +## [3.9.0] + +### Added +- **New skill `dw-demo-headless` — headless-commerce demos (Next.js storefront + DW10 Delivery + API).** A new `flow` demo skill in the presales cluster, sister to `dw-demo-swift` and + "Use AFTER `dw-demo-base`", covering the fifth demo consumer: a `vercel/commerce`-based Next.js + storefront driven by the Dynamicweb 10 Delivery API (`/dwapi/**`) over a dedicated, + presentation-agnostic serialized baseline that is its own product line — fully decoupled from + Swift, sharing no item-type rows. Three references: + - `references/headless-backend.md` — configure/verify the DW10 backend for headless: the Delivery + API surface map (endpoint/auth table; REST/JSON only, no GraphQL/OData), the **two-token trap** + (the admin/Management token 401s on `/dwapi`; the frontend needs a JWT from + `POST /dwapi/users/authenticate {UserName,Password}` → `{token}`), the locale gotcha (product + data lives under `ENU`, not `LANG1` — always pass `LanguageId`+`ShopId`), the PLP + repository/named-query requirement (`GET /dwapi/ecommerce/products/search?RepositoryName=…&QueryName=…`; + `POST /dwapi/ecommerce/products` 400s — count at `totalProductsCount`, facets at + `facetGroups[i].facets[j]`), and the item-type-XML-materialized-at-startup fact. + - `references/headless-frontend.md` — work with the Next.js storefront: the provider-module + data-layer swap (replace `lib/shopify/` with `lib/dynamicweb/`, keep every UI component), `DW_*` + env wiring, the self-signed-TLS dev bypass (`NODE_TLS_REJECT_UNAUTHORIZED=0`, dev-only), the + build-time RSC fetch caveat (`next build` needs a reachable provider), slug conventions (product + number = handle; group id = collection handle), and `@vercel/*` self-host disposition. + - `references/headless-baseline.md` — deserialize the headless baseline: the `Headless_*` + presentation-agnostic item-type layer, the `200000–209999` id floor band (an authoring + convention verified in YAML — DW reassigns DB ids), EN/NL sibling-area parity (paired manifest + entries), disk-overlay staging of `itemtypes/` + `repositories/` BEFORE host start, and the Full + index build after deserialize. + Registered the skill in `.claude-plugin/marketplace.json` (dynamicweb-presales plugin) and + regenerated `manifest.json`. Added the headless demo path to `dw-demo-base` SKILL.md (sister-skill + list + the baseline "explicit non-step") so it is discoverable from the foundation skill. Routes + to the existing `dw-headless-delivery` knowledge skill for the raw endpoint catalog rather than + duplicating it. + ## [3.8.6] ### Fixed diff --git a/manifest.json b/manifest.json index d249815..9c12574 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "generatedAt": "2026-07-01T22:46:05.923Z", + "generatedAt": "2026-07-03T07:52:43.546Z", "skills": [ { "name": "dw-demo-base", @@ -16,6 +16,13 @@ "description": "Dynamicweb 10 ERP integration -- owns the always-on rule that an ERP is a source AND target in DW10's Integration Framework, never a `ShopType=3` channel or an `EcomFeed`.", "path": "skills/dw-demo-erp/SKILL.md" }, + { + "name": "dw-demo-headless", + "type": "flow", + "group": "demo", + "description": "Dynamicweb 10 headless-commerce demos — a Next.js storefront (vercel/commerce starter) driven by the DW10 Delivery API, plus a dedicated presentation-agnostic serialized baseline.", + "path": "skills/dw-demo-headless/SKILL.md" + }, { "name": "dw-demo-pim", "type": "flow", diff --git a/skills/dw-demo-base/SKILL.md b/skills/dw-demo-base/SKILL.md index b8b64c6..59a6b1e 100644 --- a/skills/dw-demo-base/SKILL.md +++ b/skills/dw-demo-base/SKILL.md @@ -2,7 +2,7 @@ name: dw-demo-base type: flow group: demo -description: Foundation skill for Dynamicweb 10 demos — scaffolds the dw10-suite host, wires Backend MCP and the localhost TLS bypass, and drops the customisations and customer-context guardrails. Does NOT load a baseline. Use FIRST on any new Dynamicweb demo, when MCP tools fail to load ("Failed to connect", silent tools/list), on a fresh Windows machine, when auditing the customisation budget, or when the demo targets a hosted/cloud install reached only by URL + Admin API key (references/online-mode.md). Also owns the orchestrator abstraction (GSD primary vs the native `/demo:*` commands) — "drive the demo build", "run the demo natively", "GSD vs native" route to references/orchestrator.md — and the maintainer fold-back workflow — "fold this into the skill", "save this back to the plugin" route to references/iterate-plugin.md. Sister skills (dw-demo-pim, dw-demo-swift, dw-demo-erp, dw-integration-bc) are Use AFTER, never standalone. `\customer-context\` is read-only. +description: Foundation skill for Dynamicweb 10 demos — scaffolds the dw10-suite host, wires Backend MCP and the localhost TLS bypass, and drops the customisations and customer-context guardrails. Does NOT load a baseline. Use FIRST on any new Dynamicweb demo, when MCP tools fail to load ("Failed to connect", silent tools/list), on a fresh Windows machine, when auditing the customisation budget, or when the demo targets a hosted/cloud install reached only by URL + Admin API key (references/online-mode.md). Also owns the orchestrator abstraction (GSD primary vs the native `/demo:*` commands) — "drive the demo build", "run the demo natively", "GSD vs native" route to references/orchestrator.md — and the maintainer fold-back workflow — "fold this into the skill", "save this back to the plugin" route to references/iterate-plugin.md. Sister skills (dw-demo-pim, dw-demo-swift, dw-demo-headless, dw-demo-erp, dw-integration-bc) are Use AFTER, never standalone. `\customer-context\` is read-only. --- # Dynamicweb Demo Base Skill @@ -50,12 +50,13 @@ Walk every step in order — skip none. Each step's reference contains its own v ## Baseline data — explicit non-step -Loading reference content into the project DB is **NOT** part of this skill's canonical flow. Two separate paths follow base, depending on demo type: +Loading reference content into the project DB is **NOT** part of this skill's canonical flow. Three separate paths follow base, depending on demo type: - **PIM demo** -> start with a blank/fresh demo DB; the PIM skill's modelling recipes build content from scratch via MCP. No deserialize step. See [`dynamicweb-pim-demo/SKILL.md`](../dw-demo-pim/SKILL.md). -- **Swift demo** -> deserialize the Swift content baseline from `$env:DW_VAULT\serialized-data\\` via the Serializer. Owned end-to-end by [`dynamicweb-swift-demo/references/deserialize-flow.md`](../dw-demo-swift/references/deserialize-flow.md) + [`dynamicweb-swift-demo/references/integrity-sweep.md`](../dw-demo-swift/references/integrity-sweep.md). Prerequisite: the Serializer is installed per [`references/serializer-reference.md`](references/serializer-reference.md) "Installation". +- **Swift demo** -> deserialize the Swift content baseline via the Serializer. Owned end-to-end by [`dynamicweb-swift-demo/references/deserialize-flow.md`](../dw-demo-swift/references/deserialize-flow.md) + [`dynamicweb-swift-demo/references/integrity-sweep.md`](../dw-demo-swift/references/integrity-sweep.md). Prerequisite: the Serializer is installed per [`references/serializer-reference.md`](references/serializer-reference.md) "Installation". +- **Headless demo** -> deserialize the separate, presentation-agnostic `headless/2.3` baseline (its own product line, no shared item-type rows with Swift) for a Next.js storefront that reads the DW10 Delivery API. Owned by [`dynamicweb-headless-demo/references/headless-baseline.md`](../dw-demo-headless/references/headless-baseline.md); backend config in [`headless-backend.md`](../dw-demo-headless/references/headless-backend.md). Same Serializer prerequisite. -The Serializer install steps live in base so any sister skill can pull them; the act of deserializing is Swift-specific. +The Serializer install steps live in base so any sister skill can pull them; the act of deserializing is Swift- or headless-specific. ## Where to find things diff --git a/skills/dw-demo-headless/SKILL.md b/skills/dw-demo-headless/SKILL.md new file mode 100644 index 0000000..6fbbf13 --- /dev/null +++ b/skills/dw-demo-headless/SKILL.md @@ -0,0 +1,106 @@ +--- +name: dw-demo-headless +type: flow +group: demo +description: Dynamicweb 10 headless-commerce demos — a Next.js storefront (vercel/commerce starter) driven by the DW10 Delivery API, plus a dedicated presentation-agnostic serialized baseline. Triggers: building a headless/decoupled storefront demo, wiring a Next.js/vercel-commerce frontend to a DW10 backend, "the /dwapi call returns 401 with my admin token", product search returns 400 on POST /dwapi/ecommerce/products, PLP needs a repository+query, product data missing under a language, deserializing a headless baseline, Headless_* item types, running/building the storefront against a self-signed dev host. Non-triggers: server-side Swift storefront -> dw-demo-swift; PIM data modelling -> dw-demo-pim; raw Delivery API endpoint catalog (non-demo) -> dw-headless-delivery; demo setup/MCP/TLS -> dw-demo-base. Use AFTER dw-demo-base (host running, Serializer installed). Headless is its own baseline product line, fully decoupled from Swift. +--- + +# Dynamicweb Headless Demo Skill + +Build a **headless-commerce demo**: a Next.js storefront (from the `vercel/commerce` +starter) that reads a Dynamicweb 10 backend through the **Delivery API** (`/dwapi/**`), backed +by a **dedicated, presentation-agnostic serialized baseline** that is its own product line — +fully decoupled from the Swift baseline. **Use AFTER** `dynamicweb-demo-base` — assumes a host +is running, TLS bypass is wired, and the Serializer is installed in the host (per base's +[`../dw-demo-base/references/serializer-reference.md`](../dw-demo-base/references/serializer-reference.md) +"Installation"). + +This SKILL.md is pure nav. Headless is a knowledge book, not a recipe — see references for any +specific topic. + +## How to run me + +This skill holds domain knowledge, not build sequencing. An **orchestrator** owns the phase +order: GSD injects this skill into its agents (via the `agent_skills` block), or the native +`/demo:*` command set invokes it; **standalone**, the skill's own lightweight harness guards its +documented order (gate every step, persist progress to `.demo//flow-state.json`). The +orchestrator abstraction (GSD primary, native command set, and the standalone harness) is owned by +[`../dw-demo-base/references/orchestrator.md`](../dw-demo-base/references/orchestrator.md). + +## Headless vs Swift — pick the right demo flow + +A headless demo and a Swift demo are **separate product lines**, not two skins of one baseline: + +- **Swift demo** (`dynamicweb-swift-demo`) — DW renders the storefront server-side with Razor; + content is presentation-coupled (`Swift-v2_*` paragraph item types carry template/layout/ + colorscheme/icon fields). Loads the `swift-2.3` baseline. +- **Headless demo** (this skill) — a **separate** Next.js app renders the storefront; DW is a pure + JSON backend behind `/dwapi/**`. Content is presentation-agnostic (`Headless_*` item types carry + no layout fields). Loads a **separate** `headless/2.3` baseline that shares **no** item-type rows + with Swift. + +Do not port Swift paragraph item types into a headless demo — that reintroduces the presentation +coupling headless exists to avoid. Reuse the commerce/PIM **domain** model (products, groups, +variants, prices, orders, users, facets — all delivered through the Delivery API, never as item +types) and field-type conventions only. + +## Step 0 — Stand up the headless demo (every headless demo) + +Walk these in order; each reference owns its own verification gate. + +1. **Configure and verify the DW10 backend for headless** -> [`references/headless-backend.md`](references/headless-backend.md) + Confirm the Delivery API (`/dwapi/**`) is reachable, learn the endpoint/auth surface map, and + clear the **two-token trap** (the admin/Management token 401s on `/dwapi`; the frontend needs a + JWT from `POST /dwapi/users/authenticate`). Owns the locale gotcha (product data lives under + `ENU`, not `LANG1` — always pass `LanguageId`+`ShopId`) and the PLP repository/named-query + requirement (search is `GET /dwapi/ecommerce/products/search?RepositoryName=…&QueryName=…`; + `POST /dwapi/ecommerce/products` 400s). + +2. **Deserialize the headless baseline** -> [`references/headless-baseline.md`](references/headless-baseline.md) + Stage the disk-overlay `itemtypes/` and `repositories/` **before** host start (DW materializes + item types and repositories at startup), deserialize the `deploy/` then `seed/` mode trees, and + run a **Full** index build afterwards. Owns the `Headless_*` item-type layer, the id floor band, + and EN/NL sibling-area parity. + +3. **Wire and run the Next.js storefront** -> [`references/headless-frontend.md`](references/headless-frontend.md) + Swap the starter's default provider for a DynamicWeb data-layer module, wire the `DW_*` env vars, + apply the self-signed-TLS dev bypass, and run/build it against the backend. Owns the slug + conventions (product number = handle; group id = collection handle) and the build-time RSC fetch + caveat (the starter cannot `next build` without a reachable provider). + +## Where to find things + +| If you need to... | Read this reference | +|---|---| +| **Configure/verify the DW10 backend for headless** — Delivery API surface map + auth model, the two-token trap, the ENU/ShopId locale gotcha, the PLP repository+query requirement, item-type XML materialization | **references/headless-backend.md** | +| **Deserialize the headless baseline** — `Headless_*` item-type layer, id floor band, EN/NL parity, disk-overlay staging of `itemtypes/`+`repositories/` before host start, Full index build after deserialize | **references/headless-baseline.md** | +| **Work with the Next.js storefront** — provider-module/data-layer swap, `DW_*` env wiring, self-signed-TLS dev bypass, build-time RSC fetch caveat, slug conventions | **references/headless-frontend.md** | +| **The raw Delivery API endpoint catalog** (non-demo reference — content/commerce/users/forms/query families, request/response shapes) | [`../dw-headless-delivery/SKILL.md`](../dw-headless-delivery/SKILL.md) | +| **Build a per-demo product search index** by hand (Lucene `ProductIndexBuilder`, query + facets) | [`../dw-search-indexing/SKILL.md`](../dw-search-indexing/SKILL.md) | + +## Inherited from dynamicweb-demo-base + +This skill assumes `dynamicweb-demo-base` ran first. Its always-on rules are NOT restated here — +see the owning reference in base for each: + +| Rule | Owner | +|------|-------| +| Customer-context read-only contract | [dynamicweb-demo-base/references/customer-context.md](../dw-demo-base/references/customer-context.md) | +| Customisations-ledger preflight | [dynamicweb-demo-base/references/customisations.md](../dw-demo-base/references/customisations.md) | +| Baseline-drift self-diagnosis rule | [dynamicweb-demo-base/SKILL.md "Self-diagnosis rule"](../dw-demo-base/SKILL.md) | + +If you find yourself running this skill standalone with no base context, fix that before +continuing — see the description's "Use AFTER" hint. If `dynamicweb-demo-base` is not installed, +install it first — this skill's inherited rules require it. + +## Sister skills + +- **`dynamicweb-demo-base`** — foundation skill (Use FIRST). Owns all setup + Serializer install + + customisations + customer-context. Does NOT deserialize a baseline. +- **`dynamicweb-swift-demo`** — the Swift (server-side Razor) storefront line. A headless demo is + the decoupled peer of a Swift demo, not a variant of it; the two baselines never share item-type + rows. +- **`dynamicweb-pim-demo`** — PIM modelling from a blank DB. The headless baseline reuses the same + commerce/PIM domain model PIM builds; pair them when a headless demo needs richer catalog data. +- **`dynamicweb-headless-delivery`** — the raw `/dwapi/` knowledge skill (endpoint catalog, auth, + architecture rules). This demo skill routes to it for endpoint detail rather than duplicating it. diff --git a/skills/dw-demo-headless/references/headless-backend.md b/skills/dw-demo-headless/references/headless-backend.md new file mode 100644 index 0000000..95dffbe --- /dev/null +++ b/skills/dw-demo-headless/references/headless-backend.md @@ -0,0 +1,175 @@ +# headless-backend.md + +## Contents + +- [1. What "configure the backend for headless" means](#1-what-configure-the-backend-for-headless-means) +- [2. Delivery API surface map](#2-delivery-api-surface-map) +- [3. The two-token trap (admin token vs frontend JWT)](#3-the-two-token-trap-admin-token-vs-frontend-jwt) +- [4. The locale gotcha (ENU, not LANG1)](#4-the-locale-gotcha-enu-not-lang1) +- [5. Product search needs a repository + named query](#5-product-search-needs-a-repository--named-query) +- [6. Item types materialize from XML at host startup](#6-item-types-materialize-from-xml-at-host-startup) +- [7. Verification gate](#7-verification-gate) + +> Configure and verify a Dynamicweb 10 host so a headless storefront can read it. DW10 ships a +> single first-class headless surface — the **Delivery API** (`/dwapi/**`), a REST/JSON API. There +> is **no GraphQL and no OData** (`/graphql`, `/odata`, `/api`, bare `/dwapi/content` all 404); the +> Delivery API is the only contract. A live OpenAPI 3.0 document sits at `https:///dwapi/api.json` +> (Swagger UI at `/dwapi/docs/`). This reference is demo-facing backend config; for the full raw +> endpoint catalog and response shapes see [`../../dw-headless-delivery/SKILL.md`](../../dw-headless-delivery/SKILL.md). +> +> **Use AFTER `dynamicweb-demo-base`.** Assumes a running host and the localhost TLS bypass wired. + +## 1. What "configure the backend for headless" means + +For a headless demo the DW host is a pure JSON backend — there is no server-side storefront to +theme. "Configuring it" is three things, each verified before the frontend is wired: + +1. The Delivery API is reachable and you know its **auth model** (§2–§3). +2. The **locale/shop context** the storefront will pass is the one the catalog data actually lives + under (§4). +3. A **product-search surface** (repository + named query) exists so the PLP/faceted-navigation + path resolves (§5). + +Everything the storefront reads flows through `/dwapi/**`; nothing requires custom C# on the host. + +## 2. Delivery API surface map + +The endpoints a storefront consumes, and whether each needs a token. **Catalog, groups, +navigation, content, areas, and cart are anonymous** — the storefront reads the full catalog with +no credential. Only user-scoped endpoints (orders, profile, favorites, loyalty, impersonation) +require a Bearer JWT. + +| Concern | Method + path | Auth | +|---|---|---| +| Product detail | `GET /dwapi/ecommerce/products/{id}` (+ `/{id}/{variantId}`) | anonymous | +| Product list / facets | `GET /dwapi/ecommerce/products/search` (see §5) | anonymous | +| Product list (index-free) | `GET /dwapi/ecommerce/products/search?ProductIds=…` | anonymous | +| Variants | `GET /dwapi/ecommerce/variants/{productId}` | anonymous | +| Related / BOM | `GET /dwapi/ecommerce/products/{id}/related` · `/{id}/bom` | anonymous | +| Groups (collections) | `GET /dwapi/ecommerce/groups` · `/groups/{groupId}` | anonymous | +| Navigation / menu | `GET /dwapi/frontend/navigations/{areaId}` | anonymous | +| Content pages | `GET /dwapi/content/pages/{id}` · `/pages/url` | anonymous | +| Page rows / paragraphs | `GET /dwapi/content/rows/{pageId}/{device}` · `/content/paragraphs` | anonymous | +| Areas (site/domain map) | `GET /dwapi/content/areas` · `/areas/{id}` · `/areas/domain/{domain}` | anonymous | +| Cart | `POST /dwapi/ecommerce/carts/create` · `GET/PATCH/DELETE /carts/{secret}` · `/{secret}/items` · `/{secret}/createOrder` · `/{secret}/checkout` | cart secret (anon ok) | +| Countries / currencies | `GET /dwapi/ecommerce/International/countries` · `/currencies` | anonymous | +| Orders | `GET /dwapi/ecommerce/orders` · `/orders/{secret}` · `/orders/search` | **Bearer JWT** | +| Addresses / profile | `GET/PATCH /dwapi/users/addresses…` · `/users/info…` | **Bearer JWT** | +| Favorites (wishlist) | `GET/POST /dwapi/ecommerce/favorites/lists…` | Bearer JWT | +| Loyalty points | `GET /dwapi/ecommerce/loyaltyPoints/balance` · `/transactions` | Bearer JWT | +| CSR impersonation (B2B) | `GET /dwapi/users/impersonatees` · `/users/impersonate` | Bearer JWT | +| User auth | `POST /dwapi/users/token` · `POST/GET /dwapi/users/authenticate` · `/authenticate/refresh` | credentials → JWT | +| Password flows | `POST /dwapi/users/password/{change,reset,recover…}` | mixed | + +**Cart** uses an opaque per-cart `secret` returned by `carts/create`; a cart can start anonymous +and later bind to the authenticated user. **B2B price/permission gating is server-side** — a +user-scoped price/permission is applied when the JWT (or `PriceSettings.UserId`) is supplied. + +> Two content-list routes are **not** routes on this platform version: `GET /dwapi/content/pages` +> (no id) returns 404 — read a page by id (`/pages/{id}` or `/pages?pageId={id}`) or by URL +> (`/pages/url`). Do not build the provider around a bare page-list call. + +## 3. The two-token trap (admin token vs frontend JWT) + +The single most common wiring failure. **There are two different tokens and they are not +interchangeable:** + +- The **admin / Management-API token** (minted at `/Admin/TokenAuthentication/authenticate`, + format `CLAUDE.` when captured for MCP/Serializer work) authenticates the admin/index/ + Serializer APIs (`/admin/api/**`). **It 401s on `/dwapi/**`.** Passing the admin bearer to + `GET /dwapi/ecommerce/products` returns `401`, while anonymous returns `200`. Confirmed live. +- The **frontend JWT** authenticates the `/dwapi` user scope. Mint it from **frontend user + credentials**: + +```http +POST /dwapi/users/authenticate +Content-Type: application/json + +{ "UserName": "", "Password": "" } +``` + +Response: `{ "token": "" }`. Send it as `Authorization: Bearer `. (`POST /dwapi/users/token` +is the equivalent path; it also exchanges an existing `Dynamicweb.Extranet` cookie for a JWT.) + +**Symptom → cause:** anonymous catalog reads work but every user-scoped call 401s → you are sending +the admin token (or no token) instead of a frontend JWT. Mint the JWT via `/dwapi/users/authenticate` +first. Never write either token to disk; keep it in conversation/session state only. + +For SSR/build-time server fetches where no user is involved, `POST /dwapi/serviceauth/token` +`{ "apiKey": "…" }` issues a short-lived service JWT — keep the API key server-side, never expose it +to the browser. + +## 4. The locale gotcha (ENU, not LANG1) + +Product data does **not** live under the language you may expect. On the reference host the catalog +sits under language **`ENU`** and shop **`SHOP1`** — passing `LANG1` returns an empty/short product +set even though the products exist. + +**Rule: the storefront must establish an explicit locale + shop context on every Delivery API +call** — pass `LanguageId=ENU` and `ShopId=SHOP1` (the canonical storefront defaults) rather than +relying on a host default. The product-search query scopes by the runtime +`Dynamicweb.Ecommerce.Context:LanguageID` / `:ShopID`, so an unset context yields the wrong (or no) +data. + +Areas carry the shop/language/currency bindings (`AreaEcomShopId` / `AreaEcomLanguageId` / +`AreaEcomCurrencyId`). Those binding columns are **per-environment** and are excluded from +serialization — set them at provisioning, do not expect them to arrive in a baseline. An EN area and +its NL sibling each bind to the same shop and their own language. + +## 5. Product search needs a repository + named query + +The PLP, faceted navigation, and text search all go through the search endpoint, which requires a +**`RepositoryName` + `QueryName`** pair pointing at a provisioned repository index + named query: + +``` +GET /dwapi/ecommerce/products/search?RepositoryName=&QueryName=&LanguageId=ENU&ShopId=SHOP1&PageSize=N +``` + +Returns `200` with: +`{ "products":[…], "pageSize", "pageCount", "currentPage", "totalProductsCount", "sortOrder", "facetGroups" }`. + +- **Product count JSON path: `totalProductsCount`** (not `products.length`, which is one page). +- **Facets JSON path: `facetGroups[i].facets[j]`.** Each facet carries + `{ name, queryParameter, facetField, facetType, facetValue, options, optionCount, … }`. A facet + group with `optionCount=0` is defined but unpopulated (e.g. a Manufacturer facet when the data set + has no manufacturer rows) — present, but nothing to click. +- **Use GET, not POST.** The sibling `POST /dwapi/ecommerce/products` returns **400 for every probed + body shape** on this platform version — its request model is unresolved. The `GET + /products/search` endpoint is the query-resolution proof; build the provider on it. +- A stock/harness `Products` repository typically ships an index with **no resolvable query** (probes + → 400/404). A headless demo ships its **own** complete search surface (index + query + facets) — + see [`headless-baseline.md`](headless-baseline.md) §"Product search surface". + +**Index-free fallback:** `GET /dwapi/ecommerce/products/search?ProductIds=…` (and `GroupId=…`) +returns products without any repository query — use it for PDP-by-id and simple collection lists +before the faceted query is provisioned, behind the same provider function so no component changes +when the query lands. + +## 6. Item types materialize from XML at host startup + +A durable platform fact that shapes how a headless baseline is staged: **DW10 materializes item +types from `wwwroot\Files\System\Items\ItemType_.xml` at host startup** — the +ItemManager creates/updates the backing `ItemType_` SQL table from that XML. **DW10 does +not consume standalone item-type JSON files.** If a baseline ships item-type definitions as JSON, +they must be rendered to the DW item XML shape and staged into `Files/System/Items` **before the host +starts** (a disk-overlay step, zero custom code). Content that references an item type whose XML is +not on disk fails to deserialize. See [`headless-baseline.md`](headless-baseline.md) §"Item-type +staging". + +## 7. Verification gate + +Before wiring the frontend, confirm all of these against the running host (anonymous unless noted): + +- `GET https:///dwapi/api.json` → 200 (Delivery API is up; note the operation count). +- `GET /dwapi/ecommerce/products/{knownId}` → 200 with a full product model. +- `GET /dwapi/ecommerce/groups` → 200 with the collection list. +- `GET /dwapi/content/areas` → 200; confirm the storefront's area and its shop/language binding. +- Admin token on `/dwapi/ecommerce/products` → **401** (proves the two-token trap is understood), + anonymous on the same → **200**. +- `POST /dwapi/users/authenticate` with a frontend user → `{ "token": … }`; that JWT on a user-scoped + endpoint (e.g. `/dwapi/ecommerce/orders`) → 200. +- `GET /dwapi/ecommerce/products/search?RepositoryName=…&QueryName=…&LanguageId=ENU&ShopId=SHOP1` → + 200 with non-zero `totalProductsCount`. + +If any fails, resolve it here before touching the storefront — a provider wired against an unverified +backend fails in ways that look like frontend bugs. diff --git a/skills/dw-demo-headless/references/headless-baseline.md b/skills/dw-demo-headless/references/headless-baseline.md new file mode 100644 index 0000000..61fecc8 --- /dev/null +++ b/skills/dw-demo-headless/references/headless-baseline.md @@ -0,0 +1,201 @@ +# headless-baseline.md + +## Contents + +- [1. A headless baseline is its own product line](#1-a-headless-baseline-is-its-own-product-line) +- [2. Package shape (deploy/seed, not a flat content folder)](#2-package-shape-deployseed-not-a-flat-content-folder) +- [3. The `Headless_*` item-type layer](#3-the-headless_-item-type-layer) +- [4. Item-instance id floor band](#4-item-instance-id-floor-band) +- [5. EN/NL sibling-area parity](#5-ennl-sibling-area-parity) +- [6. Disk-overlay staging BEFORE host start](#6-disk-overlay-staging-before-host-start) +- [7. Product search surface (repository + query + facets)](#7-product-search-surface-repository--query--facets) +- [8. Deserialize order + Full index build](#8-deserialize-order--full-index-build) +- [9. Verification gate](#9-verification-gate) + +> Deserialize the **headless baseline** into a demo host. The headless baseline is a **separate +> serialized product line** from Swift — it carries exactly the content a headless frontend needs +> (catalog navigation/menus, content pages, B2B/customer-center structures) as **zero-custom-code +> YAML**, in a presentation-agnostic `Headless_*` item-type layer that shares **no** item-type rows +> with Swift. Consumer: the Next.js storefront via the Delivery API ([`headless-frontend.md`](headless-frontend.md)). +> +> **Use AFTER `dynamicweb-demo-base`** (Serializer installed) and after the backend is understood +> ([`headless-backend.md`](headless-backend.md)). + +## 1. A headless baseline is its own product line + +Because a headless frontend and a Swift frontend consume content completely differently, the +headless baseline is packaged as a **distinct product line** (`baseline/headless/2.3`), not a +variant of `swift/2.3`. It has an independent lifecycle and its own gate pass, and it shares no +item-type rows with Swift. The point is independent evolution: Swift item types roll with Swift's +Razor building blocks; `Headless_*` items roll with the Next.js component contract. + +**Reuse the domain model, not the item types.** Commerce/PIM data (products, groups, variants, +prices, orders, users, facets) is delivered through the Delivery API product/order endpoints and +captured as `Ecom*` SQL rows — never as item types. Only field-type *conventions* carry over. Do +not port Swift paragraph item types: they are presentation-coupled (template/CSS/layout/colorscheme/ +icon fields bound to Razor), and lifting them reintroduces exactly the coupling headless exists to +avoid. + +## 2. Package shape (deploy/seed, not a flat content folder) + +The serializer keys its merge behaviour off the **mode**, so a headless baseline splits content into +`deploy/` (source-wins) and `seed/` (field-level merge), each with its own `schemaVersion: 2` +manifest, `_content/` (page trees), and `_sql/` (commerce rows). The disk-overlay surfaces +(`itemtypes/`, `repositories/`) sit alongside: + +``` +baseline/headless/2.3/ +├─ BASELINE.md +├─ config/headless-2.3.json # serializer predicate list (Content + SqlTable) +├─ itemtypes/Headless_*.json # D-agnostic item-type defs (disk-overlay, zero code) +├─ repositories/Headless/ # product search surface (disk-overlay, zero code) +│ ├─ Products.index · Products.query · Products.facets +├─ deploy/ # source-wins framework (nav, customer-center, area chrome) +│ ├─ deploy-manifest.json +│ ├─ _content/Headless/ … # EN +│ ├─ _content/Headless Nederlands/ … # NL parity +│ └─ _sql/ # commerce reference tables (serialize-captured) +└─ seed/ # customer-owned bootstrap (home, about, catalog landing) + ├─ seed-manifest.json + ├─ _content/Headless/ … + _content/Headless Nederlands/ … + └─ _sql/ # catalog/prices (serialize-captured) +``` + +`_sql/` row files are **not hand-authored** — a serialize pass captures them from the live host, the +same way the Swift baseline does. A demo can run headless as an **additional leg on top of the +swift/2.3 catalog** in the same DB (shared-catalog): the headless deploy/seed manifests list only +`Content` entries, so the `Ecom*` catalog comes from the Swift leg and the headless deserialize never +touches it. That is the low-friction path until the headless package captures its own `_sql`. + +## 3. The `Headless_*` item-type layer + +New, **presentation-agnostic** item types namespaced `Headless_*`, so they never collide with Swift's +`ItemType_` rows (the same namespacing discipline packs use for their ids). None carry +layout/template/colorscheme/icon fields. + +| Item type | Category | Purpose | +|---|---|---| +| `Headless_Master` | site | Area master: site name, locale, menu refs, contact. No layout fields. | +| `Headless_PageProperties` | page | Minimal structural page-property item. No Icon/SubmenuType. | +| `Headless_Page` | page | Generic semantic content node (title/slug/summary/SEO). | +| `Headless_ContentPage` | page | Rich content page; body is portable markdown/structured text. | +| `Headless_Menu` | navigation | Menu container keyed by `MenuKey` (header/footer/catalog) → normalized `Menu[]`. | +| `Headless_MenuItem` | navigation | One nav entry → content path or commerce group/product id. No template selection. | +| `Headless_CustomerCenter` | b2b | Customer-center root: auth requirement + permitted access-user groups. | +| `Headless_AccountSection` | b2b | One account section mapped to a Delivery API endpoint + role (orders/reorder/users/addresses). | +| `Headless_SpecSheet` | commerce-content | Presentation-free product spec sheet (JSON spec rows). | +| `Headless_DownloadableAsset` | commerce-content | Presentation-free datasheet/manual metadata. | + +The two candidates that are genuinely presentation-free (spec-sheet, downloadable-asset) are defined +**fresh** in this namespace, not lifted from Swift rows. + +## 4. Item-instance id floor band + +Item-instance ids are **global per itemType across packs and baselines** because `(itemType, +fields.Id)` pairs land as PK rows in the shared `ItemType_` tables. Feature packs occupy +the `100000+` band. **The headless baseline reserves `200000–209999`** for all headless item +instances — a distinct band, well clear of the packs. + +- EN instances use even offsets, NL instances use the next (odd) offset, so EN and NL never collide + within the same `ItemType_Headless_*` table (both language layers write the same tables). +- New headless content takes the next free id in `200000+`. + +**Live nuance — the band is an authoring convention, not a DB column.** DW's content deserializer +**reassigns** content-item instance ids on landing: the DB `Id` column is *not* the authored +`fields.Id` (a fresh content-page row can land as `Id=1`, not `200100`). Because `Headless_*` tables +are brand-new tables only this baseline writes, there is zero collision risk regardless of the +reassigned DB id. Verify the band **statically in the YAML** (every authored `"Id": ""` sits in +`200000–209999`), and verify **landing dynamically** (rows appear across `ItemType_Headless_*` with no +`Swift-v2_*` table touched) — do not assert the band against the DB `Id` column. + +## 5. EN/NL sibling-area parity + +The baseline ships an English area (`Headless`) and a Dutch layer (`Headless Nederlands`), mirroring +the Swift precedent of paired language areas. Two facts to respect: + +- **The NL layer needs its own `Content` manifest entry.** A single manifest entry creates only EN; + the deploy/seed manifests must carry **paired** EN (`Headless`) + NL (`Headless Nederlands`) + entries or NL never lands. +- **NL is authored as a sibling area** (`AreaMasterAreaId: 0`) for deterministic, environment- + independent parity. Wiring it as a *true DW language layer* (a host-assigned master id, like the + Swift NL area pointing at the EN area's numeric id) is a separate, still-open step — the sibling-area + form is what deserializes cleanly today. + +Parity is asserted as equal authored page counts per area (EN == NL). + +## 6. Disk-overlay staging BEFORE host start + +Two surfaces must be on disk **before the host starts**, because DW builds them at startup — staging +them after start is a no-op until the next restart. Both are gate tooling / config (zero custom code, +the disk-overlay precedent), never serialized DB content: + +1. **Item types.** DW10 materializes item types from `wwwroot\Files\System\Items\ItemType_.xml` + at startup and **ignores standalone JSON item-type files**. The baseline ships human-authored JSON + under `itemtypes/`; a converter renders each JSON def to the exact DW item-type XML shape (editor + map: Text→TextEditor, LongText→LongTextEditor, Checkbox→CheckboxEditor, Integer→IntegerEditor, + List→DropDownListEditor + Static options, Link→LinkEditor) and writes + `ItemType_.xml` into `Files/System/Items`. Write the XML **UTF-8 with BOM** — DW reads + these files by BOM, not by the declared encoding attribute. Content that references a `Headless_*` + type whose XML is not staged fails to deserialize. + +2. **Repositories.** Copy `repositories/Headless/*` (`.index` / `.query` / `.facets`) into + `wwwroot\Files\System\Repositories\Headless\` before start (a file-sentinel idempotent copy). See + §7. + +## 7. Product search surface (repository + query + facets) + +The PLP/faceted path needs a `RepositoryName` + `QueryName` pair the stock/harness repository does not +supply (its index has no resolvable query). The headless baseline ships its own complete surface under +`repositories/Headless/`: + +- **`Products.index`** — a Lucene `ProductIndexBuilder` (single `Products` instance). Use **only + fields with backing rows in the target DB**: name/number search fields, a manufacturer facet field, + a price-bucket grouping, a freetext copy-field, a sort field. **Do not** add + `ProductCategory|*` custom-field sources unless the products actually have them — an index that + references a field with no backing row fails the Full build ("field not found in products"). +- **`Products.query`** — the named query (`QueryName=Products`). Runtime locale/shop scoping via the + `Dynamicweb.Ecommerce.Context:LanguageID` / `:ShopID` macros; parameters: `q`/`eq` (text search), + `GroupID` (collection PLP), `sku`, plus facet params. Paging/sort are Delivery API runtime + parameters (`PageSize`/`PageIndex`/`SortBy`/`SortOrder`). +- **`Products.facets`** — the facet groups (e.g. Manufacturer, Group, Price buckets). A facet group + with no backing data lands with `optionCount=0` (present but unpopulated) — that is data-shape, not a + wiring error. + +The provider consumes this via `GET /dwapi/ecommerce/products/search?RepositoryName=Headless&QueryName=Products&…` +(see [`headless-backend.md`](headless-backend.md) §5). + +## 8. Deserialize order + Full index build + +1. **Stage disk overlays before start** (§6) — item-type XML and the repository files. +2. **Start the host.** +3. **Deserialize `deploy/` then `seed/`** — POST `/Admin/Api/SerializerDeserialize` per mode, strict + mode on. If running shared-catalog, deserialize the Swift baseline's deploy+seed first so the + `Ecom*` catalog exists; the headless leg then lands its `Content` entries on top. +4. **Full index build** — after products exist in the DB, trigger a **Full** build of the `Headless` + repository's `Products` index and poll the index/instance status paths until it completes within + the verify timeout. The build must run **after** deserialize (the index is empty until the catalog + rows land); a stale/empty index makes the search endpoint return zero hits even though the query + resolves. + +## 9. Verification gate + +Clean-room, shared-catalog, on the supported Swift version: + +- **Deserialize** — `deploy/` + `seed/` POST return HTTP 200 with zero strict-mode escalations + (requires the `Headless_*` item-type XML staged pre-start). +- **Areas** — both `Headless` and `Headless Nederlands` area rows exist. +- **Item rows** — rows land across `ItemType_Headless_*` tables with **no `Swift-v2_*` table touched**; + authored YAML ids all sit in `200000–209999` (static scan — DW reassigns DB ids, §4). +- **Parity** — authored page count per area is equal (EN == NL) and ≥ 1. +- **Delivery-API read** — `GET /dwapi/frontend/navigations/{areaId}` and `GET /dwapi/content/pages/{id}` + return the nav + content pages with `Headless_*` fields. +- **Search** — `GET /dwapi/ecommerce/products/search?RepositoryName=Headless&QueryName=Products&LanguageId=ENU&ShopId=SHOP1` + returns 200 with non-zero `totalProductsCount`; a `q=` request returns ≥ 1 hit. +- **Facets** — the response carries the facet groups under `facetGroups[].facets[]` with ≥ 1 + populated option; a faceted request (e.g. `GroupID=GROUP1`) returns a strict subset. + +**Two things are NOT provable in the harness gate — verify them on a real host:** (1) storefront HTML +render (the clean-room does not provision the server-side product-render index); (2) +permission-gated customer-center access (customer-group → page-permission grants do not materialize +in-gate). In-gate, assert only that the pages/rows deserialize and the permission fields persist, not +that access is enforced. diff --git a/skills/dw-demo-headless/references/headless-frontend.md b/skills/dw-demo-headless/references/headless-frontend.md new file mode 100644 index 0000000..bafb4b8 --- /dev/null +++ b/skills/dw-demo-headless/references/headless-frontend.md @@ -0,0 +1,162 @@ +# headless-frontend.md + +## Contents + +- [1. What the storefront is](#1-what-the-storefront-is) +- [2. Provider-module architecture (the data-layer swap)](#2-provider-module-architecture-the-data-layer-swap) +- [3. Environment-variable wiring](#3-environment-variable-wiring) +- [4. Self-signed-TLS dev bypass](#4-self-signed-tls-dev-bypass) +- [5. Build-time RSC fetch caveat](#5-build-time-rsc-fetch-caveat) +- [6. Slug conventions](#6-slug-conventions) +- [7. `@vercel/*` couplings on self-host](#7-vercel-couplings-on-self-host) +- [8. Run / verify](#8-run--verify) + +> Work with the Next.js storefront that fronts a headless DW10 demo. The storefront is a **separate +> app** built from the `vercel/commerce` starter (Next.js App Router) — your team's headless +> storefront repo (vercel/commerce-based), which builds and runs anywhere Node runs (`pnpm dev`, +> `next build && next start`, or a container). **No Vercel account is required** to build, run, or +> validate it; "Vercel" is one hosting option, not a dependency. +> +> **Use AFTER** the backend is verified per [`headless-backend.md`](headless-backend.md) — the +> storefront is only as good as the `/dwapi` surface behind it. + +## 1. What the storefront is + +`vercel/commerce` is a production Next.js storefront (PLP, PDP, cart, checkout, search, menus) whose +entire UI consumes a small set of **normalized domain types** (`Product`, `Collection`, `Cart`, +`Menu`, `Page`) produced by a single **data-layer module**. The starter ships that module for +Shopify (`lib/shopify/`). A DW headless demo **replaces the data module, keeps every UI component**. +That is the whole architecture: the React layer never learns it is talking to Dynamicweb. + +## 2. Provider-module architecture (the data-layer swap) + +The starter has no formal plugin SDK. Its "provider contract" is a single folder exporting async +functions that return the normalized types: + +``` +getProduct, getProducts, getCollection, getCollectionProducts, +getCart, createCart, addToCart, updateCart, removeFromCart, +getMenu, getPage, getPages, … +``` + +Every UI component consumes only the normalized types from `lib/*/types`. So "conform to the +provider contract" means: **replace `lib/shopify/` with a `lib/dynamicweb/` module implementing the +same function surface**, calling the Delivery API over REST/JSON and **reshaping** DW view-models +into the unchanged domain types. Do not rewrite the domain layer or the components. + +Structure of the DW module: + +- `lib/dynamicweb/index.ts` — the ~15 exported provider functions. +- `lib/dynamicweb/types.ts` — reshapers: DW view-model → normalized `Product`/`Collection`/`Cart`/ + `Menu`/`Page`. +- `lib/dynamicweb/dwapi.ts` — a thin fetch client (base URL, locale/shop query params, the dev TLS + bypass of §4, JWT handling for user-scoped calls). + +**Mapping (DW source → normalized type), abbreviated:** + +| Normalized type | DW source | Key field mapping | +|---|---|---| +| `Product` | `GET /ecommerce/products/{id}`, `products[]` of the search result | `handle ← number`; `title ← name`; `descriptionHtml ← longDescription`; `priceRange ← price`/`prices[]`; `variants ← variantInfo` + `/variants/{id}`; `images ← imagePatternImages`; `availableForSale ← active && (neverOutOfstock || stockLevel>0)` | +| `Collection` | `GET /ecommerce/groups`, `/groups/{groupId}` | `handle ← id` (e.g. `GROUP1`); `title ← name`; `path ← '/search/'+id`; `products ← search?GroupId=id` | +| `Cart` | `carts/create`, `/carts/{secret}`, `/{secret}/items`, `/{secret}/checkout` | `id ← secret`; `checkoutUrl ← /carts/{secret}/checkout`; `lines ← cart lines`; `cost.* ← cart price fields` | +| `Menu` | `GET /frontend/navigations/{areaId}` | `Menu[] ← nodes[]` recursively → `{ title ← node.title, path ← node.link }` | +| `Page` | `GET /content/pages/{id}`, `/pages/url`; body via `/content/rows/{pageId}/{device}` | `handle ← path`; `title ← title`; `body ← rows/paragraphs`; `seo ← {title,description}` | + +All DW coupling (endpoints, view-model quirks, price/VAT handling, the search-query dependency) +lives in this one folder — easy to gate, mock, and evolve as the headless baseline matures. + +## 3. Environment-variable wiring + +`DYNAMICWEB_*` / `DW_*` env vars replace the starter's `SHOPIFY_*`. The provider reads them; nothing +is hardcoded: + +| Var | Purpose | +|---|---| +| `DW_API_BASE` | Delivery API base, e.g. `https:///dwapi` | +| `DW_SHOP_ID` | shop context — `SHOP1` (see the locale gotcha, [`headless-backend.md`](headless-backend.md) §4) | +| `DW_LANGUAGE_ID` | language context — `ENU` (**not** `LANG1`) | +| `DW_CURRENCY_CODE` | currency for price display | +| `DW_AREA_ID` | area id for `GET /frontend/navigations/{areaId}` (menu) | + +Seed the shop/language/currency/area defaults from `GET /dwapi/content/areas` — the area carries the +`ecomShopId` / `ecomLanguageId` / `ecomCurrencyCode` bindings the provider should default to. Remove +the `SHOPIFY_*` vars and the Shopify webhook revalidation route (`app/api/revalidate` keyed on +Shopify topics) when you delete `lib/shopify/`. + +## 4. Self-signed-TLS dev bypass + +The demo host serves HTTPS with a self-signed certificate, so the provider's server-side `fetch` +rejects it by default. For **local development only**, set on the Node process running the storefront: + +``` +NODE_TLS_REJECT_UNAUTHORIZED=0 +``` + +This is the same class of bypass `dynamicweb-demo-base` wires for the MCP/browser layer, and it is +**dev-only**. Never set it on a hosted/production storefront — it disables all TLS verification for +the process. For a hosted demo, terminate TLS with a real certificate and drop this var. Confine it +to the storefront's local `.env`/shell; do not commit it into any deploy config. + +## 5. Build-time RSC fetch caveat + +The starter's pages are React Server Components that fetch during rendering — and the App Router +executes those fetches **at build time** for static generation. Consequence: **`next build` cannot +complete without a reachable provider.** If the DW backend is down (or `DW_API_BASE` is wrong) when +you build, the build fails on the data fetch, not at runtime. + +Handling: + +- For a gate/CI build, ensure the backend is up and reachable first (the backend verification gate in + [`headless-backend.md`](headless-backend.md) §7 is the precondition). +- Or force affected routes to dynamic rendering (`export const dynamic = 'force-dynamic'`) / set + `revalidate` so they are not statically pre-rendered at build. +- `pnpm dev` does not hit this — it fetches per request — so develop against `dev`, and treat a clean + `next build` as a separate gate that needs the live backend. + +## 6. Slug conventions + +`vercel/commerce` routes on `handle` slugs; DW keys numeric/string ids. Use **business-stable** +sources so URLs survive baseline rebuilds: + +| Domain type | `handle` source | Provider reverse-resolution | +|---|---|---| +| Product | **product `number`** (`EcomProducts.ProductNumber`) | search `sku` param, or a product-number filter | +| Collection | **group id** (`EcomGroups.GroupId`, e.g. `GROUP1`) | search `GroupID` param (match on `ParentGroupIDs`) | + +Product numbers and group ids are business-stable — they survive re-serialization and rebuilds. +Database-assigned auto ids and display names (rename-fragile) are **not** slug sources. The baseline's +menu items follow the same rule: a menu item carries a group id for a product-group link and a product +number for a product link. Page-ref resolution (a menu item pointing at a content page) resolves the +page path — confirm it end-to-end in the provider. + +## 7. `@vercel/*` couplings on self-host + +The dominant coupling in the starter is **Shopify**, not `@vercel/*` — swapping the provider removes +most of it. For the rest: + +- **Removed with `lib/shopify/`:** the GraphQL client/queries/mutations, all `SHOPIFY_*` env, the + Shopify-topic webhook revalidation route, Shopify GraphQL types. +- **Kept (Next-native, no account needed):** on-demand ISR via `revalidateTag`/`revalidatePath` — + these are `next/cache`, not `@vercel/*`, and work under `next start` on any Node host. Re-point + revalidation from Shopify webhooks to a DW-driven trigger, or fall back to time-based `revalidate`. +- **Dropped / opt-in for self-host:** `@vercel/analytics`, `@vercel/speed-insights`, and the Vercel + Toolbar are optional telemetry — remove them or leave them inert. None are required to build or to + pass the demo gate. + +Reconcile the exact `@vercel/*` list against the actually-scaffolded starter `package.json` — the +starter evolves, so treat the list above as the shape, not a pinned inventory. + +## 8. Run / verify + +- `pnpm install` then `pnpm dev` — the storefront comes up on its own port (default `3000`), + fetching live from `DW_API_BASE`. +- Confirm the parity surfaces render **against the backend**, not just that the app compiles: PLP + lists products, PDP renders a product by its number-slug, a collection page filters by group, + add-to-cart returns a cart secret, and the menu renders from the navigations endpoint. +- Treat `next build` as a separate gate (needs the live backend per §5). + +> **Gate scope note.** The clean-room harness does not provision the server-side product-render index +> the Swift Razor storefront uses, so **storefront HTML render is not provable in the harness gate**. +> Assert catalog/PLP/PDP at the SQL / Delivery-API level (the backend gate) and treat rendered +> storefront output as a real-host UAT item — the same "verify behavior on a real host" posture the +> feature-pack render-proof deferral uses.