@forgecart/cli 2.202610280649.0 → 2.202610280926.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/dist/src/commands/init-questions.d.ts +12 -11
  2. package/dist/src/commands/init-questions.js +15 -23
  3. package/dist/src/commands/init-questions.js.map +1 -1
  4. package/dist/src/commands/init.d.ts +13 -13
  5. package/dist/src/commands/init.js +75 -144
  6. package/dist/src/commands/init.js.map +1 -1
  7. package/dist/src/commands/refresh.d.ts +3 -3
  8. package/dist/src/commands/refresh.js +4 -14
  9. package/dist/src/commands/refresh.js.map +1 -1
  10. package/dist/src/commands/template-manifest.d.ts +2 -5
  11. package/dist/src/commands/template-manifest.js +1 -1
  12. package/dist/src/commands/template-manifest.js.map +1 -1
  13. package/dist/src/commands/type/init.input.d.ts +1 -1
  14. package/dist/src/commands/type/init.types.d.ts +4 -4
  15. package/dist/src/template/template-set.d.ts +17 -27
  16. package/dist/src/template/template-set.js +25 -42
  17. package/dist/src/template/template-set.js.map +1 -1
  18. package/dist/src/template/template-unavailable-error.d.ts +3 -4
  19. package/dist/src/template/template-unavailable-error.js +3 -4
  20. package/dist/src/template/template-unavailable-error.js.map +1 -1
  21. package/dist/src/template/type/template-set.types.d.ts +14 -17
  22. package/dist/src/template/type/template-set.types.js +6 -1
  23. package/dist/src/template/type/template-set.types.js.map +1 -1
  24. package/dist/src/template/util/template-identity.util.d.ts +1 -3
  25. package/dist/src/template/util/template-identity.util.js +0 -5
  26. package/dist/src/template/util/template-identity.util.js.map +1 -1
  27. package/package.json +1 -1
  28. package/templates/storefront-blank/.forgecartignore +2 -0
  29. package/templates/storefront-blank/Procfile +1 -0
  30. package/templates/storefront-blank/README.md +86 -0
  31. package/templates/storefront-blank/SEO-MIGRATION.md +718 -0
  32. package/templates/storefront-blank/_gitignore +21 -0
  33. package/templates/storefront-blank/next.config.js +105 -0
  34. package/templates/storefront-blank/package.json +35 -0
  35. package/templates/storefront-blank/postcss.config.js +5 -0
  36. package/templates/storefront-blank/src/app/%5F%5Ffc/identify/route.ts +205 -0
  37. package/templates/storefront-blank/src/app/%5F%5Ffc/track/route.ts +189 -0
  38. package/templates/storefront-blank/src/app/%5F%5Fforge_beacon/route.ts +87 -0
  39. package/templates/storefront-blank/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
  40. package/templates/storefront-blank/src/app/error.tsx +21 -0
  41. package/templates/storefront-blank/src/app/global-error.tsx +23 -0
  42. package/templates/storefront-blank/src/app/globals.css +110 -0
  43. package/templates/storefront-blank/src/app/layout.tsx +102 -0
  44. package/templates/storefront-blank/src/app/page.tsx +52 -0
  45. package/templates/storefront-blank/src/app/pages/[slug]/not-found.tsx +23 -0
  46. package/templates/storefront-blank/src/app/pages/[slug]/page.tsx +115 -0
  47. package/templates/storefront-blank/src/app/ping/route.ts +21 -0
  48. package/templates/storefront-blank/src/app/robots.ts +69 -0
  49. package/templates/storefront-blank/src/app/sitemap.ts +107 -0
  50. package/templates/storefront-blank/src/components/ForgeErrorBeacon.tsx +102 -0
  51. package/templates/storefront-blank/src/components/ForgeTracker.tsx +479 -0
  52. package/templates/storefront-blank/src/components/ForgecartDesigner.tsx +43 -0
  53. package/templates/storefront-blank/src/components/LocaleLink.tsx +49 -0
  54. package/templates/storefront-blank/src/instrumentation.ts +109 -0
  55. package/templates/storefront-blank/src/lib/account/account-link.ts +76 -0
  56. package/templates/storefront-blank/src/lib/account/account-state.ts +152 -0
  57. package/templates/storefront-blank/src/lib/account/register-state.ts +133 -0
  58. package/templates/storefront-blank/src/lib/account/reset-password-state.ts +111 -0
  59. package/templates/storefront-blank/src/lib/account/verify-state.ts +56 -0
  60. package/templates/storefront-blank/src/lib/account-actions.ts +76 -0
  61. package/templates/storefront-blank/src/lib/account-session.ts +114 -0
  62. package/templates/storefront-blank/src/lib/action-result.ts +30 -0
  63. package/templates/storefront-blank/src/lib/asset-alt.ts +34 -0
  64. package/templates/storefront-blank/src/lib/backend-actions.ts +20 -0
  65. package/templates/storefront-blank/src/lib/backend-client.ts +47 -0
  66. package/templates/storefront-blank/src/lib/cart-context.tsx +236 -0
  67. package/templates/storefront-blank/src/lib/checkout-session.ts +185 -0
  68. package/templates/storefront-blank/src/lib/content/page-metadata.ts +113 -0
  69. package/templates/storefront-blank/src/lib/content/render-fields.tsx +256 -0
  70. package/templates/storefront-blank/src/lib/content/resolve-page.ts +143 -0
  71. package/templates/storefront-blank/src/lib/error-messages.ts +118 -0
  72. package/templates/storefront-blank/src/lib/experiments.ts +333 -0
  73. package/templates/storefront-blank/src/lib/forgecart.ts +464 -0
  74. package/templates/storefront-blank/src/lib/format.ts +89 -0
  75. package/templates/storefront-blank/src/lib/identify-forward.ts +152 -0
  76. package/templates/storefront-blank/src/lib/locale/channel-locales-loader.ts +169 -0
  77. package/templates/storefront-blank/src/lib/locale/channel-locales-map.ts +46 -0
  78. package/templates/storefront-blank/src/lib/locale/channel-locales.ts +191 -0
  79. package/templates/storefront-blank/src/lib/locale/grammar.ts +194 -0
  80. package/templates/storefront-blank/src/lib/locale/localized-path.ts +55 -0
  81. package/templates/storefront-blank/src/lib/locale/middleware-plan.ts +107 -0
  82. package/templates/storefront-blank/src/lib/locale/request-binding.ts +80 -0
  83. package/templates/storefront-blank/src/lib/locale/request-locale.ts +66 -0
  84. package/templates/storefront-blank/src/lib/marketing-params.ts +213 -0
  85. package/templates/storefront-blank/src/lib/money.ts +50 -0
  86. package/templates/storefront-blank/src/lib/seo/alternates.ts +123 -0
  87. package/templates/storefront-blank/src/lib/seo/json-ld.ts +266 -0
  88. package/templates/storefront-blank/src/lib/seo/metadata.ts +419 -0
  89. package/templates/storefront-blank/src/lib/seo/noindex.ts +223 -0
  90. package/templates/storefront-blank/src/lib/seo/public-origin.ts +166 -0
  91. package/templates/storefront-blank/src/lib/seo/redirect-plan.ts +86 -0
  92. package/templates/storefront-blank/src/lib/seo/resolve-path.ts +107 -0
  93. package/templates/storefront-blank/src/lib/seo/scaffolded-routes.ts +83 -0
  94. package/templates/storefront-blank/src/lib/seo/sidecar.ts +75 -0
  95. package/templates/storefront-blank/src/lib/seo/site-verification.ts +98 -0
  96. package/templates/storefront-blank/src/lib/seo/sitemap-cache.ts +114 -0
  97. package/templates/storefront-blank/src/lib/seo/sitemap-entries.ts +310 -0
  98. package/templates/storefront-blank/src/lib/session-actions.ts +61 -0
  99. package/templates/storefront-blank/src/lib/session-cookies.ts +98 -0
  100. package/templates/storefront-blank/src/lib/shop-config.ts +51 -0
  101. package/templates/storefront-blank/src/lib/shop-session.ts +204 -0
  102. package/templates/storefront-blank/src/lib/track-forward.ts +200 -0
  103. package/templates/storefront-blank/src/lib/uuid.ts +19 -0
  104. package/templates/storefront-blank/src/middleware.ts +379 -0
  105. package/templates/storefront-blank/src/seo/redirects.ts +44 -0
  106. package/templates/storefront-blank/src/seo/static-routes.ts +12 -0
  107. package/templates/storefront-blank/src/server/app.module.ts +18 -0
  108. package/templates/storefront-blank/src/server/backend-api.ts +21 -0
  109. package/templates/storefront-blank/src/server/backend-method.decorator.ts +23 -0
  110. package/templates/storefront-blank/src/server/bootstrap.ts +122 -0
  111. package/templates/storefront-blank/src/server/forge/live-revision.ts +158 -0
  112. package/templates/storefront-blank/src/server/forgecart/forgecart-client.factory.ts +73 -0
  113. package/templates/storefront-blank/src/server/forgecart/forgecart.module.ts +9 -0
  114. package/templates/storefront-blank/src/server/runner.ts +90 -0
  115. package/templates/storefront-blank/src/server/types.ts +36 -0
  116. package/templates/storefront-blank/tsconfig.json +25 -0
  117. package/templates/storefront-blank-sdk-floor.json +1174 -0
  118. package/templates/storefront-shadcn-blank/.forgecartignore +2 -0
  119. package/templates/storefront-shadcn-blank/Procfile +1 -0
  120. package/templates/storefront-shadcn-blank/README.md +93 -0
  121. package/templates/storefront-shadcn-blank/SEO-MIGRATION.md +718 -0
  122. package/templates/storefront-shadcn-blank/_gitignore +21 -0
  123. package/templates/storefront-shadcn-blank/components.json +20 -0
  124. package/templates/storefront-shadcn-blank/next.config.js +105 -0
  125. package/templates/storefront-shadcn-blank/package.json +42 -0
  126. package/templates/storefront-shadcn-blank/postcss.config.js +5 -0
  127. package/templates/storefront-shadcn-blank/src/app/%5F%5Ffc/identify/route.ts +205 -0
  128. package/templates/storefront-shadcn-blank/src/app/%5F%5Ffc/track/route.ts +189 -0
  129. package/templates/storefront-shadcn-blank/src/app/%5F%5Fforge_beacon/route.ts +87 -0
  130. package/templates/storefront-shadcn-blank/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
  131. package/templates/storefront-shadcn-blank/src/app/error.tsx +23 -0
  132. package/templates/storefront-shadcn-blank/src/app/global-error.tsx +23 -0
  133. package/templates/storefront-shadcn-blank/src/app/globals.css +156 -0
  134. package/templates/storefront-shadcn-blank/src/app/layout.tsx +102 -0
  135. package/templates/storefront-shadcn-blank/src/app/page.tsx +53 -0
  136. package/templates/storefront-shadcn-blank/src/app/pages/[slug]/not-found.tsx +24 -0
  137. package/templates/storefront-shadcn-blank/src/app/pages/[slug]/page.tsx +115 -0
  138. package/templates/storefront-shadcn-blank/src/app/ping/route.ts +21 -0
  139. package/templates/storefront-shadcn-blank/src/app/robots.ts +69 -0
  140. package/templates/storefront-shadcn-blank/src/app/sitemap.ts +107 -0
  141. package/templates/storefront-shadcn-blank/src/components/ForgeErrorBeacon.tsx +102 -0
  142. package/templates/storefront-shadcn-blank/src/components/ForgeTracker.tsx +479 -0
  143. package/templates/storefront-shadcn-blank/src/components/ForgecartDesigner.tsx +43 -0
  144. package/templates/storefront-shadcn-blank/src/components/LocaleLink.tsx +49 -0
  145. package/templates/storefront-shadcn-blank/src/components/ui/alert.tsx +75 -0
  146. package/templates/storefront-shadcn-blank/src/components/ui/badge.tsx +40 -0
  147. package/templates/storefront-shadcn-blank/src/components/ui/button.tsx +64 -0
  148. package/templates/storefront-shadcn-blank/src/components/ui/card.tsx +28 -0
  149. package/templates/storefront-shadcn-blank/src/components/ui/input.tsx +26 -0
  150. package/templates/storefront-shadcn-blank/src/components/ui/label.tsx +22 -0
  151. package/templates/storefront-shadcn-blank/src/components/ui/native-select.tsx +27 -0
  152. package/templates/storefront-shadcn-blank/src/components/ui/skeleton.tsx +21 -0
  153. package/templates/storefront-shadcn-blank/src/components/ui/utils.ts +16 -0
  154. package/templates/storefront-shadcn-blank/src/instrumentation.ts +109 -0
  155. package/templates/storefront-shadcn-blank/src/lib/account/account-link.ts +76 -0
  156. package/templates/storefront-shadcn-blank/src/lib/account/account-state.ts +152 -0
  157. package/templates/storefront-shadcn-blank/src/lib/account/register-state.ts +133 -0
  158. package/templates/storefront-shadcn-blank/src/lib/account/reset-password-state.ts +111 -0
  159. package/templates/storefront-shadcn-blank/src/lib/account/verify-state.ts +56 -0
  160. package/templates/storefront-shadcn-blank/src/lib/account-actions.ts +76 -0
  161. package/templates/storefront-shadcn-blank/src/lib/account-session.ts +114 -0
  162. package/templates/storefront-shadcn-blank/src/lib/action-result.ts +30 -0
  163. package/templates/storefront-shadcn-blank/src/lib/asset-alt.ts +34 -0
  164. package/templates/storefront-shadcn-blank/src/lib/backend-actions.ts +20 -0
  165. package/templates/storefront-shadcn-blank/src/lib/backend-client.ts +47 -0
  166. package/templates/storefront-shadcn-blank/src/lib/cart-context.tsx +236 -0
  167. package/templates/storefront-shadcn-blank/src/lib/checkout-session.ts +185 -0
  168. package/templates/storefront-shadcn-blank/src/lib/content/page-metadata.ts +113 -0
  169. package/templates/storefront-shadcn-blank/src/lib/content/render-fields.tsx +256 -0
  170. package/templates/storefront-shadcn-blank/src/lib/content/resolve-page.ts +143 -0
  171. package/templates/storefront-shadcn-blank/src/lib/error-messages.ts +118 -0
  172. package/templates/storefront-shadcn-blank/src/lib/experiments.ts +333 -0
  173. package/templates/storefront-shadcn-blank/src/lib/forgecart.ts +464 -0
  174. package/templates/storefront-shadcn-blank/src/lib/format.ts +89 -0
  175. package/templates/storefront-shadcn-blank/src/lib/identify-forward.ts +152 -0
  176. package/templates/storefront-shadcn-blank/src/lib/locale/channel-locales-loader.ts +169 -0
  177. package/templates/storefront-shadcn-blank/src/lib/locale/channel-locales-map.ts +46 -0
  178. package/templates/storefront-shadcn-blank/src/lib/locale/channel-locales.ts +191 -0
  179. package/templates/storefront-shadcn-blank/src/lib/locale/grammar.ts +194 -0
  180. package/templates/storefront-shadcn-blank/src/lib/locale/localized-path.ts +55 -0
  181. package/templates/storefront-shadcn-blank/src/lib/locale/middleware-plan.ts +107 -0
  182. package/templates/storefront-shadcn-blank/src/lib/locale/request-binding.ts +80 -0
  183. package/templates/storefront-shadcn-blank/src/lib/locale/request-locale.ts +66 -0
  184. package/templates/storefront-shadcn-blank/src/lib/marketing-params.ts +213 -0
  185. package/templates/storefront-shadcn-blank/src/lib/money.ts +50 -0
  186. package/templates/storefront-shadcn-blank/src/lib/seo/alternates.ts +123 -0
  187. package/templates/storefront-shadcn-blank/src/lib/seo/json-ld.ts +266 -0
  188. package/templates/storefront-shadcn-blank/src/lib/seo/metadata.ts +419 -0
  189. package/templates/storefront-shadcn-blank/src/lib/seo/noindex.ts +223 -0
  190. package/templates/storefront-shadcn-blank/src/lib/seo/public-origin.ts +166 -0
  191. package/templates/storefront-shadcn-blank/src/lib/seo/redirect-plan.ts +86 -0
  192. package/templates/storefront-shadcn-blank/src/lib/seo/resolve-path.ts +107 -0
  193. package/templates/storefront-shadcn-blank/src/lib/seo/scaffolded-routes.ts +83 -0
  194. package/templates/storefront-shadcn-blank/src/lib/seo/sidecar.ts +75 -0
  195. package/templates/storefront-shadcn-blank/src/lib/seo/site-verification.ts +98 -0
  196. package/templates/storefront-shadcn-blank/src/lib/seo/sitemap-cache.ts +114 -0
  197. package/templates/storefront-shadcn-blank/src/lib/seo/sitemap-entries.ts +310 -0
  198. package/templates/storefront-shadcn-blank/src/lib/session-actions.ts +61 -0
  199. package/templates/storefront-shadcn-blank/src/lib/session-cookies.ts +98 -0
  200. package/templates/storefront-shadcn-blank/src/lib/shop-config.ts +51 -0
  201. package/templates/storefront-shadcn-blank/src/lib/shop-session.ts +204 -0
  202. package/templates/storefront-shadcn-blank/src/lib/track-forward.ts +200 -0
  203. package/templates/storefront-shadcn-blank/src/lib/uuid.ts +19 -0
  204. package/templates/storefront-shadcn-blank/src/middleware.ts +379 -0
  205. package/templates/storefront-shadcn-blank/src/seo/redirects.ts +44 -0
  206. package/templates/storefront-shadcn-blank/src/seo/static-routes.ts +12 -0
  207. package/templates/storefront-shadcn-blank/src/server/app.module.ts +18 -0
  208. package/templates/storefront-shadcn-blank/src/server/backend-api.ts +21 -0
  209. package/templates/storefront-shadcn-blank/src/server/backend-method.decorator.ts +23 -0
  210. package/templates/storefront-shadcn-blank/src/server/bootstrap.ts +122 -0
  211. package/templates/storefront-shadcn-blank/src/server/forge/live-revision.ts +158 -0
  212. package/templates/storefront-shadcn-blank/src/server/forgecart/forgecart-client.factory.ts +73 -0
  213. package/templates/storefront-shadcn-blank/src/server/forgecart/forgecart.module.ts +9 -0
  214. package/templates/storefront-shadcn-blank/src/server/runner.ts +90 -0
  215. package/templates/storefront-shadcn-blank/src/server/types.ts +36 -0
  216. package/templates/storefront-shadcn-blank/tsconfig.json +25 -0
  217. package/templates/storefront-shadcn-blank-sdk-floor.json +1174 -0
  218. package/templates/template-set.json +15 -0
@@ -0,0 +1,718 @@
1
+ # Storefront SEO migration
2
+
3
+ This document migrates a storefront that was built before the platform's locale and SEO layer
4
+ existed. It is written for the agent that edits this storefront. The operator (the person who runs
5
+ this store) hands it to you; no platform process runs it. Follow it top to bottom, as one piece of
6
+ work, and finish with the DONE summary in the last section.
7
+
8
+ **This file is platform-owned. Do not edit this file**, and do not record progress in it. The
9
+ operator does not edit it either. `forgecart refresh` replaces an unchanged copy with every newer
10
+ version and leaves an edited copy alone, reporting it under `conflicts` — an edited copy stops
11
+ receiving corrections. Your progress goes in the DONE summary.
12
+
13
+ **How it arrived.** The operator ran the template refresh with no `adopt` list. The refresh:
14
+
15
+ - wrote every platform file that was missing, or unchanged since it was generated, at the current
16
+ platform version, and listed it under `rendered`;
17
+ - left every platform file that differs from the current platform version untouched, and listed it
18
+ under `conflicts`;
19
+ - listed every platform file that already matched under `upToDate`;
20
+ - never touched a file the platform does not ship.
21
+
22
+ **Does the migration apply?** Yes, when that refresh listed `src/lib/locale/grammar.ts` or
23
+ `src/lib/seo/metadata.ts` under `rendered`: neither file existed in this storefront before. There
24
+ is no version number to check instead. If you do not have the report, run the sweep in the
25
+ deletion list: any match outside the listed platform matches means the migration applies. With no
26
+ such match there is nothing to migrate — report that, with the sweep as evidence, and stop.
27
+
28
+ **What `conflicts` contains.** A provisioned storefront usually has no
29
+ `.forgecart/template-manifest.json` before its first refresh, so the refresh cannot tell a file the
30
+ merchant customized from a file that is only an older platform version: `conflicts` lists every
31
+ platform file that differs from the current version. The merge steps handle both kinds the same
32
+ way. The refresh wrote the manifest, so every later refresh is exact.
33
+
34
+ **The tree is mixed until you finish.** New platform modules now sit next to old files. For
35
+ example, `src/app/sitemap.ts` imports `getProductSeoEntries` and `getServedPageRoutes` from
36
+ `src/lib/forgecart.ts`; an older `src/lib/forgecart.ts` exports neither, so `/sitemap.xml` fails
37
+ until that file is merged. Therefore:
38
+
39
+ 1. Do the migration as its own work. Do not mix feature changes into the same turns.
40
+ 2. Put the deletions and the platform-file merges in the same promote.
41
+ 3. Do not ask the operator to publish until every step of the verification section passes.
42
+
43
+ **The refresh operation.** You call it to take the platform version of a file (to adopt it):
44
+
45
+ ```text
46
+ CallAdminOperation { "name": "workspaceTemplate.adminRefreshWorkspaceTemplate", "args": { "input": { "adopt": ["src/middleware.ts", "src/lib/forgecart.ts"] } } }
47
+ ```
48
+
49
+ It is the admin mutation `refreshWorkspaceTemplate(input: { adopt: [...] })` and returns
50
+ `rendered`, `conflicts`, `adopted`, `upToDate` and `dependenciesChanged`. Each `adopt` path must
51
+ be one a refresh reported under `conflicts`. When `dependenciesChanged` is `true`, dependencies were
52
+ re-installed before the call returned. `WORKSPACE_TEMPLATE_REFRESH_UNAVAILABLE` means the
53
+ workspace is not running yet; retry.
54
+
55
+ ## What the platform now ships
56
+
57
+ The layer the migration moves this storefront onto. Every rule below is implemented by a platform
58
+ file; your job is to call it, never to reimplement it.
59
+
60
+ - **The URL is the only language selector.** The channel's default language is served unprefixed
61
+ (`/products`); every other language under its two-letter prefix (`/de/products`). Both render the
62
+ SAME route: the middleware rewrites `/de/products` to `/products` and passes the language on. There
63
+ is no `[locale]` segment, no query parameter and no cookie that selects a language.
64
+ - **Two-letter top-level segments are reserved** for languages. Routes that exist once per store —
65
+ `/sitemap.xml`, `/robots.txt`, `/ping`, `/api`, `/__fc`, `/__forge_beacon`, `/verify`,
66
+ `/reset-password` — never take a prefix.
67
+ - **The root layout decides the status.** A prefix naming the default language answers 308 to the
68
+ unprefixed URL (`/en/products` → `/products`); a prefix the channel does not offer answers 404.
69
+ - **Links stay in their language.** Every internal link is a `LocaleLink` with the unprefixed path;
70
+ outside JSX, `localizedPath(path, binding)`. The language switcher uses plain anchors with
71
+ `hrefLang`, so switching loads a new document.
72
+ - **`src/middleware.ts` is the only `Content-Language` emitter.** Every page under a language prefix
73
+ answers with exactly one `Content-Language: <xx>`; an unprefixed page answers with none — its
74
+ `<html lang>` states the language.
75
+ - **Metadata is composed, never hand-built.** Every route's `generateMetadata` returns
76
+ `routeMetadata(...)`; the root layout returns `shellMetadata(...)`; an entity's title, description
77
+ and social image come from `entityMetadata(...)`. Canonical, hreflang, `x-default`, robots and
78
+ social tags are produced together and cannot disagree.
79
+ - **Product URLs resolve themselves.** `resolveProductPath` answers a stale slug, a renamed
80
+ product's previous slug and another language's slug with one 308 to the current address. Product
81
+ renames never need a redirect entry.
82
+ - **The XML sitemap** is `/sitemap.xml`, built by `src/app/sitemap.ts` from `STATIC_ROUTES` in
83
+ `src/seo/static-routes.ts`, the fragments in `src/seo/routes.d/`, the published pages under
84
+ `/pages/<route>` and the catalog, through the rules in `src/lib/seo/sitemap-entries.ts`.
85
+ - **`robots.txt`** is built by `src/app/robots.ts` from `NOINDEX_PATHS` and `NOINDEX_QUERY_KEYS`.
86
+ - **One noindex mechanism:** the metadata `robots` field. No `X-Robots-Tag` header anywhere.
87
+ - **Structured data** is Product + BreadcrumbList on a product page and exactly one BlogPosting on a
88
+ blog post, nothing else. Organization, WebSite, logo, `sameAs` and SearchAction are refused.
89
+ - **Rename redirects** live in `REDIRECTS` in `src/seo/redirects.ts` and work without a restart.
90
+ - **Preview posture.** A deployment without `FORGECART_PUBLIC_ORIGIN` — every preview — marks every
91
+ page noindex, emits relative canonicals, serves `robots.txt` as `Disallow: /` and an empty XML
92
+ sitemap. A published deployment gets its origin from `forgecart init --public-origin`.
93
+ - **One SDK client per language.** Changing the language of a shared client is the defect this
94
+ replaces.
95
+ - **Site verification.** The root layout reads the search-console verification placements with
96
+ `getSiteVerifications()` and passes them to `shellMetadata`, so every page serves them as
97
+ `<meta>` tags.
98
+
99
+ The platform files, and the symbols they export:
100
+
101
+ | File | What it gives you | Symbols |
102
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
103
+ | `src/lib/locale/grammar.ts` | The URL grammar: default language unprefixed, others under `/<xx>/`, reserved segments | `isLocaleSegment`, `isUnprefixedPath`, `parseLocalePath`, `stripLocalePrefix`, `localePrefixedPath` |
104
+ | `src/lib/locale/middleware-plan.ts` | The locale-rewrite decision the middleware applies | `planLocaleRewrite`, `localeRequestHeaders`, `hasServerAuthoredHeaders`, `LOCALE_REQUEST_HEADER` |
105
+ | `src/middleware.ts` | Rename redirects, the locale rewrite, the `Content-Language` header, visitor and campaign cookies | `middleware`, `config` |
106
+ | `src/lib/locale/request-binding.ts` | The request's language, resolved once per request | `getRequestLocale`, `RequestLocale` |
107
+ | `src/lib/locale/request-locale.ts` | Render, 308 to the unprefixed default, or 404 for a language the channel does not offer | `decideRequestLocale`, `LocaleDecision` |
108
+ | `src/lib/locale/localized-path.ts` | Paths in the current language, and the switcher's target | `localizedPath`, `switchedLocalePath`, `LocaleBinding` |
109
+ | `src/components/LocaleLink.tsx` | The internal link | `LocaleLink`, `LocaleLinkProps` |
110
+ | `src/components/LanguageSwitcher.tsx` | The language switcher | `LanguageSwitcher` |
111
+ | `src/lib/seo/metadata.ts` | Title, description, canonical, hreflang, robots, social and verification tags, composed | `routeMetadata`, `shellMetadata`, `entityMetadata`, `buildMetadata`, `buildShellMetadata`, `pageTitle`, `SiteVerificationMeta` |
112
+ | `src/lib/seo/alternates.ts` | Self-canonical and reciprocal hreflang with `x-default` | `buildAlternates`, `staticPathsByLocale`, `PathsByLocale` |
113
+ | `src/lib/seo/resolve-path.ts` | The product slug law: 308 for stale, previous and other-language slugs | `resolveProductPath`, `productPathsByLocale`, `PathResolution` |
114
+ | `src/lib/seo/noindex.ts` | Which pages and query states are noindex | `NOINDEX_PATHS`, `NOINDEX_QUERY_KEYS`, `shouldNoindex`, `routeRobots`, `deploymentRefusesIndexing`, `NOINDEX_ROBOTS` |
115
+ | `src/lib/seo/public-origin.ts` | The deployment's public origin, absent on previews | `getPublicOrigin`, `resolvePublicOrigin` |
116
+ | `src/app/sitemap.ts` | The XML sitemap at `/sitemap.xml` | `sitemap`, `dynamic` |
117
+ | `src/seo/static-routes.ts` | The route table: every non-entity route the storefront serves | `STATIC_ROUTES` |
118
+ | `src/lib/seo/sitemap-entries.ts` | The rules for the XML sitemap's rows | `StaticRoute`, `SCAFFOLDED_ROUTES_DIR`, `mergeScaffoldedRoutes`, `pageSitemapRoutes`, `MAX_SITEMAP_URLS` |
119
+ | `src/lib/seo/scaffolded-routes.ts` | Reads the `src/seo/routes.d/*.json` fragments | `readScaffoldedRoutes` |
120
+ | `src/app/robots.ts` | `robots.txt` | `robots`, `dynamic` |
121
+ | `src/lib/seo/json-ld.ts` | Product, BreadcrumbList and BlogPosting nodes, and their safe serialization | `productJsonLd`, `breadcrumbJsonLd`, `blogPostingJsonLd`, `serializeJsonLd`, `JsonLdNode` |
122
+ | `src/seo/redirects.ts` | The rename-redirect table you fill | `REDIRECTS`, `RedirectRule` |
123
+ | `src/lib/seo/redirect-plan.ts` | The redirect rule: exact match, chains collapsed, cycles refused | `planRedirect`, `MAX_REDIRECT_HOPS` |
124
+ | `src/lib/seo/site-verification.ts` | The search-console verification placements, read per request | `getSiteVerifications` |
125
+ | `src/lib/forgecart.ts` | One shop client per language, and the reads the XML sitemap and the product page use | `getShopClientForLocale`, `getShopClient`, `getProductBySlug`, `ProductWithSeo`, `getProductSeoEntries`, `getServedPageRoutes` |
126
+ | `src/app/layout.tsx` | `<html lang>`, the 308 and 404 decisions, and the metadata floor every page inherits | `generateMetadata`, `RootLayout` |
127
+ | `src/app/products/[slug]/page.tsx` | The product page: slug law, entity metadata, Product and BreadcrumbList | `generateMetadata`, `ProductDetailPage` |
128
+ | `src/lib/asset-alt.ts` | The `alt` text of an asset image | `getAssetAlt` |
129
+
130
+ ## The deletion list
131
+
132
+ Each class below is improvised code that the platform now provides. For every class:
133
+
134
+ 1. Find it with the sweep rows the class names.
135
+ 2. Delete the files listed under **Delete:** — under whatever name this storefront used.
136
+ 3. Remove the code listed under **Remove:** from the files that stay.
137
+ 4. Use what **Replaced by:** names instead.
138
+ 5. Add every deleted path to the DONE summary's `Deleted:` list.
139
+
140
+ **Who deletes.** The platform's storefront agent cannot delete files yet: its file tools are
141
+ `Read`, `Write`, `Edit`, `Glob` and `Grep`. A `DeleteFile` storefront tool is a tracked follow-up.
142
+ Until it ships, the **Delete:** steps are carried out by an agent or operator with file-deletion
143
+ capability on the workspace. Before the promote that carries the deletions, confirm with `Glob`
144
+ that every path on your `Deleted:` list is gone.
145
+
146
+ Rules that apply to every class:
147
+
148
+ - **Replace, never keep both.** An improvised helper left beside the platform one is a second source
149
+ of truth, and the next change calls the wrong one.
150
+ - **Never delete a platform file.** Every platform file appears in exactly one list of the refresh
151
+ report (`rendered`, `conflicts`, `adopted`, `upToDate`). A path on any of those lists is merged —
152
+ see the merge steps — never deleted.
153
+ - **Delete a file only when nothing imports it.** Grep for its module path first and move every
154
+ importer onto the platform helper in the same change.
155
+
156
+ ### The sweep
157
+
158
+ Run every row on the whole storefront. `Grep` rows are extended regular expressions (the Grep tool
159
+ runs `grep -E`); escape them as JSON strings when you pass them. `Glob` rows are file patterns from
160
+ the storefront root. The platform matches listed on each row are platform files that match on
161
+ purpose — leave them. Every other match is improvised code this migration removes.
162
+
163
+ - **S1** Glob `middleware.*` — platform matches: none
164
+ - **S2** Glob `src/middleware.*` — platform matches: `src/middleware.ts`
165
+ - **S3** Glob `proxy.*` — platform matches: none
166
+ - **S4** Glob `src/proxy.*` — platform matches: none
167
+ - **S5** Grep `NextResponse\.rewrite` in `src` — platform matches: `src/middleware.ts`
168
+ - **S6** Grep `(function|const) (localizedPath|withLocale|buildHref|localizeHref|localePath)` in `src` — platform matches: `src/lib/locale/localized-path.ts`
169
+ - **S7** Grep `/\$\{(lang|locale|language)\}` in `src` — platform matches: `src/lib/locale/grammar.ts`
170
+ - **S8** Grep `from ['"]next/link['"]` in `src` — platform matches: `src/components/LocaleLink.tsx`
171
+ - **S9** Glob `src/lib/seo.*` — platform matches: none
172
+ - **S10** Grep `<link rel=['"](canonical|alternate)` in `src` — platform matches: `src/components/LanguageSwitcher.tsx`, `src/lib/seo/json-ld.ts`
173
+ - **S11** Grep `<html lang=['"]` in `src` — platform matches: `src/app/global-error.tsx`, `src/lib/locale/channel-locales-loader.ts`
174
+ - **S12** Glob `src/app/sitemap*` — platform matches: `src/app/sitemap.ts`
175
+ - **S13** Glob `public/sitemap*` — platform matches: none
176
+ - **S14** Glob `next-sitemap.config.*` — platform matches: none
177
+ - **S15** Glob `src/app/robots*` — platform matches: `src/app/robots.ts`
178
+ - **S16** Glob `public/robots.txt` — platform matches: none
179
+ - **S17** Grep `(redirects|headers)(\(\) *\{| *:)|i18n *:` in `next.config.js` — platform matches: none
180
+ - **S18** Grep `Response\.redirect\(` in `src` — platform matches: `src/middleware.ts`
181
+ - **S19** Grep `['"]Content-Language['"]` in `src` — platform matches: `src/middleware.ts`
182
+ - **S20** Grep `[hH]ttp-?[eE]quiv` in `src` — platform matches: none
183
+ - **S21** Grep `[?&](lang|locale)=` in `src` — platform matches: none
184
+ - **S22** Grep `[pP]arams\.(lang|locale)|\.get\(['"][A-Za-z_-]*(lang|locale|language)['"]\)` in `src` — platform matches: none
185
+ - **S23** Glob `src/components/language-switcher.*` — platform matches: none
186
+ - **S24** Grep `[Aa]ccept-[Ll]anguage|NEXT_LOCALE` in `src` — platform matches: none
187
+ - **S25** Grep `application/ld\+json` in `src` — platform matches: `src/app/products/[slug]/page.tsx`, `src/lib/seo/json-ld.ts`
188
+ - **S26** Grep `['"]@type['"]: *['"](Organization|WebSite)` in `src` — platform matches: none
189
+ - **S27** Grep `\.setLanguageCode\(` in `src` — platform matches: `src/lib/shop-session.ts`
190
+ - **S28** Grep `['"]X-Robots-Tag['"]` in `src` — platform matches: none
191
+
192
+ One check is by inspection: Glob `src/app/*` and read the top-level route directories. A
193
+ `[locale]`, `[lang]` or two-letter directory (`src/app/de/`) belongs to the `[locale]` class below.
194
+
195
+ ### Improvised middleware locale stages
196
+
197
+ - **Find:** S1–S5. A second middleware or proxy file, or a rewrite outside `buildBaseResponse` in
198
+ `src/middleware.ts`.
199
+ - **Delete:** `middleware.ts`, `middleware.js`, `src/middleware.js`, `proxy.ts`, `src/proxy.ts`
200
+ - **Remove:** a language rewrite, redirect or matcher inside `src/middleware.ts` — by merging that
201
+ file (merge steps).
202
+ - **Replaced by:** the platform `src/middleware.ts` and `planLocaleRewrite`. That file is merged,
203
+ never deleted: it also carries the rename redirects and the visitor and campaign cookies.
204
+
205
+ ### `localizedPath` twins and bare links
206
+
207
+ - **Find:** S6–S8. A link helper defined outside the platform files, a path built by hand as
208
+ `/${locale}/…`, or `next/link` imported anywhere but `LocaleLink`.
209
+ - **Delete:** `src/lib/localized-path.ts`
210
+ - **Remove:** hand-prefixed hrefs and bare `next/link` imports in components that stay.
211
+ - **Replaced by:** `LocaleLink` in JSX (`href` is the unprefixed path, `locale` is the request's
212
+ `LocaleBinding`) and `localizedPath(path, binding)` outside JSX. Trap: the platform file
213
+ `src/lib/locale/localized-path.ts` has the same basename as the usual twin. Keep it.
214
+
215
+ ### Hand-written `seo.ts` metadata helpers
216
+
217
+ - **Find:** S9–S11. A metadata helper module, hand-written head tags, a literal `<html lang>`.
218
+ - **Delete:** `src/lib/seo.ts`
219
+ - **Remove:** hand-written `<link rel="canonical">` and `<link rel="alternate" hreflang>` tags,
220
+ static `export const metadata` objects, and `alternates`, `canonical` or `robots` values built
221
+ by hand in a route's metadata.
222
+ - **Replaced by:** `routeMetadata(...)` in every route's `generateMetadata`, `shellMetadata(...)` in
223
+ the root layout, and `entityMetadata(...)` for an entity (`src/lib/seo/metadata.ts`).
224
+
225
+ ### XML sitemap emitters other than `src/app/sitemap.ts`
226
+
227
+ - **Find:** S12–S14.
228
+ - **Delete:** `src/app/sitemap.xml/route.ts`, `public/sitemap.xml`, `next-sitemap.config.js`
229
+ - **Remove:** a `postbuild` script that runs `next-sitemap`, and the `next-sitemap` dependency (see
230
+ `package.json` in the merge steps).
231
+ - **Replaced by:** `src/app/sitemap.ts`. A route handler at `src/app/sitemap.xml/route.ts` claims
232
+ the same URL as that file and must go.
233
+
234
+ ### `robots.txt` files other than `src/app/robots.ts`
235
+
236
+ - **Find:** S15–S16.
237
+ - **Delete:** `public/robots.txt`, `src/app/robots.txt/route.ts`
238
+ - **Replaced by:** `src/app/robots.ts`. To keep a page out of search, make it noindex (see the
239
+ static pages below); never hand-edit a disallow list.
240
+
241
+ ### Ad-hoc rename redirects and Pages Router `i18n`
242
+
243
+ - **Find:** S17–S18. `redirects()` or an `i18n` key in `next.config.js`, a redirect map in the old
244
+ middleware, a route handler that only redirects a moved page.
245
+ - **Delete:** `src/app/<old-path>/route.ts`
246
+ - **Remove:** the `redirects()` function and the `i18n` key from `next.config.js` (merge steps).
247
+ - **Replaced by:** one `REDIRECTS` entry per moved page (see the rename redirects below) and the
248
+ locale grammar.
249
+
250
+ ### Duplicate `Content-Language` emitters
251
+
252
+ - **Find:** S17, S19–S20.
253
+ - **Delete:** no file.
254
+ - **Remove:** every `Content-Language` header set outside the rewrite branch of `src/middleware.ts`
255
+ — a `headers()` entry in `next.config.js`, a second `headers.set('Content-Language', …)`, a route
256
+ handler — and every `<meta httpEquiv="content-language">`.
257
+ - **Replaced by:** the rewrite branch of `src/middleware.ts` (`buildBaseResponse`). Do not add a
258
+ header to unprefixed pages: they carry none by design, and a second emitter produces two header
259
+ lines, which the promote gate reports as `SEO_DUPLICATE_LOCALE_HEADER`.
260
+
261
+ ### `?lang=` and `?locale=` switchers
262
+
263
+ - **Find:** S21–S23.
264
+ - **Delete:** `src/components/language-switcher.tsx`
265
+ - **Remove:** every read of a `lang` or `locale` query parameter, and every link or `router.push`
266
+ that sets one.
267
+ - **Replaced by:** `LanguageSwitcher` (`src/components/LanguageSwitcher.tsx`), which
268
+ `src/components/Header.tsx` already renders. Trap: `language-switcher.tsx` and
269
+ `LanguageSwitcher.tsx` differ only in letter case. Delete the lowercase file by its exact path;
270
+ the platform file exports `LanguageSwitcher` and imports `switchedLocalePath`.
271
+
272
+ ### Language state outside the URL
273
+
274
+ - **Find:** S24, plus any cookie or `localStorage` entry that stores the language and any redirect
275
+ of `/` by language.
276
+ - **Delete:** no file.
277
+ - **Remove:** the cookie and storage reads and writes, the `Accept-Language` negotiation, the
278
+ redirect.
279
+ - **Replaced by:** the URL: the default language unprefixed, every other language under `/<xx>/`.
280
+
281
+ ### `[locale]` segments and per-language route copies
282
+
283
+ - **Find:** the inspection of `src/app/*` above.
284
+ - **Delete:** `src/app/[locale]/`, `src/app/[lang]/`, `src/app/de/`
285
+ - **Remove:** `generateStaticParams` over languages in the pages you keep.
286
+ - **Replaced by:** one route per page in the flat tree: `src/app/our-story/page.tsx` serves
287
+ `/our-story` and, through the middleware rewrite, `/de/our-story`. Move each page to its
288
+ unprefixed route before you delete the segment, and merge per-language copies into that one route:
289
+ it reads its copy in the request's language (`getRequestLocale()`). A route left in a two-letter
290
+ directory is reported as `SEO_RESERVED_LOCALE_SEGMENT`.
291
+
292
+ ### Hand-built JSON-LD
293
+
294
+ - **Find:** S25–S26.
295
+ - **Delete:** no file.
296
+ - **Remove:** every hand-built `<script type="application/ld+json">` and every Organization,
297
+ WebSite, logo, `sameAs` or SearchAction node.
298
+ - **Replaced by:** `productJsonLd`, `breadcrumbJsonLd`, `blogPostingJsonLd` and `serializeJsonLd`
299
+ (`src/lib/seo/json-ld.ts`). Anything else is reported as `SEO_FORBIDDEN_JSONLD`.
300
+
301
+ ### Language set on a shared SDK client
302
+
303
+ - **Find:** S27.
304
+ - **Delete:** no file.
305
+ - **Remove:** every `setLanguageCode(` call on a server-side client, and every module-level client
306
+ shared across languages.
307
+ - **Replaced by:** `getShopClient()` for the request's language and `getShopClientForLocale(locale)`
308
+ for an explicit one (`src/lib/forgecart.ts`). Trap: `src/lib/shop-session.ts` calls
309
+ `client.setLanguageCode(...)` on the shopper's browser session client when the language changes.
310
+ That is platform code; leave it.
311
+
312
+ ### A second noindex mechanism
313
+
314
+ - **Find:** S17, S28.
315
+ - **Delete:** no file.
316
+ - **Remove:** every `X-Robots-Tag` header.
317
+ - **Replaced by:** the metadata `robots` field, which `routeMetadata` sets from `NOINDEX_PATHS`,
318
+ `NOINDEX_QUERY_KEYS`, the deployment's posture and the route's `contentIndexable`.
319
+
320
+ ## Merge steps per platform file — replace, don't add
321
+
322
+ Every file the refresh listed under `conflicts` is merged with this procedure:
323
+
324
+ 1. **Read it and list what the merchant customized:** markup, copy, styling, extra components,
325
+ extra helpers, non-SEO settings. Anything the deletion list covers is not a customization — it is
326
+ what the migration replaces.
327
+ 2. **Adopt it** — take the platform version:
328
+ `CallAdminOperation { "name": "workspaceTemplate.adminRefreshWorkspaceTemplate", "args": { "input": { "adopt": ["<path>"] } } }`.
329
+ Adopt a file BEFORE you edit it in the turn, never after: adopting writes the live file at once,
330
+ and an edit made before it was based on the old content, so the promote reports it as a conflict.
331
+ Adopt several files in one call when you can.
332
+ 3. **Re-apply the customizations from step 1 onto the platform version**, through the platform
333
+ helpers: links as `LocaleLink`, metadata through `routeMetadata`, structured data through
334
+ `json-ld.ts`. Never paste the old implementation back beside the new one.
335
+ 4. **Record it.** With nothing to re-apply, the file is "Adopted as-is". Otherwise it is "Merged".
336
+
337
+ A merged file keeps appearing under `conflicts` in every later refresh: it now differs from the
338
+ platform version on purpose. An adopted-as-is file appears under `upToDate`.
339
+
340
+ ### `src/middleware.ts`
341
+
342
+ - This is the one middleware. Keep the platform order inside `middleware()`: `planRedirect(...)`
343
+ with `REDIRECTS` first (308), then `planLocaleRewrite`, then the visitor and campaign cookie
344
+ stages.
345
+ - Merchant logic that is not about language or SEO (an access gate, extra response headers) goes
346
+ inside `middleware()` after the redirect stage. It never rewrites or redirects by language.
347
+ - Keep the platform `export const config` matcher. Never add a second `config`.
348
+ - Never set `Content-Language` or `X-Robots-Tag` here or anywhere else.
349
+ - A rename redirect the old middleware carried becomes a `REDIRECTS` entry, not code here.
350
+
351
+ ### `src/app/layout.tsx`
352
+
353
+ - `RootLayout` stays `async` and reads `await getRequestLocale()`. It raises
354
+ `permanentRedirect(decision.to)` and `notFound()` in the shell, before any markup.
355
+ - `<html lang={binding.locale}>` — never a literal language.
356
+ - `generateMetadata` returns `shellMetadata({ shopName, channelResolved, siteVerifications })` with
357
+ `siteVerifications` from `await getSiteVerifications()`. No static `export const metadata`.
358
+ - Keep the development-only revision stamp: it is merged into `other`, which carries the
359
+ verification tags — never overwrite `other`.
360
+ - No `<Suspense>` above `<body>`. Keep `CartProvider` with
361
+ `getBrowserShopConfig(binding.locale)` and `<Header locale={binding} languageCodes={languageCodes} />`.
362
+ - Re-apply the merchant's header and footer chrome and copy; every internal link is
363
+ `<LocaleLink href="/unprefixed-path" locale={binding}>`.
364
+
365
+ ### `src/app/products/[slug]/page.tsx`
366
+
367
+ - Keep the slug law in the page shell: `resolveProductPath({ requestedSlug, product, binding })`,
368
+ `notFound()` for a missing product, `permanentRedirect(resolution.to)` for a redirect.
369
+ - Keep the one product read shared by `generateMetadata` and the page (`productForRequest`).
370
+ - `generateMetadata` returns `routeMetadata` with `pathsByLocale: productPathsByLocale(product)`,
371
+ title, description and social image from `entityMetadata(product, binding.locale)`, and
372
+ `contentIndexable` from the resolution.
373
+ - Structured data only through `productJsonLd`, `breadcrumbJsonLd` and `serializeJsonLd`, gated on
374
+ `routeRobots` exactly as the platform file does it.
375
+ - Re-apply the merchant's display markup inside `ProductDetail`: exactly one `<h1>`, images with
376
+ `alt={getAssetAlt(asset)}`.
377
+ - Delete language-specific slug lookups: the shop API resolves slugs per language, including a
378
+ product's previous slug.
379
+
380
+ ### `src/lib/forgecart.ts`
381
+
382
+ - One shop client per language: `getShopClient()` for the request's language,
383
+ `getShopClientForLocale(locale)` for an explicit one. No module-level shared client, no
384
+ `setLanguageCode(`.
385
+ - Keep `getProductBySlug` returning `ProductWithSeo`, `getProductSeoEntries` and
386
+ `getServedPageRoutes`: `src/app/sitemap.ts` and the product page import them.
387
+ - Re-apply merchant reads as functions on `await getShopClient()`, through the SDK's typed methods.
388
+
389
+ ### `next.config.js`
390
+
391
+ - Remove `redirects()` (its entries become `REDIRECTS` entries), every `headers()` entry that sets
392
+ `Content-Language` or `X-Robots-Tag`, and the Pages Router `i18n` key.
393
+ - Keep `cacheComponents: false` as an explicit key. Next re-enables an absent key, and with the
394
+ option on, the root layout can no longer answer its 308 and 404.
395
+ - Keep the platform's `output`, `serverExternalPackages`, `allowedDevOrigins`, `experimental` and
396
+ `turbopack` settings.
397
+ - Re-apply merchant settings that are not about language or SEO — for example a narrower
398
+ `images.remotePatterns`.
399
+
400
+ ### Every other file in `conflicts`
401
+
402
+ Typical ones: `src/app/page.tsx`, `src/app/products/page.tsx`, `src/components/Header.tsx`,
403
+ `src/components/ProductCard.tsx`, `package.json`. The same procedure applies:
404
+
405
+ - links as `LocaleLink`; route metadata through `routeMetadata` with
406
+ `staticPathsByLocale(languageCodes, '<route>')`; images with `alt={getAssetAlt(asset)}`;
407
+ - `package.json`: adopt it. Adopting keeps every package the storefront declares that the platform
408
+ version does not, and takes the platform's range for a package both declare. Then delete the
409
+ packages the deletion list removed, such as `next-sitemap`, from it; the promote reinstalls the
410
+ dependencies. A `package.json` that still declares a package of the storefront's is Merged, with
411
+ those packages as its customizations.
412
+
413
+ ### Routes the platform does not ship
414
+
415
+ A merchant route (for example `src/app/our-story/page.tsx`) is not in the refresh report. Migrate
416
+ it in place: `generateMetadata` returns `routeMetadata(...)`, every internal link is a
417
+ `LocaleLink`, the page has exactly one `<h1>`, and it joins the XML sitemap (next section). A route
418
+ that sets `other` in its metadata passes `siteVerifications: await getSiteVerifications()` to
419
+ `routeMetadata` and merges its own keys into the returned `other` — otherwise it replaces the
420
+ verification tags the root layout serves.
421
+
422
+ ### The data tables — never adopt them
423
+
424
+ `src/seo/redirects.ts` (`REDIRECTS`) and `src/seo/static-routes.ts` (`STATIC_ROUTES`) are tables.
425
+ Never adopt either once it carries a row you or the merchant added: adopting replaces the table
426
+ with the platform's. `src/seo/redirects.ts` with your rows appears under `conflicts` in every later
427
+ refresh — expected. Do not add rows to `STATIC_ROUTES` in this migration: pages join the XML
428
+ sitemap through fragments (next section), which keeps `src/seo/static-routes.ts` identical to the
429
+ platform version, so later refreshes keep updating it. `src/lib/seo/sitemap-entries.ts` holds the
430
+ rules that read the table and no routes: never add one there.
431
+
432
+ ## Existing rename redirects and static pages
433
+
434
+ ### Rename redirects
435
+
436
+ 1. Collect every rename the old storefront served: `redirects()` in `next.config.js`, redirect
437
+ maps in the old middleware, route handlers that only redirect a moved page (sweep rows S17, S18).
438
+ 2. For each moved page, add one entry to `REDIRECTS` in `src/seo/redirects.ts`:
439
+ `{ from: '/old-path', to: '/new-path' }`.
440
+ - Paths are rooted and unprefixed. `/old-path` covers `/de/old-path` too, and each redirects
441
+ inside its own language. Never write a prefixed path: it covers one language only.
442
+ - Matching is exact: one entry per moved page. Chains collapse into one 308, and cycles are
443
+ refused.
444
+ - Routes that exist once per store (`/sitemap.xml`, `/robots.txt`, `/api/…`, `/ping`,
445
+ `/verify`, `/reset-password`) are never redirected.
446
+ 3. **Product renames are never `REDIRECTS` entries.** The shop API keeps a product's previous slug,
447
+ and `resolveProductPath` answers it with a 308. Drop the old product redirect instead of moving
448
+ it.
449
+ 4. Remove the old mechanism in the same change (deletion list).
450
+ 5. A page that only moved out of a `[locale]` segment or a two-letter directory keeps its URLs —
451
+ `/de/our-story` is now served by `src/app/our-story/page.tsx` through the rewrite — and needs no
452
+ entry. A default-language URL the old storefront served under a prefix (`/en/our-story`) is
453
+ redirected by the platform; it needs no entry either.
454
+
455
+ ### Static pages
456
+
457
+ 1. Every page that is not a platform file and not an ACF page joins the XML sitemap with one
458
+ fragment file, `src/seo/routes.d/<page>.json`:
459
+
460
+ ```json
461
+ { "path": "/our-story", "indexable": true }
462
+ ```
463
+
464
+ - `path` is rooted and unprefixed. One file per page; the file name names the page.
465
+ - A malformed fragment is skipped and named in the server log.
466
+ - A path already in `STATIC_ROUTES` (`src/seo/static-routes.ts`) keeps the platform's row; a
467
+ fragment cannot override it.
468
+
469
+ 2. A session or funnel page (sign-in, account, order status) gets `"indexable": false` in its
470
+ fragment and `contentIndexable: false` in its `routeMetadata(...)`.
471
+ 3. The page's `generateMetadata` has the shape of the one in `src/app/products/page.tsx`:
472
+
473
+ ```ts
474
+ export async function generateMetadata({
475
+ searchParams,
476
+ }: {
477
+ searchParams: Promise<RouteSearchParams>;
478
+ }): Promise<Metadata> {
479
+ const [resolvedSearchParams, { binding, languageCodes, shopName, channelResolved }] =
480
+ await Promise.all([searchParams, getRequestLocale()]);
481
+ return routeMetadata({
482
+ binding,
483
+ shopName,
484
+ channelResolved,
485
+ pathname: '/our-story',
486
+ pathsByLocale: staticPathsByLocale(languageCodes, '/our-story'),
487
+ searchParams: resolvedSearchParams,
488
+ title: 'Our story',
489
+ });
490
+ }
491
+ ```
492
+
493
+ 4. ACF pages under `/pages/<route>` join the XML sitemap by themselves. Write no fragment for them.
494
+ 5. A page in a two-letter top-level directory moves to a name of three letters or more, with a
495
+ `REDIRECTS` entry for the old path.
496
+
497
+ ### SEO source mapping for imported page-kind definitions
498
+
499
+ A route that renders the records of a page-kind definition the merchant imported — a blog, a
500
+ journal, a lookbook, for example `src/app/blog/[slug]/page.tsx` — maps its SEO sources at the route:
501
+ its `generateMetadata` passes the record's own fields, in the request's language, to
502
+ `routeMetadata`. The target is no `SEO_UNMAPPED_PAGE_KIND` on those routes.
503
+
504
+ ACF pages the platform serves at `/pages/<route>` render the page's name as their title and no
505
+ description, so they report `SEO_UNMAPPED_PAGE_KIND` (a WARN) whatever you do in this migration. Do
506
+ not edit `src/app/pages/[slug]/page.tsx` to silence it; list those routes under Remaining WARNs.
507
+
508
+ Sign off every imported definition against this table:
509
+
510
+ | Element | Where it comes from | How you check it |
511
+ | ---------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
512
+ | Title tag | `routeMetadata({ title })` from the record's title field; the shop name is appended | `<title>` on every language's URL; `SEO_TITLE_MISSING` |
513
+ | Meta description | `routeMetadata({ description })` from the record's excerpt field — never the body | `<meta name="description">` on every language's URL; `SEO_UNMAPPED_PAGE_KIND` |
514
+ | H1 | Exactly one `<h1>`, carrying the text the title comes from | `SEO_H1_MISSING`, `SEO_H1_DUPLICATE` |
515
+ | Slug | The route path, unprefixed; a changed code-page path gets a `REDIRECTS` entry | `SEO_RENAME_WITHOUT_REDIRECT` |
516
+ | Excerpt (dek) | Rendered as the intro paragraph under the `<h1>`, and the same words as the description | The snapshot of each language's page |
517
+ | Social opt-in | `socialImage` only when the record has a stored social image; otherwise leave it out | No `og:*` or `twitter:*` tags without one |
518
+ | Canonical | Produced by `routeMetadata`; never written by hand | `SEO_CANONICAL_MISSING_OR_FOREIGN` on the published deployment |
519
+ | Author | `authorName` in `blogPostingJsonLd(...)` | The BlogPosting node |
520
+ | Dates | `publishedAt` and `updatedAt` in `blogPostingJsonLd(...)`: the shop API's ISO-8601 stamps | `SEO_SCHEMA_BLOGPOSTING_INVALID` |
521
+ | BlogPosting | Exactly one node per post, from `blogPostingJsonLd(...)` and `serializeJsonLd(...)` | `SEO_SCHEMA_BLOGPOSTING_INVALID` |
522
+ | Alt | `alt={getAssetAlt(asset)}` for asset images; an embed's own `alt` for body images | `SEO_IMG_ALT_MISSING` |
523
+ | Indexability | `contentIndexable` in `routeMetadata(...)`, and `indexable` in the page's fragment | `SEO_NOINDEX_MISSING` |
524
+
525
+ ### Alt-text backfill
526
+
527
+ Migrated media arrive with no alt text: an image rendered through `getAssetAlt(asset)` renders
528
+ `alt=""` until its asset is described.
529
+
530
+ 1. Describe each content image once, on its asset — one value for every language:
531
+ `CallAdminOperation { "name": "asset.updateAsset", "args": { "input": { "id": "<asset id>", "altText": "<what the image shows>" } } }`.
532
+ The operator can do the same in the dashboard's media library.
533
+ 2. Start with hero and featured images, then images inside bodies. An image embedded in a rich-text
534
+ body carries its own `alt`, per language, in that body.
535
+ 3. Decorative images stay `alt=""`. Never fill alt text with a file name or a product name.
536
+
537
+ Until the backfill is done, `SEO_IMG_ALT_MISSING` WARNs on the routes you change are expected and
538
+ do not block the migration. List the remaining ones in the DONE summary.
539
+
540
+ ## Verification — the crawler assertions and the per-locale browser round
541
+
542
+ Every step runs on the final tree, after the promote that applied it. Read the channel's languages
543
+ first: `CallAdminOperation { "name": "channel.channels", "args": { "options": { "take": 1 } } }`
544
+ returns `defaultLanguageCode` and `availableLanguageCodes`. The examples use `en` as the default and
545
+ `de` as another language.
546
+
547
+ ### 1. The sweep is clean
548
+
549
+ Run every sweep row (S1–S28) again. Each returns exactly its platform matches. Then check:
550
+
551
+ - every path on your `Deleted:` list is gone;
552
+ - no path on your `Deleted:` list appears in any list of the refresh report.
553
+
554
+ ### 2. The crawler assertions — the promote gate
555
+
556
+ > **Parked 2026-10-26 (#3699).** The promote no longer runs this gate: no promote crawls the
557
+ > storefront, no promote is rolled back on `SEO_RED`, and every promote reports its SEO verdict
558
+ > as not run. The crawl returns as a separate survey (#3698). Until it lands, the rest of this
559
+ > step describes what that crawl checks, not what a promote enforces — a finding below never
560
+ > refuses a promote.
561
+
562
+ Every promote runs the platform's SEO gate: it crawls the routes the promote changed, in every
563
+ language, on the storefront's own server, plus `/`, `/robots.txt` and `/sitemap.xml`. It is the
564
+ crawler that verifies this migration; `SEO_RED` rolls the promote back and names each finding with
565
+ its route.
566
+
567
+ - Findings on the routes the promote changed are RED. Findings on routes it did not change, on the
568
+ links out of `/`, and four codes on every route (`SEO_H1_MISSING`, `SEO_H1_DUPLICATE`,
569
+ `SEO_IMG_ALT_MISSING`, `SEO_UNMAPPED_PAGE_KIND`) are WARN. `/robots.txt` and `/sitemap.xml` are
570
+ always RED.
571
+ - It plans at most 25 changed routes per promote and names the ones it skipped. Migrate merchant
572
+ routes in batches of at most 25, after the promote that carries the deletions and the platform
573
+ merges.
574
+ - **The preview asserts nothing about its address.** It has no public origin — whether it is the
575
+ local `<code>.127.0.0.1.nip.io:8081` host or a hosted preview — so every page is noindex, canonicals
576
+ are relative, `robots.txt` is `Disallow: /` and the XML sitemap is empty. That is correct. The
577
+ gate therefore SKIPS `SEO_CANONICAL_MISSING_OR_FOREIGN`, `SEO_HREFLANG_SET_INCOMPLETE`,
578
+ `SEO_XDEFAULT_MISSING` and `SEO_SITEMAP_ROUTE_MISSING` there and prints why. A skip is not a pass:
579
+ step 6 checks them on the published deployment. A wrong claim is still reported on the preview —
580
+ a foreign canonical, a `?lang=` hreflang, a set that is not reciprocated.
581
+ - Fix a RED at the named route before anything else, and never re-promote an unchanged tree hoping
582
+ it passes: the gate is deterministic.
583
+
584
+ | Code | Severity on a changed route | What it means after a migration | Fix |
585
+ | ---------------------------------- | -------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------- |
586
+ | `SEO_HTML_LANG_MISMATCH` | RED | `<html lang>` is a literal or ignores the URL's language | `<html lang={binding.locale}>` in `src/app/layout.tsx` |
587
+ | `SEO_CANONICAL_MISSING_OR_FOREIGN` | RED | A hand-written or missing canonical | `routeMetadata(...)`; delete the hand-written tag |
588
+ | `SEO_HREFLANG_NOT_RECIPROCAL` | RED | Language versions list different alternate sets | `staticPathsByLocale` or `productPathsByLocale` for `pathsByLocale` |
589
+ | `SEO_HREFLANG_SET_INCOMPLETE` | RED on catalog routes, else WARN | The alternate set omits a language the store serves | Build `pathsByLocale` from `languageCodes` |
590
+ | `SEO_HREFLANG_QUERY_PARAM` | RED | An alternate points at `?lang=` | The deletion list's `?lang=` class |
591
+ | `SEO_XDEFAULT_MISSING` | RED on catalog routes, else WARN | Hand-built alternates without `x-default` | `routeMetadata(...)` |
592
+ | `SEO_LOCALE_PREFIX_NOT_REDIRECTED` | RED | `/en/…` answers 200: an improvised stage or segment serves it | Delete the improvised stage or `[locale]` segment |
593
+ | `SEO_LOCALE_ORPHAN_LINK` | RED | A link leaves its language: a hand-built or bare link remains | `LocaleLink` or `localizedPath` |
594
+ | `SEO_TITLE_MISSING` | RED | A route emits no title | `generateMetadata` returning `routeMetadata({ title })` |
595
+ | `SEO_RESERVED_LOCALE_SEGMENT` | RED | A route sits in a two-letter or infrastructure directory | Move it to a longer name, with a `REDIRECTS` entry |
596
+ | `SEO_RENAME_WITHOUT_REDIRECT` | RED | A moved or removed route has no redirect | A `REDIRECTS` entry |
597
+ | `SEO_NOINDEX_MISSING` | RED | A session, funnel or query-state page is indexable | `contentIndexable: false`; delete a second noindex mechanism |
598
+ | `SEO_FORBIDDEN_JSONLD` | RED | Hand-built or forbidden structured data | `json-ld.ts` builders only |
599
+ | `SEO_SITEMAP_INVALID` | RED | An improvised emitter answers `/sitemap.xml`, or it fails | Delete the emitter; merge `src/lib/forgecart.ts` |
600
+ | `SEO_SITEMAP_ROUTE_MISSING` | RED | An indexable route has no XML sitemap row | A `src/seo/routes.d/<page>.json` fragment |
601
+ | `SEO_ROUTE_UNREACHABLE` | RED | A changed route does not answer 200 | `TailServerLogs`: usually an import of a deleted file or missing export |
602
+ | `SEO_DUPLICATE_LOCALE_HEADER` | RED | Two `Content-Language` values on one response | Delete the duplicate emitter |
603
+ | `SEO_SCHEMA_BLOGPOSTING_INVALID` | RED | A blog post's node is missing, doubled or unreadable | One `blogPostingJsonLd(...)` node |
604
+ | `SEO_H1_MISSING` | WARN | No `<h1>` | Exactly one `<h1>` |
605
+ | `SEO_H1_DUPLICATE` | WARN | More than one `<h1>` | Exactly one `<h1>` |
606
+ | `SEO_IMG_ALT_MISSING` | WARN | An image has no alt text | The alt-text backfill |
607
+ | `SEO_UNMAPPED_PAGE_KIND` | WARN | A page-kind route renders no description of its own | The route-level SEO source mapping |
608
+
609
+ ### 3. Route probes
610
+
611
+ `ProbeRoute` requests one route on the storefront's server without following redirects and reports
612
+ the status and the start of the body. Probe:
613
+
614
+ | Route | Expected |
615
+ | ---------------------------------------------------- | -------------------------------------------------------- |
616
+ | `/` | 200; the `<html` tag carries `lang="en"` |
617
+ | `/de` | 200; `lang="de"` |
618
+ | `/de/products` | 200; `lang="de"` |
619
+ | `/en/products` | 308 — the default language is never prefixed |
620
+ | `/zz/products`, with `zz` not offered | 404 |
621
+ | `/products?lang=de` | 200; `lang="en"` — a query parameter selects no language |
622
+ | Every `from` path in `REDIRECTS`, and its `/de` form | 308 |
623
+ | `/sitemap.xml` | 200; empty on the preview |
624
+ | `/robots.txt` | 200; `Disallow: /` on the preview |
625
+
626
+ `ProbeRoute` does not report headers. `Content-Language` is covered twice: in the source by sweep
627
+ row S19, whose only match is `src/middleware.ts`, and on the wire by the promote gate
628
+ (`SEO_DUPLICATE_LOCALE_HEADER`). The expected wire state: exactly one `Content-Language: de` on
629
+ `/de` and every `/de/…` page, and none on an unprefixed page.
630
+
631
+ ### 4. The per-locale browser round
632
+
633
+ After the promote — the preview serves the promoted tree — `browser_navigate` to every language's
634
+ URL of every route the migration touched: the default language unprefixed, every other language
635
+ under its prefix. For each one, `browser_snapshot` and confirm:
636
+
637
+ - the page shows that language's own copy, not the default language's;
638
+ - `html lang` equals that language;
639
+ - the language switcher's target is the same page in the other language;
640
+ - every internal link stays in the language: `/de/…` on a German page, unprefixed on a
641
+ default-language page.
642
+
643
+ ### 5. The refresh settles
644
+
645
+ After the last promote, call the refresh operation once more with an empty `adopt` list. Without
646
+ an `adopt` list it never overwrites a file that differs from the platform version. Expect:
647
+
648
+ - `rendered` and `adopted` are empty;
649
+ - `conflicts` is exactly your Merged list, plus `src/seo/redirects.ts` when you added redirects;
650
+ - every Adopted-as-is file and `SEO-MIGRATION.md` are in `upToDate`.
651
+
652
+ A file in `conflicts` that is not on your Merged list was never merged: merge it.
653
+
654
+ ### 6. Publish, then check the published deployment
655
+
656
+ Ask the operator to publish the storefront; an agent cannot deploy. On the published deployment:
657
+
658
+ - `robots.txt` names `Sitemap: <origin>/sitemap.xml` and disallows the noindex paths;
659
+ - `/sitemap.xml` lists every indexable route in every language;
660
+ - every indexable page carries an absolute canonical and a complete hreflang set with `x-default`;
661
+ - every page serves the search-console verification `<meta>` tags. Until the published deployment
662
+ serves them, the platform records `SEO_VERIFICATION_PLACEMENT_NOT_SERVED` for the connected
663
+ search console. That is expected and is not a failed verification (`SITE_NOT_VERIFIED`); it
664
+ clears on the platform's next sync after the publish.
665
+
666
+ ### How the platform tests this document
667
+
668
+ The `retrofit` phase, the last phase of the platform's storefront crawler harness
669
+ (`tool/storefront-verify`), follows this document on a storefront built by the real CLI. It turns
670
+ that storefront into an improvised pre-SEO one: ten defects drawn from the deletion list, and no
671
+ `.forgecart/template-manifest.json`, like a provisioned storefront. It runs the refresh with no
672
+ `adopt` list. It then applies a DONE summary in the form of the last section, in this document's
673
+ order: the deletions; one adopting refresh of the merged and adopted files, before any edit to
674
+ them; the re-applied customizations; a rename moved into `src/seo/redirects.ts`; a merchant page
675
+ migrated in place and joined by a `src/seo/routes.d/<page>.json` fragment; the final refresh. Its
676
+ 49 assertions run in three stages:
677
+
678
+ - `migrated` (5) reads the tree and the three refresh reports. The first refresh renders this
679
+ document and passes the applicability test above. The deletion list names every class the
680
+ storefront carried (`retrofit-the-document-names-every-deletion-class`). No improvisation
681
+ survives beside its replacement. The final refresh settles exactly as step 5 expects
682
+ (`retrofit-the-final-refresh-is-settled`).
683
+ - `served` (43) serves the migrated storefront with one `next dev` boot. There is one
684
+ `Content-Language` emitter, the migrated page stays in its language, `?lang=` selects nothing,
685
+ the moved rename answers 308 in every language, and the XML sitemap lists the migrated page with
686
+ the hreflang set the page emits. Every re-applied customization still acts, and the crawler's 37
687
+ locale and SEO assertions pass again, each as `retrofit:<id>`.
688
+ - `restored` (1) proves that the harness's workspace is back byte for byte, whether the phase
689
+ passed or failed (`retrofit-the-workspace-is-restored-byte-for-byte`).
690
+
691
+ It is a platform test: your workspace does not contain it, and nothing here asks you to run it.
692
+
693
+ ## The DONE summary lists the deleted files
694
+
695
+ Report the migration in exactly this form:
696
+
697
+ ```text
698
+ SEO migration — DONE
699
+ Deleted:
700
+ - <every removed path, one per line>
701
+ Merged (platform version taken, customizations re-applied):
702
+ - <path> — <the customizations re-applied>
703
+ Adopted as-is:
704
+ - <path>
705
+ Redirects added to src/seo/redirects.ts:
706
+ - /old-path → /new-path
707
+ Routes joined the XML sitemap:
708
+ - src/seo/routes.d/<page>.json — /<page>, indexable: true|false
709
+ Remaining WARNs:
710
+ - <CODE> at <url> — <why it remains>
711
+ ```
712
+
713
+ - The summary lists every deleted file: `Deleted:` names every path the migration removed, and
714
+ nothing else. `src/middleware.ts` and every other platform file belong under Merged or Adopted
715
+ as-is, never under `Deleted:`.
716
+ - A summary whose `Deleted:` list is empty has replaced nothing: either the sweep found no
717
+ improvised code — then say so and quote it — or the migration is not done.
718
+ - Write "none" under a heading that has no entries. Never leave a heading out.