@tokenoftrust/storefront-runner 2.0.40 → 2.0.42

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 (150) hide show
  1. package/apps/storefront/integrations/lifecycle-host-asset.mjs +20 -6
  2. package/apps/storefront/package.json +1 -0
  3. package/apps/storefront/platform-reconcile-tenants.json +3 -0
  4. package/apps/storefront/public/favicon.ico +0 -0
  5. package/apps/storefront/public/platform/google-measurement/v2/entry.mjs +1 -1
  6. package/apps/storefront/public/shared/commerce-chrome.css +147 -16
  7. package/apps/storefront/src/components/AsyncStylesheet.astro +26 -0
  8. package/apps/storefront/src/components/Breadcrumbs.astro +8 -1
  9. package/apps/storefront/src/components/Img.astro +6 -5
  10. package/apps/storefront/src/components/ProductCard.astro +6 -4
  11. package/apps/storefront/src/components/Seo.astro +2 -1
  12. package/apps/storefront/src/components/chrome/SiteFooter.astro +45 -11
  13. package/apps/storefront/src/components/chrome/SiteHeader.astro +83 -13
  14. package/apps/storefront/src/components/commerce/RatingStars.astro +1 -1
  15. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +2 -2
  16. package/apps/storefront/src/components/content/PromoTiles.astro +107 -0
  17. package/apps/storefront/src/components/content/TrustStrip.astro +30 -14
  18. package/apps/storefront/src/components/home/Hero.astro +156 -4
  19. package/apps/storefront/src/components/islands/AgeGate.tsx +135 -26
  20. package/apps/storefront/src/components/islands/ImageGallery.tsx +6 -9
  21. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +194 -46
  22. package/apps/storefront/src/components/islands/SearchTypeahead.tsx +10 -4
  23. package/apps/storefront/src/components/marketing/Faq.astro +9 -2
  24. package/apps/storefront/src/config/devTenantSeed.ts +1 -1
  25. package/apps/storefront/src/config/kvTenantResolver.ts +7 -1
  26. package/apps/storefront/src/config/publicationSpaceConfig.ts +12 -1
  27. package/apps/storefront/src/config/publicationSpaceRegistry.ts +5 -0
  28. package/apps/storefront/src/config/tenants.ts +22 -0
  29. package/apps/storefront/src/layouts/Layout.astro +69 -44
  30. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +24 -8
  31. package/apps/storefront/src/lib/activity/recordActivity.ts +15 -0
  32. package/apps/storefront/src/lib/asyncCss.ts +23 -0
  33. package/apps/storefront/src/lib/auth/loginGate.ts +22 -11
  34. package/apps/storefront/src/lib/basePath.ts +70 -9
  35. package/apps/storefront/src/lib/chrome/managedCart.ts +50 -0
  36. package/apps/storefront/src/lib/chrome/model.ts +4 -0
  37. package/apps/storefront/src/lib/chrome/newsletterAgeAttestation.ts +9 -0
  38. package/apps/storefront/src/lib/content/trustStrip.ts +38 -3
  39. package/apps/storefront/src/lib/cspStaticHashes.ts +4 -1
  40. package/apps/storefront/src/lib/diag/capture.ts +161 -0
  41. package/apps/storefront/src/lib/diag/consoleLine.ts +76 -0
  42. package/apps/storefront/src/lib/diag/debugConsole.ts +118 -0
  43. package/apps/storefront/src/lib/diag/debugMode.ts +163 -0
  44. package/apps/storefront/src/lib/diag/debugWindow.ts +111 -0
  45. package/apps/storefront/src/lib/diag/reference.ts +148 -0
  46. package/apps/storefront/src/lib/diag/referenceStore.ts +113 -0
  47. package/apps/storefront/src/lib/diag/report.ts +109 -0
  48. package/apps/storefront/src/lib/diag/session.ts +93 -0
  49. package/apps/storefront/src/lib/featuredProductsPresentation.ts +5 -0
  50. package/apps/storefront/src/lib/heroSlideshow.ts +41 -0
  51. package/apps/storefront/src/lib/homeVisualParity.ts +107 -0
  52. package/apps/storefront/src/lib/imageAttrs.ts +9 -6
  53. package/apps/storefront/src/lib/integration/nonceStamp.ts +23 -1
  54. package/apps/storefront/src/lib/integration/renderIntegrationState.ts +36 -0
  55. package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +17 -12
  56. package/apps/storefront/src/lib/jsonScript.ts +13 -0
  57. package/apps/storefront/src/lib/pinnedRouteRewriteBoundary.ts +31 -0
  58. package/apps/storefront/src/lib/previewReload.ts +53 -9
  59. package/apps/storefront/src/lib/rawChrome.ts +148 -19
  60. package/apps/storefront/src/lib/rawMarketingHtml.ts +120 -7
  61. package/apps/storefront/src/lib/storyblok/content-model.ts +46 -3
  62. package/apps/storefront/src/lib/styleGuideThemes.ts +107 -0
  63. package/apps/storefront/src/lib/tenantRouting.ts +15 -4
  64. package/apps/storefront/src/lib/tot/syncState.ts +21 -0
  65. package/apps/storefront/src/lib/tracking/publishSafetyNet.ts +29 -3
  66. package/apps/storefront/src/middleware/controlPlane.ts +9 -5
  67. package/apps/storefront/src/middleware/index.ts +13 -1
  68. package/apps/storefront/src/middleware/renderChain.ts +54 -11
  69. package/apps/storefront/src/middleware/routeBranch.ts +58 -12
  70. package/apps/storefront/src/middleware/staticBundle.ts +37 -3
  71. package/apps/storefront/src/pages/[...slug].astro +6 -2
  72. package/apps/storefront/src/pages/api/request-access.ts +5 -4
  73. package/apps/storefront/src/pages/blog/[slug].astro +11 -1
  74. package/apps/storefront/src/pages/cart.ts +23 -0
  75. package/apps/storefront/src/pages/index.astro +64 -85
  76. package/apps/storefront/src/pages/products/[handle].astro +8 -6
  77. package/apps/storefront/src/pages/sitemap.xml.ts +8 -2
  78. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +14 -25
  79. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +10 -17
  80. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +9 -16
  81. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +9 -15
  82. package/apps/storefront/src/pages/the-build/[slug].astro +7 -1
  83. package/apps/storefront/src/styles/fonts.css +7 -0
  84. package/apps/storefront/src/styles/global.css +397 -3
  85. package/package.json +4 -1
  86. package/packages/cli/src/visual-parity.mjs +321 -0
  87. package/packages/public-runtime/package.json +2 -0
  88. package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +1 -1
  89. package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +1 -1
  90. package/packages/public-runtime/src/chrome.ts +44 -7
  91. package/packages/public-runtime/src/csp.ts +12 -1
  92. package/packages/public-runtime/src/extension-contract-values.d.mts +19 -0
  93. package/packages/public-runtime/src/extension-contract-values.mjs +62 -0
  94. package/packages/public-runtime/src/extension-contract.ts +20 -6
  95. package/packages/public-runtime/src/index.ts +8 -0
  96. package/packages/public-runtime/src/integration/consent-broker.ts +10 -7
  97. package/packages/public-runtime/src/integration/consent-config.ts +9 -0
  98. package/packages/public-runtime/src/integration/contact-sales-action-source.ts +56 -10
  99. package/packages/public-runtime/src/integration/csp-compiler.ts +6 -3
  100. package/packages/public-runtime/src/integration/governed-event-properties.ts +62 -0
  101. package/packages/public-runtime/src/integration/index.ts +1 -0
  102. package/packages/public-runtime/src/integration/lifecycle-boot.ts +34 -6
  103. package/packages/public-runtime/src/integration/lifecycle-host-asset.d.mts +2 -0
  104. package/packages/public-runtime/src/integration/lifecycle-host-asset.mjs +17 -3
  105. package/packages/public-runtime/src/integration/native-amplitude.ts +12 -3
  106. package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.d.mts +2 -0
  107. package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.mjs +35 -1
  108. package/packages/public-runtime/src/integration/native-ga4-sandbox-entry.ts +45 -12
  109. package/packages/public-runtime/src/integration/native-ga4-sandbox.ts +28 -7
  110. package/packages/public-runtime/src/integration/native-ga4.ts +18 -2
  111. package/packages/public-runtime/src/integration/platform-adapters.ts +14 -3
  112. package/packages/public-runtime/src/integration/render-phases.ts +28 -2
  113. package/packages/public-runtime/src/publication-redirects.d.mts +45 -0
  114. package/packages/public-runtime/src/publication-redirects.mjs +379 -0
  115. package/packages/public-runtime/src/publication-space.ts +24 -3
  116. package/packages/public-runtime/src/static-bundle.ts +145 -26
  117. package/packages/public-runtime/src/tenant-embed-defaults.d.mts +3 -0
  118. package/packages/public-runtime/src/tenant-embed-defaults.mjs +40 -0
  119. package/packages/public-runtime/src/tenant.ts +59 -1
  120. package/pnpm-lock.runner.yaml +12 -0
  121. package/pnpm-workspace.yaml +4 -0
  122. package/scripts/dev/git-status.mjs +1 -1
  123. package/scripts/dev/publish.mjs +6 -0
  124. package/scripts/lib/tenant-asset-copy.mjs +4 -2
  125. package/scripts/tenant/materialize-tenant-content.mjs +1 -0
  126. package/scripts/tenant/validate.mjs +171 -8
  127. package/apps/storefront/src/lib/cfImage.ts +0 -55
  128. package/apps/storefront/src/workers/pruneRollupWorker.ts +0 -120
  129. package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
  130. package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
  131. package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
  132. package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
  133. package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
  134. package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
  135. package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  136. package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  137. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  138. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  139. package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
  140. package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
  141. package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
  142. package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
  143. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
  144. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
  145. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
  146. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
  147. package/tenants/home/public/js/storefront.js +0 -129
  148. package/tenants/home/public/logo-wordmark.png +0 -0
  149. package/tenants/home/public/pages/storefront.css +0 -235
  150. package/tenants/home/theme.json +0 -6
@@ -9,9 +9,13 @@
9
9
  import Layout from "@/layouts/Layout.astro";
10
10
  import Section from "@/components/Section.astro";
11
11
  import Hero from "@/components/home/Hero.astro";
12
- import ProductCard from "@/components/ProductCard.astro";
12
+ import ProductCarousel from "@/components/commerce/ProductCarousel.astro";
13
+ import ProductGrid from "@/components/commerce/ProductGrid.astro";
13
14
  import CollectionCard from "@/components/CollectionCard.astro";
14
15
  import Img from "@/components/Img.astro";
16
+ import PromoTiles from "@/components/content/PromoTiles.astro";
17
+ import TrustStrip from "@/components/content/TrustStrip.astro";
18
+ import Faq from "@/components/marketing/Faq.astro";
15
19
  import MarketingBlocks from "@/components/marketing/MarketingBlocks.astro";
16
20
  import TierCards from "@/components/membership/TierCards.astro";
17
21
  import { withBase } from "@/lib/basePath";
@@ -23,10 +27,6 @@ import type { CatalogCollection, CatalogProduct } from "@tot/public-runtime";
23
27
  import {
24
28
  isMarketingBlock,
25
29
  type HeroBlock,
26
- type EditorialBlock,
27
- type NewsletterBlock,
28
- type PromoTilesBlock,
29
- type MembershipBandBlock,
30
30
  type MarketingBlock,
31
31
  type FeaturedProductsBlock,
32
32
  } from "@/lib/storyblok/content-model";
@@ -35,8 +35,14 @@ import {
35
35
  normalizeProductSectionSource,
36
36
  } from "@tot/public-runtime";
37
37
  import { resolveSectionSource } from "@/lib/sections";
38
+ import { featuredProductsPresentationView } from "@/lib/featuredProductsPresentation";
39
+ import {
40
+ editorialMediaView,
41
+ resolveCommerceStaticBlock,
42
+ type CommerceStaticResolved,
43
+ } from "@/lib/homeVisualParity";
38
44
 
39
- const { tenant, tot, content, basePath, features } = Astro.locals;
45
+ const { tenant, tot, content, basePath, canonicalBase, features } = Astro.locals;
40
46
  const marketing = !features.catalog;
41
47
 
42
48
  // Raw-HTML home (vendor-injected): serve the tenant's own self-contained
@@ -64,6 +70,7 @@ if (rawHtml && rawHtmlPolicy(tenant).allowed) {
64
70
  chromeWrapper,
65
71
  plan: Astro.locals.integrationPlan,
66
72
  consentSnapshot: Astro.locals.consentSnapshot,
73
+ canonicalUrl: new URL("/", canonicalBase + "/").toString(),
67
74
  });
68
75
  }
69
76
 
@@ -79,13 +86,10 @@ const collections = marketing ? [] : await tot.listCollections();
79
86
 
80
87
  // --- Resolve each block into a typed, render-ready view-model -----------------
81
88
  type Resolved =
82
- | { kind: "hero"; block: HeroBlock; product: CatalogProduct | null }
83
- | { kind: "promo_tiles"; title?: string; tiles: PromoTilesBlock["tiles"] }
89
+ | { kind: "hero"; block: HeroBlock; product: CatalogProduct | null; instanceKey: string }
84
90
  | { kind: "collections"; title?: string; cols: CatalogCollection[] }
85
- | { kind: "products"; title?: string; products: CatalogProduct[] }
86
- | { kind: "membership_band"; block: MembershipBandBlock }
87
- | { kind: "editorial"; block: EditorialBlock }
88
- | { kind: "newsletter"; block: NewsletterBlock };
91
+ | { kind: "products"; title?: string; products: CatalogProduct[]; presentation?: FeaturedProductsBlock["presentation"] }
92
+ | CommerceStaticResolved;
89
93
 
90
94
  async function resolveProducts(
91
95
  source: FeaturedProductsBlock["source"],
@@ -101,6 +105,11 @@ async function resolveProducts(
101
105
 
102
106
  const resolved: Resolved[] = [];
103
107
  for (const block of marketing ? [] : home.blocks) {
108
+ const staticBlock = resolveCommerceStaticBlock(block);
109
+ if (staticBlock) {
110
+ resolved.push(staticBlock);
111
+ continue;
112
+ }
104
113
  switch (block.component) {
105
114
  case "hero": {
106
115
  let product: CatalogProduct | null = null;
@@ -109,13 +118,7 @@ for (const block of marketing ? [] : home.blocks) {
109
118
  const firstHandle = col?.product_handles[0];
110
119
  if (firstHandle) product = await tot.getProductByHandle(firstHandle);
111
120
  }
112
- resolved.push({ kind: "hero", block, product });
113
- break;
114
- }
115
- case "promo_tiles": {
116
- if (block.tiles?.length) {
117
- resolved.push({ kind: "promo_tiles", title: block.title, tiles: block.tiles });
118
- }
121
+ resolved.push({ kind: "hero", block, product, instanceKey: `home-${resolved.length}` });
119
122
  break;
120
123
  }
121
124
  case "featured_collections": {
@@ -134,18 +137,9 @@ for (const block of marketing ? [] : home.blocks) {
134
137
  block.handles,
135
138
  block.limit ?? 4,
136
139
  );
137
- resolved.push({ kind: "products", title: block.title, products });
140
+ resolved.push({ kind: "products", title: block.title, products, presentation: block.presentation });
138
141
  break;
139
142
  }
140
- case "membership_band":
141
- resolved.push({ kind: "membership_band", block });
142
- break;
143
- case "editorial":
144
- resolved.push({ kind: "editorial", block });
145
- break;
146
- case "newsletter":
147
- resolved.push({ kind: "newsletter", block });
148
- break;
149
143
  }
150
144
  }
151
145
 
@@ -180,40 +174,13 @@ const homeSectionWidgets = marketing
180
174
  {
181
175
  !marketing && resolved.map((r) => {
182
176
  if (r.kind === "hero") {
183
- return <Hero block={r.block} featureProduct={r.product} />;
177
+ return <Hero block={r.block} featureProduct={r.product} instanceKey={r.instanceKey} />;
184
178
  }
185
179
  if (r.kind === "promo_tiles") {
186
- return (
187
- <Section title={r.title} eyebrow="Featured" tone="bg">
188
- <div
189
- class:list={[
190
- "grid gap-5",
191
- r.tiles.length >= 3 ? "sm:grid-cols-3" : "sm:grid-cols-2",
192
- ]}
193
- >
194
- {r.tiles.map((t) => (
195
- <a
196
- href={withBase(basePath, t.href)}
197
- class="group relative block aspect-[16/9] overflow-hidden rounded-[var(--radius-lg)] shadow-[var(--shadow-sm)]"
198
- >
199
- {t.image && (
200
- <img
201
- src={t.image}
202
- alt={t.label}
203
- width={1600}
204
- height={900}
205
- loading="lazy"
206
- class="absolute inset-0 h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
207
- />
208
- )}
209
- <span class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 to-transparent p-4 font-display text-lg font-semibold text-white">
210
- {t.label}
211
- </span>
212
- </a>
213
- ))}
214
- </div>
215
- </Section>
216
- );
180
+ return <PromoTiles block={r.block} basePath={basePath} tenantId={tenant.tenant_id} />;
181
+ }
182
+ if (r.kind === "trust_strip") {
183
+ return <TrustStrip items={r.block.items} tenantId={tenant.tenant_id} mobile={r.block.mobile} />;
217
184
  }
218
185
  if (r.kind === "collections") {
219
186
  return (
@@ -227,15 +194,10 @@ const homeSectionWidgets = marketing
227
194
  );
228
195
  }
229
196
  if (r.kind === "products") {
230
- return (
231
- <Section title={r.title} eyebrow="Shop" tone="surface">
232
- <div class="grid grid-cols-2 gap-5 lg:grid-cols-4">
233
- {r.products.map((p, i) => (
234
- <ProductCard product={p} position={i} list={r.title} />
235
- ))}
236
- </div>
237
- </Section>
238
- );
197
+ if (featuredProductsPresentationView(r.presentation) === "carousel") {
198
+ return <ProductCarousel products={r.products} title={r.title} eyebrow="Shop" tone="surface" />;
199
+ }
200
+ return <ProductGrid products={r.products} title={r.title} eyebrow="Shop" tone="surface" />;
239
201
  }
240
202
  if (r.kind === "membership_band") {
241
203
  const b = r.block;
@@ -253,6 +215,7 @@ const homeSectionWidgets = marketing
253
215
  }
254
216
  if (r.kind === "editorial") {
255
217
  const b = r.block;
218
+ const media = editorialMediaView(b, tenant.tenant_id);
256
219
  const right = b.align !== "left";
257
220
  return (
258
221
  <Section tone="bg">
@@ -269,25 +232,38 @@ const homeSectionWidgets = marketing
269
232
  </h2>
270
233
  <div class="rte mt-5 text-[var(--color-muted)]" set:html={b.body_html} />
271
234
  {b.ctaLabel && b.ctaHref && (
272
- <a
273
- href={withBase(basePath, b.ctaHref)}
274
- class="link-accent mt-7 inline-block pb-0.5 font-display text-sm font-medium"
275
- >
276
- {b.ctaLabel} →
277
- </a>
235
+ <div class="mt-7">
236
+ <a
237
+ href={withBase(basePath, b.ctaHref)}
238
+ class="link-accent link-accent--cta font-display text-sm font-medium"
239
+ >
240
+ {b.ctaLabel} →
241
+ </a>
242
+ </div>
278
243
  )}
279
244
  </div>
280
245
  <div data-reveal>
281
246
  <div class="overflow-hidden rounded-[var(--radius-lg)] shadow-[var(--shadow-md)]">
282
- <Img
283
- seed={b.imageSeed ?? b.title}
284
- label={b.title}
285
- alt=""
286
- width={1000}
287
- height={750}
288
- sizes="(min-width:768px) 48vw, 100vw"
289
- class="h-full w-full object-cover"
290
- />
247
+ {media.kind === "image" ? (
248
+ <img
249
+ src={media.src}
250
+ alt={media.alt}
251
+ width="1000"
252
+ height="750"
253
+ loading="lazy"
254
+ class="h-full w-full object-cover"
255
+ />
256
+ ) : (
257
+ <Img
258
+ seed={media.seed}
259
+ label={media.label}
260
+ alt=""
261
+ width={1000}
262
+ height={750}
263
+ sizes="(min-width:768px) 48vw, 100vw"
264
+ class="h-full w-full object-cover"
265
+ />
266
+ )}
291
267
  </div>
292
268
  </div>
293
269
  </div>
@@ -329,6 +305,9 @@ const homeSectionWidgets = marketing
329
305
  </Section>
330
306
  );
331
307
  }
308
+ if (r.kind === "faq") {
309
+ return <Faq block={r.block} />;
310
+ }
332
311
  return null;
333
312
  })
334
313
  }
@@ -460,12 +460,14 @@ const jsonLd = [productJsonLd, breadcrumbLd(crumbs, canonicalBase)];
460
460
  Delivery times, rates, and returns are covered in our policy —
461
461
  including the adult-signature (21+) requirement on every order.
462
462
  </p>
463
- <a
464
- href={withBase(basePath, "/shipping-returns")}
465
- class="link-accent mt-3 inline-block font-medium"
466
- >
467
- Shipping &amp; returns policy
468
- </a>
463
+ <div class="mt-3">
464
+ <a
465
+ href={withBase(basePath, "/shipping-returns")}
466
+ class="link-accent link-accent--cta font-medium"
467
+ >
468
+ Shipping &amp; returns policy
469
+ </a>
470
+ </div>
469
471
  </div>
470
472
  </details>
471
473
  </div>
@@ -13,6 +13,7 @@ import { nonEmptyCollections } from "@/lib/tot/query.js";
13
13
  import { listBlogPosts } from "@/lib/blog/provider.js";
14
14
  import { authorSlug, categorySlug } from "@/lib/blog/collection.js";
15
15
  import { listBuildPosts } from "@/lib/the-build/provider.js";
16
+ import { isIndexableHtml, rawPageCanonicalUrl } from "@/lib/rawMarketingHtml.js";
16
17
 
17
18
  interface SitemapUrl {
18
19
  loc: string;
@@ -58,8 +59,13 @@ export async function GET(context: APIContext): Promise<Response> {
58
59
 
59
60
  // Raw tenant-authored marketing subpages (content/<tenant>/pages-html/*.html).
60
61
  const rawPageSlugs = await content.listPageHtmlSlugs();
61
- for (const slug of rawPageSlugs) {
62
- urls.push({ loc: `${base}/${slug}/` });
62
+ const rawPages = await Promise.all(
63
+ rawPageSlugs.map(async (slug) => ({ slug, html: await content.getPageHtml(slug) })),
64
+ );
65
+ for (const { slug, html } of rawPages) {
66
+ if (html && isIndexableHtml(html)) {
67
+ urls.push({ loc: rawPageCanonicalUrl(canonicalBase, slug) });
68
+ }
63
69
  }
64
70
 
65
71
  const pages = await content.listPages();
@@ -20,7 +20,7 @@ export const prerender = false;
20
20
  // utilities + reference theme-token fallbacks so the (token-styled) components
21
21
  // render here without the app Layout. The `.sg-commerce` wrapper pins the
22
22
  // reference palette so they look coherent regardless of the marketing theme CSS.
23
- import { tenantDirSegment } from "@tot/public-runtime";
23
+ import { loadStyleGuideChrome, loadStyleGuideThemes } from "@/lib/styleGuideThemes";
24
24
  import "@/styles/global.css";
25
25
  import PriceDisplay from "@/components/commerce/PriceDisplay.astro";
26
26
  import RatingStars from "@/components/commerce/RatingStars.astro";
@@ -87,6 +87,7 @@ import type { HeroBlock, MarketingBlock } from "@/lib/storyblok/content-model";
87
87
 
88
88
  const { tenant, theme } = Astro.params;
89
89
  const cspNonce = Astro.locals.cspNonce;
90
+ const favicon = Astro.locals.tenant.theme_tokens.brand?.favicon ?? "/favicon.svg";
90
91
 
91
92
  // --- Sample chrome data (preview-only; no live tenant content) ----------------
92
93
  // Every actionable item declares its tracking intent (`action`) alongside its
@@ -468,20 +469,11 @@ const sampleMarketingBlocks: MarketingBlock[] = [
468
469
  },
469
470
  ];
470
471
 
471
- // Colocated repo-root layout: from src/pages/style-guide/[tenant]/ the repo
472
- // root is six levels up.
473
- const themeModules = import.meta.glob<Record<string, any>>(
474
- [
475
- "../../../../../../tenants/*/content/themes/*.json",
476
- "../../../../../../tenants/e2e/*/content/themes/*.json",
477
- ],
478
- { eager: true, import: "default" },
479
- );
480
- const seg = `${tenantDirSegment(tenant ?? "")}content/themes/`;
481
- const t = Object.entries(themeModules)
482
- .filter(([path]) => path.includes(seg))
483
- .map(([, mod]) => mod)
484
- .find((m) => m.id === theme);
472
+ const t = (await loadStyleGuideThemes(
473
+ tenant ?? "",
474
+ Astro.locals.tenant,
475
+ Astro.locals.content,
476
+ )).find((candidate) => candidate.id === theme) as Record<string, any> | undefined;
485
477
 
486
478
  if (!t) return new Response(null, { status: 404 });
487
479
 
@@ -489,15 +481,11 @@ if (!t) return new Response(null, { status: 404 });
489
481
  // longer ships its own mkt.css (retired at integration) — link the
490
482
  // platform /shared/commerce-chrome.css instead so the marketing-component
491
483
  // samples below still render in-brand.
492
- const chromeModules = import.meta.glob<Record<string, any>>(
493
- [
494
- "../../../../../../tenants/*/content/chrome.json",
495
- "../../../../../../tenants/e2e/*/content/chrome.json",
496
- ],
497
- { eager: true, import: "default" },
484
+ const chromeConfig = await loadStyleGuideChrome(
485
+ tenant ?? "",
486
+ Astro.locals.tenant,
487
+ Astro.locals.content,
498
488
  );
499
- const chromeSeg = `${tenantDirSegment(tenant ?? "")}content/chrome.json`;
500
- const chromeConfig = Object.entries(chromeModules).find(([path]) => path.includes(chromeSeg))?.[1];
501
489
  // Adoption is now "does chrome.json carry the consolidated header/footer
502
490
  // shape" (lib/chrome/model.ts ChromeConfig) — the old `sharedChrome: true`
503
491
  // opt-in flag is retired; a successfully-shaped config IS the adopted signal.
@@ -546,7 +534,8 @@ const componentVarsInline = Object.entries(componentVars)
546
534
  <meta charset="utf-8" />
547
535
  <meta name="viewport" content="width=device-width, initial-scale=1" />
548
536
  <meta name="robots" content="noindex, nofollow" />
549
- <link rel="icon" type="image/png" href={`/tenants/${tenant}/favicon.png`} />
537
+ <link rel="icon" href={favicon} />
538
+ <meta name="description" content={`${t.name} preview style guide and component contract.`} />
550
539
  <title>{t.name} — style guide (preview)</title>
551
540
  {/* The tenant's OWN CSS (external, CSP-clean 'self'): the theme token/base
552
541
  layer under review PLUS the shared marketing composition layer (mkt.css).
@@ -1410,7 +1399,7 @@ const componentVarsInline = Object.entries(componentVars)
1410
1399
  <p class="lede" style="font-size:.95rem;color:var(--color-muted)">Client-hydrated Preact islands exercised with fixture data — no live catalog, cart, or ToT calls.</p>
1411
1400
 
1412
1401
  <p class="form-help" style="margin:18px 0 8px">AgeGate — full-viewport 21+ affirmation (real overlay; dismiss it once to keep browsing the guide, same as a real visitor)</p>
1413
- <AgeGate client:load minAge={21} brand={t.name} />
1402
+ <AgeGate client:load minAge={21} brand={t.name} tenantId={tenant ?? "style-guide"} />
1414
1403
 
1415
1404
  <p class="form-help" style="margin:22px 0 8px">VariantSelector — PDP option picker (radiogroup + live price/SKU/stock region)</p>
1416
1405
  <VariantSelector
@@ -27,23 +27,15 @@ export const prerender = false;
27
27
 
28
28
  const { tenant, theme } = Astro.params;
29
29
  const cspNonce = Astro.locals.cspNonce;
30
+ const favicon = Astro.locals.tenant.theme_tokens.brand?.favicon ?? "/favicon.svg";
30
31
 
31
- // Tenant-local theme descriptors. From src/pages/style-guide/[tenant]/chrome/ the
32
- // repo root is seven levels up.
33
- import { tenantDirSegment } from "@tot/public-runtime";
32
+ import { loadStyleGuideThemes } from "@/lib/styleGuideThemes";
34
33
 
35
- const themeModules = import.meta.glob<Record<string, any>>(
36
- [
37
- "../../../../../../../tenants/*/content/themes/*.json",
38
- "../../../../../../../tenants/e2e/*/content/themes/*.json",
39
- ],
40
- { eager: true, import: "default" },
41
- );
42
- const seg = `${tenantDirSegment(tenant ?? "")}content/themes/`;
43
- const t = Object.entries(themeModules)
44
- .filter(([path]) => path.includes(seg))
45
- .map(([, mod]) => mod)
46
- .find((m) => m.id === theme);
34
+ const t = (await loadStyleGuideThemes(
35
+ tenant ?? "",
36
+ Astro.locals.tenant,
37
+ Astro.locals.content,
38
+ )).find((candidate) => candidate.id === theme) as Record<string, any> | undefined;
47
39
 
48
40
  if (!t) return new Response(null, { status: 404 });
49
41
 
@@ -81,7 +73,8 @@ const componentVarsInline = Object.entries(componentVars)
81
73
  <meta charset="utf-8" />
82
74
  <meta name="viewport" content="width=device-width, initial-scale=1" />
83
75
  <meta name="robots" content="noindex, nofollow" />
84
- <link rel="icon" type="image/png" href={`/tenants/${tenant}/favicon.png`} />
76
+ <link rel="icon" href={favicon} />
77
+ <meta name="description" content={`${t.name} preview shared-chrome contract.`} />
85
78
  <title>{t.name} — shared chrome (preview)</title>
86
79
  {/* The theme token sheet (fonts + any :root tokens) + the shared chrome sheet.
87
80
  NO tenant mkt.css — the chrome renders purely from the platform stylesheet. */}
@@ -146,7 +139,7 @@ const componentVarsInline = Object.entries(componentVars)
146
139
  </div>
147
140
  <div class="header-actions">
148
141
  <a href="#" aria-label="Account"><svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"></circle><path d="M4 21c0-4 4-6 8-6s8 2 8 6"></path></svg></a>
149
- <a class="cart" href="#" aria-label="Cart"><svg viewBox="0 0 24 24"><path d="M3 3h2l2 13h11l2-8H7"></path></svg><span class="count">2</span></a>
142
+ <a class="cart" href="#" aria-label="Cart, 2 items"><svg viewBox="0 0 24 24"><path d="M3 3h2l2 13h11l2-8H7"></path></svg><span class="count">2</span></a>
150
143
  </div>
151
144
  <button class="hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
152
145
  </div>
@@ -17,28 +17,20 @@
17
17
  */
18
18
  export const prerender = false;
19
19
 
20
- import { tenantDirSegment } from "@tot/public-runtime";
21
20
  import "@/styles/global.css";
22
21
  import type { MarkdownInstance } from "astro";
23
22
  import StyleGuideNav from "@/components/style-guide/StyleGuideNav.astro";
23
+ import { loadStyleGuideThemes } from "@/lib/styleGuideThemes";
24
24
 
25
25
  const { tenant, theme } = Astro.params;
26
26
  const cspNonce = Astro.locals.cspNonce;
27
+ const favicon = Astro.locals.tenant.theme_tokens.brand?.favicon ?? "/favicon.svg";
27
28
 
28
- // Tenant-local theme descriptors. From src/pages/style-guide/[tenant]/guide/ the
29
- // repo root is seven levels up (same depth as the chrome guide).
30
- const themeModules = import.meta.glob<Record<string, any>>(
31
- [
32
- "../../../../../../../tenants/*/content/themes/*.json",
33
- "../../../../../../../tenants/e2e/*/content/themes/*.json",
34
- ],
35
- { eager: true, import: "default" },
36
- );
37
- const seg = `${tenantDirSegment(tenant ?? "")}content/themes/`;
38
- const t = Object.entries(themeModules)
39
- .filter(([path]) => path.includes(seg))
40
- .map(([, mod]) => mod)
41
- .find((m) => m.id === theme);
29
+ const t = (await loadStyleGuideThemes(
30
+ tenant ?? "",
31
+ Astro.locals.tenant,
32
+ Astro.locals.content,
33
+ )).find((candidate) => candidate.id === theme) as Record<string, any> | undefined;
42
34
 
43
35
  if (!t) return new Response(null, { status: 404 });
44
36
 
@@ -82,7 +74,8 @@ const componentVarsInline = Object.entries(componentVars)
82
74
  <meta charset="utf-8" />
83
75
  <meta name="viewport" content="width=device-width, initial-scale=1" />
84
76
  <meta name="robots" content="noindex, nofollow" />
85
- <link rel="icon" type="image/png" href={`/tenants/${tenant}/favicon.png`} />
77
+ <link rel="icon" href={favicon} />
78
+ <meta name="description" content={`${t.name} preview widget-library contract.`} />
86
79
  <title>Widget library guide — {t.name} (preview)</title>
87
80
  {/* The tenant's theme sheet so the prose reads under real brand tokens. */}
88
81
  <link rel="stylesheet" href={t.stylesheet} />
@@ -16,31 +16,24 @@
16
16
  */
17
17
  export const prerender = false;
18
18
 
19
- import { tenantDirSegment } from "@tot/public-runtime";
20
19
  import StyleGuideNav from "@/components/style-guide/StyleGuideNav.astro";
20
+ import { loadStyleGuideThemes } from "@/lib/styleGuideThemes";
21
21
 
22
22
  const { tenant } = Astro.params;
23
23
  const cspNonce = Astro.locals.cspNonce;
24
+ const favicon = Astro.locals.tenant.theme_tokens.brand?.favicon ?? "/favicon.svg";
24
25
 
25
- // Tenant-local theme descriptors. Colocated repo-root layout: from
26
- // src/pages/style-guide/[tenant]/ the repo root is six levels up.
27
- const themeModules = import.meta.glob<Record<string, any>>(
28
- [
29
- "../../../../../../tenants/*/content/themes/*.json",
30
- "../../../../../../tenants/e2e/*/content/themes/*.json",
31
- ],
32
- { eager: true, import: "default" },
26
+ const themes = await loadStyleGuideThemes(
27
+ tenant ?? "",
28
+ Astro.locals.tenant,
29
+ Astro.locals.content,
33
30
  );
34
- const seg = `${tenantDirSegment(tenant ?? "")}content/themes/`;
35
- const themes = Object.entries(themeModules)
36
- .filter(([path]) => path.includes(seg))
37
- .map(([, mod]) => mod);
38
31
 
39
32
  if (themes.length === 0) return new Response(null, { status: 404 });
40
33
 
41
34
  const ROLE_ORDER: Record<string, number> = { current: 0, candidate: 1, legacy: 2 };
42
35
  themes.sort(
43
- (a, b) => (ROLE_ORDER[a.role] ?? 9) - (ROLE_ORDER[b.role] ?? 9),
36
+ (a, b) => (ROLE_ORDER[a.role ?? ""] ?? 9) - (ROLE_ORDER[b.role ?? ""] ?? 9),
44
37
  );
45
38
  // The current (or first) theme skins the persistent sidebar's per-theme links.
46
39
  const navTheme = themes[0]?.id;
@@ -52,7 +45,8 @@ const navTheme = themes[0]?.id;
52
45
  <meta charset="utf-8" />
53
46
  <meta name="viewport" content="width=device-width, initial-scale=1" />
54
47
  <meta name="robots" content="noindex, nofollow" />
55
- <link rel="icon" type="image/png" href={`/tenants/${tenant}/favicon.png`} />
48
+ <link rel="icon" href={favicon} />
49
+ <meta name="description" content={`${tenant} preview theme style guides.`} />
56
50
  <title>Style guides — {tenant} (preview)</title>
57
51
  <style nonce={cspNonce}>
58
52
  :root { color-scheme: light; }
@@ -11,12 +11,18 @@ import { getTranslations } from "@/lib/blog/collection.js";
11
11
  import { buildAlternates } from "@/lib/seo/alternates.js";
12
12
  import { breadcrumbLd } from "@/lib/jsonld.js";
13
13
 
14
+ // Same contract as blog/[slug].astro: the publication composition lifts this file into the space's
15
+ // `routes.aliases`, so these redirects reach the CDN plane through the live pointer. The redirect
16
+ // below is the pointer-served plane's implementation of those same declared redirects.
17
+ export const MATERIALIZED_REDIRECT_SOURCE = "content/the-build/redirects.json";
18
+
14
19
  const { tenant, canonicalBase, basePath, content, publicationTimestamp } = Astro.locals;
15
20
  const slug = Astro.params.slug ?? "";
16
21
  const post = await getBuildPost(tenant.tenant_id, slug, { content, now: publicationTimestamp });
17
22
  if (!post) {
18
23
  const redirect = await resolveBuildRedirect(tenant.tenant_id, slug, content);
19
- if (redirect) return Astro.redirect(`${basePath}/the-build/${redirect}/`, 301);
24
+ // 308, matching the `redirect-308` alias contract the published plane serves.
25
+ if (redirect) return Astro.redirect(`${basePath}/the-build/${redirect}/`, 308);
20
26
  return new Response(null, { status: 404 });
21
27
  }
22
28
 
@@ -17,6 +17,7 @@
17
17
  * sample-store-3.com: Montserrat 600/700/800 (display), Inter 400/500/600 (body)
18
18
  * tokenoftrust.com: Plus Jakarta Sans 500/600/700/800 (display),
19
19
  * Inter 400/500/600 (body), JetBrains Mono 400/500 (mono)
20
+ * vapenicotinefree.com: Anuphan 400/500/600/700 (display + body)
20
21
  */
21
22
 
22
23
  /* Space Grotesk — reference display */
@@ -52,3 +53,9 @@
52
53
  @import "@fontsource/jetbrains-mono/400.css";
53
54
  @import "@fontsource/jetbrains-mono/500.css";
54
55
  @import "@fontsource/jetbrains-mono/600.css";
56
+
57
+ /* Anuphan — Vape Nicotine Free display + body */
58
+ @import "@fontsource/anuphan/400.css";
59
+ @import "@fontsource/anuphan/500.css";
60
+ @import "@fontsource/anuphan/600.css";
61
+ @import "@fontsource/anuphan/700.css";