@thorprovider/storefront 0.3.1 → 0.4.0-mvp.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 (179) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +248 -99
  3. package/assets/templates/app/app/[[...slug]]/page.tsx +13 -2
  4. package/assets/templates/app/app/layout.tsx +18 -4
  5. package/assets/templates/app/app/robots.ts +13 -0
  6. package/assets/templates/app/app/sitemap.ts +12 -0
  7. package/assets/templates/app/lib/__STOREFRONT__/actions.ts +542 -0
  8. package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
  9. package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +40 -57
  10. package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +608 -0
  11. package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +165 -0
  12. package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
  13. package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
  14. package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
  15. package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +232 -51
  16. package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -3
  17. package/assets/templates/app/lib/__STOREFRONT__/schema.ts +121 -0
  18. package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
  19. package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
  20. package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +96 -0
  21. package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +34 -0
  22. package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +100 -11
  23. package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
  24. package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +26 -0
  25. package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
  26. package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
  27. package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +142 -0
  28. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
  29. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
  30. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
  31. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
  32. package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +17 -0
  33. package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
  34. package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
  35. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
  36. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
  37. package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
  38. package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
  39. package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
  40. package/assets/templates/features/checkout/handlers.ts +153 -0
  41. package/assets/templates/features/checkout/state.ts +61 -0
  42. package/assets/templates/features/customer-auth/handlers.ts +59 -0
  43. package/assets/templates/features/customer-auth/state.ts +36 -0
  44. package/assets/templates/features/recommendations/state.ts +11 -0
  45. package/assets/templates/views/account/content.ts +35 -0
  46. package/assets/templates/views/account/index.tsx +114 -0
  47. package/assets/templates/views/account/registry.tsx +378 -0
  48. package/assets/templates/views/account/schema.ts +52 -0
  49. package/assets/templates/views/category/assembly/assembly.json +28 -0
  50. package/assets/templates/views/category/assembly/index.skeleton.tsx +193 -0
  51. package/assets/templates/views/category/assembly/registry.skeleton.tsx +33 -0
  52. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
  53. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
  54. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
  55. package/assets/templates/views/category/content.ts +30 -0
  56. package/assets/templates/views/category/data.ts +12 -0
  57. package/assets/templates/views/category/index.tsx +230 -0
  58. package/assets/templates/views/category/props.ts +16 -0
  59. package/assets/templates/views/category/registry.tsx +103 -0
  60. package/assets/templates/views/category/schema.ts +78 -0
  61. package/assets/templates/views/checkout/content.ts +105 -0
  62. package/assets/templates/views/checkout/data.ts +23 -0
  63. package/assets/templates/views/checkout/index.tsx +111 -0
  64. package/assets/templates/views/checkout/props.ts +49 -0
  65. package/assets/templates/views/checkout/registry.tsx +217 -0
  66. package/assets/templates/views/checkout/schema.ts +229 -0
  67. package/assets/templates/views/home/assembly/assembly.json +24 -0
  68. package/assets/templates/views/home/assembly/index.skeleton.tsx +72 -0
  69. package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
  70. package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
  71. package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
  72. package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
  73. package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
  74. package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
  75. package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
  76. package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
  77. package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
  78. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
  79. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
  80. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
  81. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
  82. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
  83. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
  84. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
  85. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
  86. package/assets/templates/views/home/content.ts +70 -0
  87. package/assets/templates/views/home/data.ts +15 -0
  88. package/assets/templates/views/home/index.tsx +38 -84
  89. package/assets/templates/views/home/props.ts +22 -0
  90. package/assets/templates/views/home/registry.tsx +38 -22
  91. package/assets/templates/views/home/schema.ts +93 -0
  92. package/assets/templates/views/product/content.ts +25 -0
  93. package/assets/templates/views/product/data.ts +48 -0
  94. package/assets/templates/views/product/index.tsx +104 -0
  95. package/assets/templates/views/product/props.ts +20 -0
  96. package/assets/templates/views/product/registry.tsx +78 -0
  97. package/assets/templates/views/product/schema.ts +108 -0
  98. package/assets/templates/views/static/content.ts +99 -0
  99. package/assets/templates/views/static/index.tsx +60 -0
  100. package/assets/templates/views/static/registry.tsx +66 -0
  101. package/assets/templates/views/static/schema.ts +45 -0
  102. package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/index.tsx +8 -70
  103. package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/registry.tsx +23 -18
  104. package/assets/templates/views/user-panel/schema.ts +79 -0
  105. package/bin/storefront.js +1278 -79
  106. package/commands/sf-add-view.md +44 -13
  107. package/commands/sf-feature.md +28 -0
  108. package/commands/sf-theme.md +14 -8
  109. package/commands/sf-view.md +54 -12
  110. package/package.json +6 -4
  111. package/recipes/archetypes.json +28 -1
  112. package/recipes/catalog.json +361 -0
  113. package/recipes/catalog.mjs +128 -0
  114. package/recipes/catalog.schema.json +64 -0
  115. package/recipes/feature.schema.json +89 -0
  116. package/recipes/features.json +139 -0
  117. package/recipes/manifest.schema.json +69 -0
  118. package/recipes/operations.schema.json +43 -0
  119. package/recipes/recipe.schema.json +11 -2
  120. package/recipes/recipes.json +60 -5
  121. package/recipes/sections.json +24 -5
  122. package/recipes/validate.mjs +784 -32
  123. package/scripts/candidate.mjs +236 -0
  124. package/scripts/composers.mjs +140 -0
  125. package/scripts/file-classes.mjs +116 -0
  126. package/scripts/logical-controls.mjs +61 -0
  127. package/scripts/manifest.mjs +298 -0
  128. package/scripts/operation-registry.mjs +256 -0
  129. package/scripts/operations.mjs +720 -0
  130. package/scripts/ownership-conflict.mjs +356 -0
  131. package/scripts/pinning.mjs +504 -0
  132. package/scripts/plan-apply.mjs +843 -0
  133. package/scripts/preflight.mjs +51 -0
  134. package/scripts/reconcile.mjs +511 -0
  135. package/scripts/render-templates.mjs +245 -28
  136. package/scripts/schema-validate.mjs +132 -0
  137. package/scripts/section-compat.mjs +300 -0
  138. package/scripts/section-operations.mjs +508 -0
  139. package/scripts/sync-assets.mjs +22 -15
  140. package/scripts/view-assembly.mjs +291 -0
  141. package/scripts/view-selection.mjs +211 -0
  142. package/skills/sf-feature/SKILL.md +67 -0
  143. package/skills/sf-theme-gen/SKILL.md +12 -5
  144. package/skills/sf-view-gen/SKILL.md +123 -93
  145. package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -1
  146. package/skills/sf-view-gen/assets/recipes/catalog.json +361 -0
  147. package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
  148. package/skills/sf-view-gen/assets/recipes/features.json +139 -0
  149. package/skills/sf-view-gen/assets/recipes/recipes.json +60 -5
  150. package/skills/sf-view-gen/assets/recipes/sections.json +24 -5
  151. package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
  152. package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
  153. package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
  154. package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
  155. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
  156. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
  157. package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
  158. package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
  159. package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
  160. package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
  161. package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
  162. package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
  163. package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
  164. package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
  165. package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
  166. package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
  167. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
  168. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
  169. package/skills/sf-view-gen/references/views.md +44 -10
  170. package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
  171. package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
  172. package/skills/sf-view-gen/scripts/validate.mjs +784 -32
  173. package/skills/storefront-workflow/SKILL.md +116 -0
  174. package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
  175. package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
  176. package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
  177. package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
  178. package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
  179. package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - **Candidate pinning contract (local prerelease `0.4.0-mvp.0`, not published)**:
6
+ the install record separates declared compatibility (Node `>=20.9.0`, Yarn 4,
7
+ the `node-modules` linker, direct dependency ranges including `@thorprovider/types`
8
+ and the CLI itself, and `cliVersion`) from the resolved pin (exact `node_modules`
9
+ versions, the real `yarn.lock` content hash, and the package-owned asset BOM).
10
+ `doctor`/`update` validate the record against the live project and refuse to stamp
11
+ an incomplete or mismatched pin set; `doctor` compares the live declarations,
12
+ resolved versions, lockfile, and installed owned BOM against the record instead of
13
+ the record against itself. Only package-owned suite paths are hashed, so a user's
14
+ own skill/command files are neither stamped nor reported as drift.
15
+ - **Composition manifest (in progress)**: a `storefront.config.json` manifest becomes
16
+ the source of truth for composition (theme, views, features, per-slot intents) and
17
+ the structural files are derived from it. This reuses the existing `recipes/` +
18
+ `archetypes.json` registry and the CLI rather than the LLM-driven view-edit path,
19
+ which is superseded: agents will delegate structural changes to the CLI and edit
20
+ only user-owned files. Coordinated with the in-progress `storefront-automation-commands`
21
+ and `deterministic-sf-init-cli` changes (shared registry and CLI).
22
+ - **Project resolution**: only `create` takes a directory. Every other command resolves
23
+ the storefront from the current directory upward (looking for `storefront.config.json`)
24
+ and errors without writing when none is found.
25
+
3
26
  ## 0.4.0
4
27
 
5
28
  - **Renamed package**: `@thorprovider/create-storefront` → `@thorprovider/storefront`;
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "@thorprovider/storefront"
3
3
  purpose: "Deterministic storefront scaffolder and installable command suite for Thor Commerce"
4
- lastReviewed: "2026-10-01"
4
+ lastReviewed: "2026-10-08"
5
5
  ---
6
6
 
7
7
  # @thorprovider/storefront
@@ -36,13 +36,18 @@ storefront it generates resolves its own dependencies from the registry.
36
36
  | Package manager | Yarn with the `node-modules` linker (for generated projects) |
37
37
  | OpenCode | a version that supports project skills in `.opencode/skills/` |
38
38
  | OS | macOS, Linux, Windows (via WSL) |
39
- | Network | access to the npm registry (the generated project installs its dependencies from it) |
39
+ | Network | access to the npm registry — only for the first scaffold per machine (repeat scaffolds can run offline, see [troubleshooting](#flaky-network-etimedout)) |
40
40
 
41
41
  Generated projects use Yarn with the `node-modules` linker, not Plug'n'Play (PnP).
42
42
  Yarn 4 defaults to PnP, which Next.js 16's Turbopack cannot resolve from, so the
43
43
  scaffold writes a `.yarnrc.yml` setting `nodeLinker: node-modules` before installing
44
44
  dependencies. PnP is not used. You do not need to configure this yourself.
45
45
 
46
+ The same `.yarnrc.yml` hardens installs for unreliable networks: `httpRetry: 10`
47
+ and `networkConcurrency: 8`. The first successful scaffold warms the global Yarn
48
+ cache (`~/.yarn/berry/cache`); repeat scaffolds then download no packages, and
49
+ `YARN_ENABLE_OFFLINE_MODE=1` drops even the metadata requests.
50
+
46
51
  ### Install
47
52
 
48
53
  Run the scaffolder directly from npm — no global install required. `npx` and
@@ -64,10 +69,13 @@ it is where `.opencode/`, the Next.js app, and `lib/<name>/` live. Run the comma
64
69
  from the parent directory you want the project created in.
65
70
 
66
71
  In a terminal, `create` runs an interactive wizard for any input you omit (name,
67
- site name, backend URL, preset — Maia is preselected). Without a terminal, every
68
- required input must be passed as a flag; the command fails fast and writes nothing
69
- rather than guessing. Brand color is never asked by the wizard — apply it later with
70
- `/sf-theme`, or pass `--brand` on the command line.
72
+ site name, backend URL, preset — Maia is preselected). Pressing Enter on an empty
73
+ prompt accepts the suggested default: the storefront name is suggested from the
74
+ target directory basename, and the site name from the Title Case of that name
75
+ (`my-shop` → `My Shop`). Without a terminal, every required input must be passed
76
+ as a flag; the command fails fast and writes nothing rather than guessing. Brand
77
+ color is never asked by the wizard — apply it later with `/sf-theme`, or pass
78
+ `--brand` on the command line.
71
79
 
72
80
  Once the package is installed as a dependency, the shorter binary form is available:
73
81
 
@@ -96,14 +104,15 @@ storefront create my-shop \
96
104
  | --- | --- | --- |
97
105
  | `<dir>` (positional) | target project directory (created if missing) | required |
98
106
  | `--name <name>` | kebab-case storefront name = `lib/<name>/` = `__STOREFRONT__` | prompted in the wizard |
99
- | `--site-name <name>` | human site name (`__SITE_NAME__`) | `<name>` |
107
+ | `--site-name <name>` | human site name (`__SITE_NAME__`) | Title Case of `<name>` (`my-shop` → `My Shop`) |
100
108
  | `--backend-url <url>` | Medusa backend URL | `http://localhost:9000` |
101
109
  | `--preset <preset>` | visual preset: `maia`, `nova`, `vega`, `luma`, `lyra`, `mira` | prompted in the wizard (Maia preselected) |
102
110
  | `--brand <color>` | named brand color overriding preset primary/nav/ring tokens (never prompted) | none |
103
111
  | `--force`, `-f` | overwrite files that already exist | off |
104
112
 
105
- The pipeline runs: preconditions → Next.js base → shadcn base with the preset →
106
- `.yarnrc.yml` (`nodeLinker: node-modules`) → dependency install → template copy +
113
+ The pipeline runs: preconditions → Next.js base (files only, `--skip-install`) →
114
+ `.yarnrc.yml` (`nodeLinker: node-modules`, retry and concurrency hardening) →
115
+ shadcn base with the preset → dependency install → template copy +
107
116
  placeholder substitution → default-page removal → `.env.local` + theme → suite
108
117
  install into `.opencode/`.
109
118
 
@@ -137,19 +146,57 @@ it incrementally from OpenCode:
137
146
 
138
147
  ### Backend configuration
139
148
 
140
- Generated projects expect a Medusa backend. `create` writes a `.env.local` with the
141
- backend URL you passed and blank placeholders for the rest:
149
+ Generated projects resolve an explicit single-store context from the host
150
+ environment before any commerce call. `create` writes a git-ignored `.env.local`
151
+ with the data source and backend URL you passed, a generated cookie secret, and
152
+ blank placeholders for the store identity, plus a committed `.env.example` that
153
+ documents every variable (never a real secret):
142
154
 
143
155
  ```bash
144
- MEDUSA_BACKEND_URL=http://localhost:9000
145
- MEDUSA_REGION_ID=
156
+ # .env.local (git-ignored)
157
+ THOR_DATA_SOURCE=medusa
158
+ MEDUSA_BASE_URL=http://localhost:9000
146
159
  MEDUSA_PUBLISHABLE_KEY=
160
+ MEDUSA_REGION_ID=
161
+ MEDUSA_SALES_CHANNEL_ID=
162
+ THOR_STORE_ID=my-shop
163
+ THOR_CURRENCY_CODE=
164
+ THOR_SESSION_SECRET=<generated per install>
147
165
  ```
148
166
 
149
- Set `MEDUSA_REGION_ID` and `MEDUSA_PUBLISHABLE_KEY` as needed, then restart
150
- `yarn dev`. The storefront shows no catalog data until these are set (see
151
- [Backend not configured](#backend-not-configured)). During local development you can
152
- use the `mock` adapter data source instead of a live backend.
167
+ The store context is **explicit and validated**:
168
+
169
+ - `THOR_STORE_ID` — stable store identity (seeded from the manifest name).
170
+ - `MEDUSA_SALES_CHANNEL_ID` — the sales channel the store is scoped to.
171
+ - `MEDUSA_REGION_ID` — the backend region that supplies currency, countries, and
172
+ shipping.
173
+ - `THOR_CURRENCY_CODE` — ISO 4217 currency, taken from the environment; it is
174
+ **never hardcoded** in the app.
175
+ - `THOR_SESSION_SECRET` — host-only cookie signing secret (≥16 chars, 32
176
+ recommended). Generate your own; never commit or log it.
177
+
178
+ `THOR_DATA_SOURCE` selects the mode **explicitly** and is never inferred from a
179
+ failure:
180
+
181
+ - `THOR_DATA_SOURCE=mock` renders labelled demo data (`Datos de demostración`).
182
+ The store context must match the mock fixture: channel
183
+ `mock-sales-channel`, region `mock-region-us` (currency `USD`) or
184
+ `mock-region-eu` (currency `EUR`). The committed `.env.example` ships these.
185
+ - `THOR_DATA_SOURCE=medusa` uses the real backend. A missing provider, invalid
186
+ configuration, or a provider failure is surfaced as a **visible error** — the
187
+ store never silently degrades to demo data. Until `MEDUSA_PUBLISHABLE_KEY` is
188
+ set, the home view shows the error notice instead of a fake catalog. Fill in
189
+ the variables, then restart `yarn dev` to serve real catalog data.
190
+
191
+ The guest flow is backend-authoritative end to end: the signed host cookie
192
+ issues a shopper identity and binds a persistent backend cart; the PDP adds the
193
+ explicitly selected variant; the cart and checkout render the backend's totals;
194
+ checkout applies the one approved manual TEST provider after shipping and
195
+ completes the cart with its native discriminator. An ambiguous completion is
196
+ never retried — the shopper reconciles the committed cart, and the active cart
197
+ is cleared only after a confirmed order. No card data is collected at launch.
198
+
199
+ For offline-only work set `THOR_DATA_SOURCE=mock` on purpose.
153
200
 
154
201
  ### Troubleshooting / Known Issues
155
202
 
@@ -157,54 +204,38 @@ Each entry below follows the same shape: **cause** → **symptom** → **remedy*
157
204
 
158
205
  #### Ancestor `.opencode/`
159
206
 
160
- - **Cause**: the project is created under a directory tree in which an ancestor
161
- directory already contains a `.opencode/`.
162
- - **Symptom**: OpenCode resolves the wrong command set — `/sf-add-view`, `/sf-view`,
163
- and `/sf-theme` run commands from the ancestor's `.opencode/` instead of the ones
164
- this package installed, so you see stale or unrelated behaviour.
165
- - **Remedy**: scaffold in a clean root with no ancestor `.opencode/` (for example a
166
- fresh directory outside the monorepo), or remove/relocate the ancestor `.opencode/`
167
- before opening the project.
207
+ - **Cause**: an ancestor directory already contains a `.opencode/`.
208
+ - **Symptom**: OpenCode resolves the wrong command set (`/sf-add-view`, `/sf-view`, `/sf-theme`), so you see stale or unrelated behaviour.
209
+ - **Remedy**: scaffold in a clean root with no ancestor `.opencode/`, or remove/relocate the ancestor `.opencode/` before opening the project.
168
210
 
169
211
  #### Non-empty target directory
170
212
 
171
- - **Cause**: `create` runs against a directory whose `lib/<name>/` already exists (a
172
- previous scaffold with the same name), or whose root is already a Next.js project.
173
- - **Symptom**: the run stops at the preconditions stage with
174
- `lib/<name>/ already exists — refusing to overwrite`.
175
- - **Remedy**: choose a different `--name`, or remove the existing `lib/<name>/`
176
- directory. To extend an existing storefront instead of creating one, use
177
- `/sf-add-view`. `create-next-app` also refuses to initialize a directory that is
178
- already a Next.js project unless it can reuse it; run `create` in an empty directory.
213
+ - **Cause**: `lib/<name>/` already exists, or the root is already a Next.js project.
214
+ - **Symptom**: the run stops at the preconditions stage with `lib/<name>/ already exists — refusing to overwrite`.
215
+ - **Remedy**: choose a different `--name`, or remove the existing `lib/<name>/`. To extend an existing storefront, use `/sf-add-view`; run `create` in an empty directory.
179
216
 
180
217
  #### Missing or invalid `--preset`
181
218
 
182
- - **Cause**: `--preset` was omitted (in a non-interactive run) or the value is not one
183
- of the recognized presets.
184
- - **Symptom**: the run stops at the input-validation stage with
185
- `--preset must be one of: maia, nova, vega, luma, lyra, mira`.
186
- - **Remedy**: pass `--preset` with one of `maia`, `nova`, `vega`, `luma`, `lyra`,
187
- `mira`. In a terminal the wizard preselects `maia`, so the preset is only required
188
- on the command line for non-interactive runs.
219
+ - **Cause**: `--preset` was omitted (non-interactive) or is not a recognized preset.
220
+ - **Symptom**: the run stops at input validation with `--preset must be one of: maia, nova, vega, luma, lyra, mira`.
221
+ - **Remedy**: pass one of those presets. In a terminal the wizard preselects `maia`, so the flag is only required for non-interactive runs.
189
222
 
190
223
  #### Yarn Plug'n'Play / linker
191
224
 
192
225
  - **Cause**: a generated project installed with Yarn's default PnP linker.
193
- - **Symptom**: Next.js 16's Turbopack fails to resolve the Next.js package
194
- (`Could not find the Next.js package`).
195
- - **Remedy**: this is why `create` writes `.yarnrc.yml` with
196
- `nodeLinker: node-modules` before installing. If you set up the project manually,
197
- add that file and re-run `yarn install` so a real `node_modules/` is materialized.
198
- Do not use PnP.
226
+ - **Symptom**: Next.js 16's Turbopack fails to resolve Next.js (`Could not find the Next.js package`).
227
+ - **Remedy**: `create` writes `.yarnrc.yml` with `nodeLinker: node-modules` before installing; if you set the project up manually, add that file and re-run `yarn install`. Do not use PnP.
228
+
229
+ #### Flaky network (ETIMEDOUT)
230
+
231
+ - **Cause**: an unstable connection stalls registry requests mid-transfer (`RequestError … AggregateError [ETIMEDOUT]`).
232
+ - **Remedy**: the scaffolded `.yarnrc.yml` sets `httpRetry: 10` and `networkConcurrency: 8`; that run warms the global cache (`~/.yarn/berry/cache`), which later scaffolds reuse. For offline re-scaffolding run `YARN_ENABLE_OFFLINE_MODE=1 storefront create <dir> …`; the first scaffold per machine still needs one successful online run.
199
233
 
200
234
  #### Backend not configured
201
235
 
202
- - **Cause**: `MEDUSA_REGION_ID` / `MEDUSA_PUBLISHABLE_KEY` (or a reachable backend) are
203
- not set in `.env.local`.
204
- - **Symptom**: the storefront renders its layout but shows no catalog data.
205
- - **Remedy**: fill in the variables in [Backend configuration](#backend-configuration)
206
- and restart `yarn dev`, or use the `mock` adapter data source during local
207
- development.
236
+ - **Cause**: `THOR_DATA_SOURCE=medusa` but `MEDUSA_BASE_URL`/`MEDUSA_PUBLISHABLE_KEY` are missing, empty, or unreachable.
237
+ - **Symptom**: the storefront renders a visible error notice (`role="alert"`) instead of backend data; real-mode failures never degrade to a fake catalog.
238
+ - **Remedy**: fill in the variables in [Backend configuration](#backend-configuration) and restart `yarn dev`. For offline work set `THOR_DATA_SOURCE=mock` on purpose.
208
239
 
209
240
  ---
210
241
 
@@ -212,13 +243,18 @@ Each entry below follows the same shape: **cause** → **symptom** → **remedy*
212
243
 
213
244
  ```
214
245
  <project>/.opencode/
215
- storefront.json # { version, installedAt } — what generation is installed
246
+ storefront.json # install record: { version, installedAt, pinning }
247
+ # pinning separates declared ranges from the resolved
248
+ # versions, yarn.lock hash, and owned asset BOM
216
249
  commands/
217
250
  sf-add-view.md
218
251
  sf-view.md
252
+ sf-feature.md
219
253
  sf-theme.md
220
254
  skills/
221
- sf-view-gen/ # assets/templates/views/, assets/recipes/, scripts/validate.mjs
255
+ storefront-workflow/ # the four phases + CLI/manifest authority
256
+ sf-view-gen/ # assets/recipes/, scripts/validate.mjs
257
+ sf-feature/
222
258
  sf-theme-gen/ # assets/
223
259
  nextjs-shadcn/
224
260
  shadcn-theming/
@@ -229,18 +265,123 @@ Each entry below follows the same shape: **cause** → **symptom** → **remedy*
229
265
 
230
266
  Each skill carries the templates, recipes, and scripts it uses inside its own
231
267
  directory. Skills address them by relative path, so nothing needs the package to be
232
- located on disk at generation time.
268
+ located on disk at generation time. The feature command is
269
+ [`commands/sf-feature.md`](./commands/sf-feature.md) and the workflow model is
270
+ [`skills/storefront-workflow/SKILL.md`](./skills/storefront-workflow/SKILL.md).
271
+
272
+ ## Workflow: bootstrap → compose → customize → verify
273
+
274
+ The project manifest (`storefront.config.json`) is the structural source of truth;
275
+ the `storefront` CLI is the only writer of derived files; the developer/agent edits
276
+ only user-owned files. Read the installed `storefront-workflow` skill for the full
277
+ model. Every command runs from inside the project (no directory argument).
278
+
279
+ | Phase | Authority | Command |
280
+ | --- | --- | --- |
281
+ | **Bootstrap** | CLI only | `npx @thorprovider/storefront create <dir> --name <name> --preset <preset>` |
282
+ | **Compose** | CLI only (agents choose, CLI writes) | `view add\|remove`, `feature add\|remove`, `theme --mode` |
283
+ | **Customize** | developer/agent | edit user-owned `content.ts`/`props.ts`/`data.ts`/`schema.ts`/`mappers.ts` |
284
+ | **Verify** | CLI + build | `npx @thorprovider/storefront sync`, `doctor`, `yarn build` |
285
+
286
+ **Ownership classes.** *Derived* files (`views/index.ts`, `views/registry.ts`,
287
+ `index.tsx`, `registry.tsx`, `catalog.ts`, `runtime.ts`, `spec/index.ts`,
288
+ `state.ts`, `handlers.ts`, `features/*/adapter.ts`) are regenerated by the CLI and
289
+ never hand-edited. *User-owned* files (content, props, data, local schema, mapper
290
+ bodies) are consumed through thin derived wrappers and survive `sync` and removal.
291
+ *External* files (`app/layout.tsx`, `app/globals.css`) belong to create-next-app +
292
+ shadcn; the CLI only appends idempotent markers.
293
+
294
+ **Curated MVP defaults.** Standalone views `home`, `category`, `product`; the
295
+ `checkout` feature (which owns the `checkout` view — a direct `view add checkout`
296
+ is rejected, use `feature add checkout`). Held post-MVP and rejected before any
297
+ write: `user-panel`, recommendations, and sections. A generated storefront is not a
298
+ complete commerce implementation: the `checkout` feature ships structural wiring
299
+ only, and the purchase flow is owned by the separate `single-tenant-commerce-mvp`
300
+ change.
301
+
302
+ **Prerequisites and pinning.** Node `>=20.9.0`, `node-modules` linker, and the
303
+ canonical Yarn pinned in the generated `package.json` as
304
+ `"packageManager": "yarn@4.5.1"` (Corepack resolves it; the scaffold runs every
305
+ install under that version). The generated project pins an exact compatible
306
+ candidate toolset and installs the local `@thorprovider/*` packages from packed
307
+ tarballs via Yarn `resolutions` (`--packs <dir>`), with no publish and no
308
+ workspace import. No package-manager safety policy is suppressed: the generated
309
+ `.yarnrc.yml` sets no `npmPreapprovedPackages`, and the pinned versions are
310
+ published past Yarn's age gate. `doctor` reports the observed linker/Node/Yarn
311
+ against the declared ranges and exits non-zero on a mismatch instead of a false
312
+ clean stamp.
313
+
314
+ ### Configurable composition (E1)
315
+
316
+ The composition engine is a truthful semantic catalog plus a structured
317
+ `inspect → plan → apply` boundary. Run these inside the project:
318
+
319
+ ```bash
320
+ storefront inspect --json
321
+ storefront plan --operations operations.json [--out plan.json] [--json]
322
+ storefront apply --plan plan.json [--json]
323
+ ```
324
+
325
+ - **`inspect --json`** is read-only and deterministic: `formatVersion: 1` /
326
+ `catalogVersion: 1`, every slot/variant with `executable` or `held` + a reason, and a
327
+ capability catalog with `verified`/`unverified` state. A held or unverified unit is
328
+ never reported as available.
329
+ - **`plan`** validates the whole intended state and emits before/after fingerprints and
330
+ the exact derived-file diff. Without `--out` it writes nothing and prints the plan to
331
+ stdout; with `--out` it writes only a safe plan artifact. `--out` is refused
332
+ (`E_OUT_PATH_REFUSED`) for owned, derived, secret-bearing, or installation paths and
333
+ for the operation input.
334
+ - **`apply`** is the only composition mutation. It applies from the recorded
335
+ before-state, reports a **verified no-op** at the recorded after-state, and rejects
336
+ anything else as stale (`E_STALE_PLAN`); concurrent runs are serialized by a local
337
+ lock. A handled write failure rolls back every touched file. An abrupt process kill is
338
+ **not** crash-atomic — the manifest is written last and `sync` detects and repairs the
339
+ divergence.
340
+
341
+ The operations envelope is `{ "formatVersion": 1, "operations": [ ... ] }` (missing or
342
+ different `formatVersion` → `E_INVALID_ENVELOPE`). Operation names are
343
+ `view.add|remove|configure` and `feature.add|remove|configure`; `view add|remove` and
344
+ `feature add|remove` remain thin aliases over the same engine.
345
+
346
+ **What E1 proves.** The only executable non-default swap is `home.benefits`
347
+ `Feature1 → Feature2` (identical `BenefitsItem[]` data shape, no bridge); every bridged
348
+ variant is `held`. A feature may declare a `config` and `mounts`; the bounded `benefits`
349
+ feature mounts at `home.benefits` with `{ "presentation": "compact" | "detailed" }`,
350
+ claims the slot exactly once, and preserves user-owned content on removal. Checkout
351
+ keeps its existing feature-owned route only — unproven checkout variants or route
352
+ options are rejected (`E_UNPROVEN_CHECKOUT_OPTION`). The category filter's action is
353
+ **held** (`unverified`, owner E2) and its control is omitted rather than exposed as a
354
+ no-op; catalog search/filters (E2) and composable sections (E3) are **not** implemented
355
+ here.
233
356
 
234
357
  ## Commands
235
358
 
236
359
  | Command | Purpose |
237
360
  | --- | --- |
238
- | `/sf-add-view <view>` | Add one view incrementally (`product`, `category`, `checkout`, `user-panel`) |
239
- | `/sf-view <view> <change>` | Modify an existing view's spec, preserving structure |
361
+ | `/sf-add-view <view>` | Add a curated view via the CLI (`product`, `category`; `checkout` via `/sf-feature`) |
362
+ | `/sf-view <view> <change>` | Customize a view's user-owned content/props/data, then `sync` |
363
+ | `/sf-feature add\|remove <name>` | Add/remove a feature bundle (e.g. `checkout`) via the CLI |
240
364
  | `/sf-theme` | Customize OKLCH color tokens (light/dark), typography, and identity metadata |
241
365
 
242
366
  Scaffolding itself is `npx @thorprovider/storefront create <dir>` — there is no
243
- `/sf-init` command; the wizard lives in the CLI and is deterministic.
367
+ `/sf-init` command; the wizard lives in the CLI and is deterministic. The color
368
+ mode is likewise a CLI capability: `npx @thorprovider/storefront theme --mode light|dark|system`.
369
+
370
+ ## Philosophy: determinism first; the LLM only where there is judgment
371
+
372
+ A storefront task is one of three kinds, and the tool follows the kind:
373
+
374
+ - **Deterministic** (output is a pure function of input) → a CLI subcommand with flags
375
+ (e.g. `storefront theme --mode`). Repeatable, reviewable, no model in the loop.
376
+ - **Mixed** → split it: the script applies the deterministic part, the LLM decides the
377
+ judgment part (`/sf-theme` decides the mode; `storefront theme --mode` applies it).
378
+ - **Pure judgment** → an `sf-*` skill (e.g. `/sf-view`): variant selection, copy, and
379
+ layout choices live here.
380
+
381
+ **Test criterion**: if you can assert the result without a model ("after this, the file
382
+ contains X"), it must be a script; if you cannot, it is skill work. LLM edits are
383
+ non-deterministic and expensive to review, so keep the judgment-heavy work where a
384
+ model adds value and make the rest a reliable script.
244
385
 
245
386
  ## Design guarantees
246
387
 
@@ -252,40 +393,59 @@ Scaffolding itself is `npx @thorprovider/storefront create <dir>` — there is n
252
393
  installer copies anything.
253
394
  - **Stack-constrained**: UI comes only from `@thorprovider/blocks`; data only from
254
395
  `@thorprovider/adapters` and `@thorprovider/runtime/commerce`.
255
- - **Varied output**: a recipe registry lets the skills pick block variants and optional
256
- sections (testimonials, FAQ, statistics, logo cloud, pricing) from your description,
257
- so sites are not identical.
396
+ - **Truthful output**: a versioned recipe/catalog exposes every slot and variant with an
397
+ `executable` or `held` status and a reason. Only executable variants render; a held
398
+ variant is rejected before any write, so a site is never presented with a choice that
399
+ silently does nothing. E1 ships a small proven set (`home.benefits` `Feature1`↔
400
+ `Feature2`); bridged variants and optional sections stay held.
258
401
 
259
402
  ## Generated project structure
260
403
 
261
- Each view is a self-contained module. Shared files compose whatever the view barrel
262
- exports and never name a view, so adding one is a copy plus a single registration.
404
+ Each view is a self-contained module. The CLI generates the barrels and wiring
405
+ from the manifest; shared files compose whatever the barrel exports and never name
406
+ a view. Only user-owned files (marked `*`) are hand-edited.
263
407
 
264
408
  ```
265
409
  lib/<name>/
266
- catalog.ts, registry.tsx, runtime.ts, handlers.ts, state.ts # shell; compose the barrel
267
- mappers.ts # CardProduct -> FeaturedProduct / CategoryCard
268
- spec/index.ts, spec/page-root.ts, spec/types.ts # routes built from the barrel
410
+ catalog.ts, registry.tsx, runtime.ts, handlers.ts, state.ts # derived shell; compose the barrel
411
+ mappers.ts * # user-owned: CardProduct -> block props
412
+ shell/
413
+ content.ts * # user-owned: wordmark accents + footer newsletter
414
+ props.ts * # user-owned: shell element props
415
+ spec/index.ts, spec/page-root.ts, spec/types.ts # derived routes built from the barrel
269
416
  views/
270
- index.ts # barrel: export const views: ViewModule[] = [home, ...]
271
- types.ts # ViewModule contract
272
- sections/*.tsx # optional-section fragments (testimonials, faq, ...)
273
- home/index.tsx # one folder per view
274
- product/index.tsx
275
- category/index.tsx
276
- checkout/index.tsx
277
- user-panel/index.tsx
417
+ index.ts # derived server barrel: export const views: ViewModule[] = [home, ...]
418
+ registry.ts # derived client barrel: viewRegistries (no server loaders)
419
+ types.ts # ViewModule / ViewRegistry contract
420
+ home/
421
+ index.tsx # derived thin wrapper (imports ./content, ./props, ./data)
422
+ registry.tsx # derived 'use client' component mappings
423
+ content.ts * # user-owned copy/data
424
+ props.ts * # user-owned element props
425
+ data.ts * # user-owned loader shaping
426
+ schema.ts * # user-owned local Zod mirror
427
+ product/ category/ checkout/ # same shape
278
428
  ```
279
429
 
280
- A view folder ships the package's default block variants; `/sf-add-view` picks the
281
- intent/variant from your description (validated by the skill's `scripts/validate.mjs`)
282
- and substitutes only the imported block identifier. The scaffold templates live at
283
- the package root, and the view templates travel inside the skills that use them:
430
+ The shell's derived `spec/index.ts` imports the user-owned `shell/props.ts`
431
+ (`...navbarShellProps`, `...footerShellProps`) and composes it **last**, so the brand
432
+ surface and footer copy are configured without touching derived files. Configure
433
+ `shell/content.ts` (`logoAccent`/`brandAccent`, `navbarClassName`/`footerClassName`,
434
+ `brandDescription`, `linkColumns`); defaults are neutral and link only to served routes
435
+ (`/`, `/categorias`, `/checkout`), never dead `#` links. `newsletter` defaults to `false`
436
+ and the scaffold ships no submit callback, so a content-only newsletter renders no form
437
+ — never a fake form.
438
+
439
+ A view's derived `index.tsx` imports the user-owned `content.ts`/`props.ts`/
440
+ `data.ts`, so a customization changes real spec/loader output and survives `sync`.
441
+ `npx @thorprovider/storefront view add <view>` chooses the intent/variant from the
442
+ registry, writes the derived files, and regenerates the barrels; the agent edits
443
+ only the user-owned files. The scaffold templates live at the package root:
284
444
 
285
445
  ```
286
- assets/templates/app/ # shell (copied by `create`)
287
- assets/templates/views/home/ # home (copied by `create`)
288
- skills/sf-view-gen/assets/templates/views/<view>/ # the other views (copied by /sf-add-view)
446
+ assets/templates/app/ # shell (generated by `create`)
447
+ assets/templates/views/home/ # home (generated by `create`)
448
+ assets/templates/views/<view>/ # the other views (generated by `storefront view add`)
289
449
  ```
290
450
 
291
451
  ## Development
@@ -301,24 +461,13 @@ node bin/storefront.js doctor <tmp> # report the recorded version
301
461
  node scripts/render-templates.mjs <tmp> my-shop # re-render templates only (no install; fast loop)
302
462
  ```
303
463
 
304
- `scripts/render-templates.mjs` is the same renderer the `create` action uses for
305
- stage 6. It copies the templates and substitutes placeholders without touching
306
- dependencies, so iterating on a template against a persistent project (e.g.
307
- `projects/<name>/`) costs seconds and no network.
308
-
309
- `npm pack` exists to verify a different thing: **what actually ships to npm** —
310
- the `files` list, and that every asset travels inside its skill. It is only
311
- meaningful at these moments:
312
-
313
- | Moment | Action |
314
- | --- | --- |
315
- | Day-to-day development | Nothing — use the local binary above |
316
- | Changing `files`, moving assets, adding or removing directories | `npm pack --dry-run` to list what would ship |
317
- | CI, before publishing | `yarn check-assets` + `npm pack --dry-run` as a guard |
318
- | Publishing a release | `npm pack` / `yarn npm publish` — the real tarball |
319
-
320
- A generated `.tgz` goes stale the moment the tree changes, so it is not a
321
- development tool. Keep it out of the working tree.
464
+ `scripts/render-templates.mjs` is the same renderer `create` uses for stage 6; it copies
465
+ templates and substitutes placeholders without touching dependencies, so iterating on a
466
+ template costs seconds and no network. `npm pack` verifies a different thing — **what
467
+ actually ships to npm** (the `files` list, and that every asset travels inside its
468
+ skill). Use it only when `files`/assets change or before a release (`npm pack --dry-run`
469
+ to list; `yarn check-assets` + `npm pack --dry-run` in CI). A generated `.tgz` goes
470
+ stale the moment the tree changes, so keep it out of the working tree.
322
471
 
323
472
  ## Maintenance
324
473
 
@@ -2,16 +2,27 @@
2
2
 
3
3
  import { notFound } from 'next/navigation'
4
4
  import { __STOREFRONT_CAMEL__App } from '@/lib/__STOREFRONT__/runtime'
5
+ import { readCartStateForRender } from '@/lib/__STOREFRONT__/cart-state'
5
6
  import { StorefrontRenderer } from './renderer'
6
7
 
7
8
  export const { generateMetadata, generateStaticParams } = __STOREFRONT_CAMEL__App
8
9
 
9
10
  export default async function Page({
10
11
  params,
12
+ searchParams,
11
13
  }: {
12
14
  params: Promise<{ slug?: string[] }>
15
+ searchParams: Promise<Record<string, string | string[] | undefined>>
13
16
  }) {
14
- const data = await __STOREFRONT_CAMEL__App.getPageData({ params })
17
+ // The raw URL search params are forwarded to the loader, which validates them
18
+ // through the canonical boundary before any commerce call (E2 task 5.1).
19
+ const data = await __STOREFRONT_CAMEL__App.getPageData({ params, searchParams })
15
20
  if (!data) return notFound()
16
- return <StorefrontRenderer {...data} />
21
+
22
+ // Read the signed cookie and load the backend cart for the initial render.
23
+ // This never creates a cart; a guest identity is issued by a Server Action.
24
+ const cartState = await readCartStateForRender()
25
+ const initialState = { ...(data.initialState ?? {}), ...cartState }
26
+
27
+ return <StorefrontRenderer {...data} initialState={initialState} />
17
28
  }
@@ -2,10 +2,12 @@
2
2
 
3
3
  import type { Metadata } from 'next'
4
4
  import { Geist } from 'next/font/google'
5
+ import { ThemeProvider } from 'next-themes'
5
6
  import { StorefrontProvider } from '@thorprovider/runtime/host-glue/providers'
6
7
  import { themeVarsToCss, themeTokensToVars } from '@thorprovider/runtime/render-core/theme'
7
8
  import { DEFAULT_THEME_TOKENS } from '@thorprovider/runtime/contracts'
8
9
  import { registry } from '@/lib/__STOREFRONT__/registry'
10
+ import { getSiteUrl } from '@/lib/__STOREFRONT__/store-context'
9
11
  import './globals.css'
10
12
 
11
13
  const geist = Geist({
@@ -15,20 +17,32 @@ const geist = Geist({
15
17
 
16
18
  const themeCss = themeVarsToCss(themeTokensToVars(DEFAULT_THEME_TOKENS))
17
19
 
20
+ // The canonical public origin is optional: when THOR_SITE_URL is unset no base
21
+ // URL is invented. `metadataBase` is left undefined in that case.
22
+ const siteUrl = getSiteUrl()
23
+
18
24
  export const metadata: Metadata = {
25
+ metadataBase: siteUrl ? new URL(siteUrl) : undefined,
19
26
  title: '__SITE_NAME__',
20
27
  description: 'Tienda online de __SITE_NAME__.',
28
+ openGraph: {
29
+ title: '__SITE_NAME__',
30
+ description: 'Tienda online de __SITE_NAME__.',
31
+ url: siteUrl,
32
+ },
21
33
  }
22
34
 
23
35
  export default function RootLayout({
24
36
  children,
25
37
  }: Readonly<{ children: React.ReactNode }>) {
26
38
  return (
27
- <html lang="es" className={geist.variable}>
39
+ <html lang="es" className={geist.variable} suppressHydrationWarning>
28
40
  <body className="font-sans antialiased">
29
- <StorefrontProvider themeCss={themeCss} registry={registry}>
30
- {children}
31
- </StorefrontProvider>
41
+ <ThemeProvider attribute="class" defaultTheme="__STOREFRONT_MODE__" enableSystem>
42
+ <StorefrontProvider themeCss={themeCss} registry={registry}>
43
+ {children}
44
+ </StorefrontProvider>
45
+ </ThemeProvider>
32
46
  </body>
33
47
  </html>
34
48
  )
@@ -0,0 +1,13 @@
1
+ // Generated by Thor Commerce storefront (template: assets/templates/app/app/robots.ts)
2
+ // Always served. Points at the sitemap only when THOR_SITE_URL is configured.
3
+
4
+ import type { MetadataRoute } from 'next'
5
+ import { robotsSitemapUrl } from '@/lib/__STOREFRONT__/spec/seo'
6
+ import { getSiteUrl } from '@/lib/__STOREFRONT__/store-context'
7
+
8
+ export default function robots(): MetadataRoute.Robots {
9
+ return {
10
+ rules: { userAgent: '*', allow: '/' },
11
+ sitemap: robotsSitemapUrl(getSiteUrl()),
12
+ }
13
+ }
@@ -0,0 +1,12 @@
1
+ // Generated by Thor Commerce storefront (template: assets/templates/app/app/sitemap.ts)
2
+ // Lists the storefront's fixed routes. Absolute URLs are emitted only when
3
+ // THOR_SITE_URL is configured; otherwise the sitemap is empty (no invented URL).
4
+
5
+ import type { MetadataRoute } from 'next'
6
+ import { __STOREFRONT_CAMEL__Spec } from '@/lib/__STOREFRONT__/spec'
7
+ import { sitemapUrls } from '@/lib/__STOREFRONT__/spec/seo'
8
+ import { getSiteUrl } from '@/lib/__STOREFRONT__/store-context'
9
+
10
+ export default function sitemap(): MetadataRoute.Sitemap {
11
+ return sitemapUrls(__STOREFRONT_CAMEL__Spec.routes ?? {}, getSiteUrl()).map((url) => ({ url }))
12
+ }