@tokenoftrust/storefront-runner 1.3.2
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.
- package/LICENSE +58 -0
- package/README.md +42 -0
- package/apps/storefront/astro.config.mjs +66 -0
- package/apps/storefront/dev-plugins/dev-loop-state.mjs +147 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +238 -0
- package/apps/storefront/env.d.ts +162 -0
- package/apps/storefront/migrations/0000_baseline.sql +54 -0
- package/apps/storefront/migrations/0001_products_fts.sql +9 -0
- package/apps/storefront/migrations/README.md +54 -0
- package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
- package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
- package/apps/storefront/migrations/meta/_journal.json +20 -0
- package/apps/storefront/package.json +42 -0
- package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
- package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
- package/apps/storefront/public/brand/northwind-og.svg +13 -0
- package/apps/storefront/public/favicon.svg +5 -0
- package/apps/storefront/public/js/dashboard-team.js +147 -0
- package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
- package/apps/storefront/public/shared/commerce-chrome.css +369 -0
- package/apps/storefront/src/components/Badge.astro +34 -0
- package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
- package/apps/storefront/src/components/CollectionCard.astro +56 -0
- package/apps/storefront/src/components/EmptyState.astro +55 -0
- package/apps/storefront/src/components/Img.astro +68 -0
- package/apps/storefront/src/components/ProductCard.astro +228 -0
- package/apps/storefront/src/components/Section.astro +39 -0
- package/apps/storefront/src/components/Seo.astro +70 -0
- package/apps/storefront/src/components/WidgetFrame.astro +34 -0
- package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +531 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +597 -0
- package/apps/storefront/src/components/admin/AdminFoxyCartSettingsPanel.astro +227 -0
- package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +465 -0
- package/apps/storefront/src/components/admin/AdminProductsTab.astro +901 -0
- package/apps/storefront/src/components/admin/AdminReportingTab.astro +435 -0
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +782 -0
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +1223 -0
- package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
- package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
- package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +201 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +201 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +234 -0
- package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
- package/apps/storefront/src/components/commerce/ArticleCard.astro +242 -0
- package/apps/storefront/src/components/commerce/BrandGrid.astro +175 -0
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
- package/apps/storefront/src/components/commerce/FoxySidecartStyles.astro +61 -0
- package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
- package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
- package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
- package/apps/storefront/src/components/commerce/ReviewList.astro +54 -0
- package/apps/storefront/src/components/commerce/ReviewSummary.astro +44 -0
- package/apps/storefront/src/components/commerce/TagFilterCloud.astro +149 -0
- package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +37 -0
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +99 -0
- package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +45 -0
- package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
- package/apps/storefront/src/components/compliance/PactActNotice.astro +35 -0
- package/apps/storefront/src/components/compliance/Prop65Warning.astro +80 -0
- package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +38 -0
- package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +35 -0
- package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +53 -0
- package/apps/storefront/src/components/home/Hero.astro +164 -0
- package/apps/storefront/src/components/islands/AgeGate.tsx +109 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
- package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
- package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
- package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
- package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
- package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
- package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
- package/apps/storefront/src/components/islands/VariantSelector.tsx +375 -0
- package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
- package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
- package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
- package/apps/storefront/src/components/marketing/Faq.astro +37 -0
- package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
- package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
- package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
- package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
- package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
- package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
- package/apps/storefront/src/components/marketing/Steps.astro +36 -0
- package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
- package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
- package/apps/storefront/src/components/membership/IllustrativeLabel.astro +56 -0
- package/apps/storefront/src/components/membership/TierCards.astro +164 -0
- package/apps/storefront/src/components/membership/TierComparisonTable.astro +148 -0
- package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
- package/apps/storefront/src/components/plp/FacetSidebar.astro +199 -0
- package/apps/storefront/src/components/plp/Pagination.astro +99 -0
- package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
- package/apps/storefront/src/components/style-guide/StyleGuideNav.astro +95 -0
- package/apps/storefront/src/components/subscription/HowItWorksSteps.astro +67 -0
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +67 -0
- package/apps/storefront/src/components/subscription/SavingsExplainer.astro +101 -0
- package/apps/storefront/src/config/adminTargetTenant.ts +28 -0
- package/apps/storefront/src/config/devTenantSeed.ts +78 -0
- package/apps/storefront/src/config/devTenants.ts +16 -0
- package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
- package/apps/storefront/src/config/resolver.ts +102 -0
- package/apps/storefront/src/config/tenants.ts +87 -0
- package/apps/storefront/src/layouts/Layout.astro +368 -0
- package/apps/storefront/src/lib/admin/README.md +122 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +21 -0
- package/apps/storefront/src/lib/admin/ai/askEnvelope.ts +48 -0
- package/apps/storefront/src/lib/admin/ai/audit.ts +14 -0
- package/apps/storefront/src/lib/admin/ai/index.ts +6 -0
- package/apps/storefront/src/lib/admin/ai/operationalRuns.ts +155 -0
- package/apps/storefront/src/lib/admin/ai/recommendation.ts +20 -0
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +172 -0
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +65 -0
- package/apps/storefront/src/lib/admin/envelope.ts +10 -0
- package/apps/storefront/src/lib/admin/evidence/evidenceReference.ts +32 -0
- package/apps/storefront/src/lib/admin/memoizeByEnvelope.ts +27 -0
- package/apps/storefront/src/lib/admin/orderClient.ts +146 -0
- package/apps/storefront/src/lib/admin/seed.ts +54 -0
- package/apps/storefront/src/lib/admin/services/mock/aiWorkflows.ts +59 -0
- package/apps/storefront/src/lib/admin/services/mock/customers.ts +86 -0
- package/apps/storefront/src/lib/admin/services/mock/evidence.ts +99 -0
- package/apps/storefront/src/lib/admin/services/mock/foxyCartSettings.ts +78 -0
- package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +62 -0
- package/apps/storefront/src/lib/admin/services/mock/index.ts +12 -0
- package/apps/storefront/src/lib/admin/services/mock/orders.ts +184 -0
- package/apps/storefront/src/lib/admin/services/mock/products.ts +105 -0
- package/apps/storefront/src/lib/admin/services/mock/reporting.ts +51 -0
- package/apps/storefront/src/lib/admin/services/mock/settings.ts +70 -0
- package/apps/storefront/src/lib/admin/services/mock/tenants.ts +29 -0
- package/apps/storefront/src/lib/admin/services/orders.ts +300 -0
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +67 -0
- package/apps/storefront/src/lib/admin/viewmodels/customers.ts +56 -0
- package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +49 -0
- package/apps/storefront/src/lib/admin/viewmodels/orders.ts +92 -0
- package/apps/storefront/src/lib/admin/viewmodels/products.ts +74 -0
- package/apps/storefront/src/lib/admin/viewmodels/reporting.ts +34 -0
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +64 -0
- package/apps/storefront/src/lib/adminTenantEnvelope.ts +90 -0
- package/apps/storefront/src/lib/analytics/index.ts +47 -0
- package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
- package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
- package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +119 -0
- package/apps/storefront/src/lib/auth/route.ts +146 -0
- package/apps/storefront/src/lib/auth/session.ts +203 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
- package/apps/storefront/src/lib/basePath.ts +41 -0
- package/apps/storefront/src/lib/cfImage.ts +55 -0
- package/apps/storefront/src/lib/chrome/model.ts +244 -0
- package/apps/storefront/src/lib/chrome/parseConfig.ts +34 -0
- package/apps/storefront/src/lib/colors.ts +97 -0
- package/apps/storefront/src/lib/commerce/foxyCartSettings.ts +253 -0
- package/apps/storefront/src/lib/content-edit/applyDiff.ts +91 -0
- package/apps/storefront/src/lib/content-edit/client.ts +230 -0
- package/apps/storefront/src/lib/content-edit/index.ts +14 -0
- package/apps/storefront/src/lib/content-edit/route.ts +25 -0
- package/apps/storefront/src/lib/content-edit/types.ts +19 -0
- package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
- package/apps/storefront/src/lib/d1/catalog.ts +366 -0
- package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
- package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
- package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
- package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
- package/apps/storefront/src/lib/demoGate.ts +113 -0
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +78 -0
- package/apps/storefront/src/lib/dev/activityStore.ts +72 -0
- package/apps/storefront/src/lib/dev/ai-widget-client.js +308 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +14 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +155 -0
- package/apps/storefront/src/lib/dev/devDraft.ts +133 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +169 -0
- package/apps/storefront/src/lib/dev/livePreview.ts +77 -0
- package/apps/storefront/src/lib/dev/runtimeStore.ts +120 -0
- package/apps/storefront/src/lib/dev/storeStatus.ts +142 -0
- package/apps/storefront/src/lib/diag/redact.ts +20 -0
- package/apps/storefront/src/lib/edgeCache.ts +128 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
- package/apps/storefront/src/lib/env.ts +60 -0
- package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
- package/apps/storefront/src/lib/format.ts +38 -0
- package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
- package/apps/storefront/src/lib/imageAttrs.ts +55 -0
- package/apps/storefront/src/lib/jsonld.ts +166 -0
- package/apps/storefront/src/lib/maintenance.ts +71 -0
- package/apps/storefront/src/lib/membership/model.ts +97 -0
- package/apps/storefront/src/lib/memoryKv.ts +32 -0
- package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
- package/apps/storefront/src/lib/placeholder.ts +99 -0
- package/apps/storefront/src/lib/previewReload.ts +125 -0
- package/apps/storefront/src/lib/priceBuckets.ts +33 -0
- package/apps/storefront/src/lib/rawChrome.ts +505 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +136 -0
- package/apps/storefront/src/lib/search/index.ts +74 -0
- package/apps/storefront/src/lib/sections.ts +129 -0
- package/apps/storefront/src/lib/securityHeaders.ts +31 -0
- package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
- package/apps/storefront/src/lib/storyblok/provider.ts +607 -0
- package/apps/storefront/src/lib/subscription/model.ts +68 -0
- package/apps/storefront/src/lib/tenantRouting.ts +91 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
- package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
- package/apps/storefront/src/lib/tot/query.ts +249 -0
- package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
- package/apps/storefront/src/lib/url.ts +142 -0
- package/apps/storefront/src/lib/wishlist.ts +63 -0
- package/apps/storefront/src/middleware/index.ts +455 -0
- package/apps/storefront/src/pages/404.astro +61 -0
- package/apps/storefront/src/pages/[...slug].astro +90 -0
- package/apps/storefront/src/pages/admin.astro +1675 -0
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +53 -0
- package/apps/storefront/src/pages/api/admin/orders.ts +71 -0
- package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
- package/apps/storefront/src/pages/api/auth/send.ts +85 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
- package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
- package/apps/storefront/src/pages/api/checkout-style.css.ts +43 -0
- package/apps/storefront/src/pages/api/cli/policy.ts +70 -0
- package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +164 -0
- package/apps/storefront/src/pages/api/csp-report.ts +25 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
- package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
- package/apps/storefront/src/pages/api/dev/activity.ts +124 -0
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +73 -0
- package/apps/storefront/src/pages/api/dev/diff.ts +68 -0
- package/apps/storefront/src/pages/api/dev/file.ts +57 -0
- package/apps/storefront/src/pages/api/dev/locate.ts +125 -0
- package/apps/storefront/src/pages/api/dev/publish.ts +128 -0
- package/apps/storefront/src/pages/api/dev/tree.ts +70 -0
- package/apps/storefront/src/pages/api/dev/write.ts +116 -0
- package/apps/storefront/src/pages/api/newsletter.ts +104 -0
- package/apps/storefront/src/pages/api/request-access.ts +172 -0
- package/apps/storefront/src/pages/auth/login.astro +149 -0
- package/apps/storefront/src/pages/auth/magic.astro +105 -0
- package/apps/storefront/src/pages/capabilities.astro +292 -0
- package/apps/storefront/src/pages/collections/[handle].astro +177 -0
- package/apps/storefront/src/pages/collections/index.astro +59 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
- package/apps/storefront/src/pages/dashboard/index.astro +160 -0
- package/apps/storefront/src/pages/index.astro +290 -0
- package/apps/storefront/src/pages/llms.txt.ts +92 -0
- package/apps/storefront/src/pages/products/[handle].astro +402 -0
- package/apps/storefront/src/pages/robots.txt.ts +24 -0
- package/apps/storefront/src/pages/saved.astro +35 -0
- package/apps/storefront/src/pages/search.astro +150 -0
- package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +1280 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +229 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +144 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +116 -0
- package/apps/storefront/src/pages/style-guide/index.astro +98 -0
- package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
- package/apps/storefront/src/styles/global.css +292 -0
- package/apps/storefront/src/themes/reference.ts +68 -0
- package/apps/storefront/src/themes/schema.ts +111 -0
- package/apps/storefront/src/themes/tenants/index.ts +51 -0
- package/apps/storefront/tsconfig.json +16 -0
- package/apps/storefront/vitest.config.ts +22 -0
- package/package.json +26 -0
- package/packages/public-runtime/LICENSE +58 -0
- package/packages/public-runtime/package.json +22 -0
- package/packages/public-runtime/src/binary-path.ts +68 -0
- package/packages/public-runtime/src/capabilities.ts +89 -0
- package/packages/public-runtime/src/catalog-api.ts +81 -0
- package/packages/public-runtime/src/catalog-d1.ts +192 -0
- package/packages/public-runtime/src/checkout-style-publish.ts +200 -0
- package/packages/public-runtime/src/checkout-style.ts +375 -0
- package/packages/public-runtime/src/csp.ts +109 -0
- package/packages/public-runtime/src/customization-preview.ts +197 -0
- package/packages/public-runtime/src/customization-reconcile.ts +63 -0
- package/packages/public-runtime/src/customization-runtime.ts +157 -0
- package/packages/public-runtime/src/customization-scripts.ts +64 -0
- package/packages/public-runtime/src/customization-versioning.ts +95 -0
- package/packages/public-runtime/src/foxy-orders.ts +136 -0
- package/packages/public-runtime/src/foxy.ts +196 -0
- package/packages/public-runtime/src/hot-standby-readiness.ts +140 -0
- package/packages/public-runtime/src/index.ts +34 -0
- package/packages/public-runtime/src/product.ts +159 -0
- package/packages/public-runtime/src/search.ts +45 -0
- package/packages/public-runtime/src/tenant-assets.ts +444 -0
- package/packages/public-runtime/src/tenant.ts +384 -0
- package/packages/public-runtime/tsconfig.json +8 -0
- package/packages/public-runtime/vitest.config.ts +8 -0
- package/pnpm-workspace.yaml +7 -0
- package/scripts/build/copy-tenant-assets.mjs +69 -0
- package/scripts/dev/ai-edit.mjs +183 -0
- package/scripts/dev/checkout-watch.mjs +116 -0
- package/scripts/dev/file-browser.mjs +159 -0
- package/scripts/dev/file-writer.mjs +141 -0
- package/scripts/dev/git-status.mjs +171 -0
- package/scripts/dev/locate-handler.mjs +163 -0
- package/scripts/dev/locate.mjs +112 -0
- package/scripts/dev/port-check.mjs +48 -0
- package/scripts/dev/safe-path.mjs +109 -0
- package/scripts/dev/shot.mjs +46 -0
- package/scripts/dev/tenant-source-root.mjs +76 -0
- package/scripts/dev/unified-diff.mjs +81 -0
- package/scripts/tot-dev.mjs +376 -0
- package/tenants/README.md +14 -0
- package/tenants/home/content/chrome.json +7 -0
- package/tenants/home/content/home.html +58 -0
- package/tenants/home/content/home.json +11 -0
- package/tenants/home/public/js/storefront.js +210 -0
- package/tenants/home/public/logo-wordmark.png +0 -0
- package/tenants/home/public/pages/storefront.css +212 -0
- package/tenants/home/theme.json +6 -0
- package/tsconfig.base.json +20 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw marketing-HTML serving — the "vendor injects their own HTML" path.
|
|
3
|
+
*
|
|
4
|
+
* A marketing tenant can ship a self-contained document (`content/<tenant>/home.html`
|
|
5
|
+
* or `content/<tenant>/pages-html/<slug>.html`) that we serve VERBATIM, bypassing the
|
|
6
|
+
* block Layout. Two transforms are applied at serve time:
|
|
7
|
+
* 1. nonce-stamp the document's inline <style>/<script> so a strict CSP admits them;
|
|
8
|
+
* 2. under path-prefix tenant routing (locals.basePath set), prefix same-origin
|
|
9
|
+
* root-absolute anchor hrefs with the base path so navigation stays in-tenant —
|
|
10
|
+
* the raw doc bypasses the Layout's client base-path fixer, so we do it here.
|
|
11
|
+
*
|
|
12
|
+
* Single home for both routes (`index.astro`, `[...slug].astro`).
|
|
13
|
+
*
|
|
14
|
+
* SECURITY: this trusts the tenant's HTML (first-party marketing). The nonce
|
|
15
|
+
* injection is a deliberate string replace — robust for first-party documents, not a
|
|
16
|
+
* sanitizer. Untrusted vendor HTML must be sandboxed/sanitized instead.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Marker a body-only page fragment's chrome wrapper uses to say "the page body
|
|
21
|
+
* goes here". A tenant's `content/<tenant>/chrome.html` is a full document with
|
|
22
|
+
* this comment where the fragment is spliced in. Kept as an HTML comment so the
|
|
23
|
+
* wrapper is still valid, previewable HTML on its own.
|
|
24
|
+
*/
|
|
25
|
+
export const CHROME_BODY_MARKER = "<!--PAGE_BODY-->";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Marker in the chrome wrapper's <head> where a fragment's page-specific <head>
|
|
29
|
+
* (title / meta / canonical / OG / JSON-LD) is spliced in — so shared chrome does
|
|
30
|
+
* NOT flatten per-page SEO. A fragment may lead with a `<head>…</head>` block; its
|
|
31
|
+
* contents go here, the rest goes to CHROME_BODY_MARKER.
|
|
32
|
+
*/
|
|
33
|
+
export const CHROME_HEAD_MARKER = "<!--PAGE_HEAD-->";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* True when `html` is a complete document (has <html> or a doctype) rather than a
|
|
37
|
+
* body-only fragment. Full documents are served verbatim; fragments get wrapped
|
|
38
|
+
* in the tenant's shared chrome. This is what keeps the mechanism backward-
|
|
39
|
+
* compatible — every existing raw home/subpage is a full document.
|
|
40
|
+
*/
|
|
41
|
+
export function isFullDocument(html: string): boolean {
|
|
42
|
+
return /^\s*<!doctype/i.test(html) || /<html[\s>]/i.test(html);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Splice a body-only fragment into a tenant chrome wrapper at CHROME_BODY_MARKER.
|
|
47
|
+
* If the wrapper has no marker (misconfigured), the fragment is returned unwrapped
|
|
48
|
+
* so a page never disappears.
|
|
49
|
+
*/
|
|
50
|
+
export function wrapFragmentInChrome(fragment: string, chromeWrapper: string): string {
|
|
51
|
+
if (!chromeWrapper.includes(CHROME_BODY_MARKER)) return fragment;
|
|
52
|
+
// A fragment may lead with a <head>…</head> of page-specific SEO tags. Split it
|
|
53
|
+
// out so the shared wrapper keeps per-page title/meta/canonical/JSON-LD.
|
|
54
|
+
let head = "";
|
|
55
|
+
let body = fragment;
|
|
56
|
+
const m = fragment.match(/^\s*<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
57
|
+
if (m) {
|
|
58
|
+
head = m[1] ?? "";
|
|
59
|
+
body = fragment.slice(m[0].length);
|
|
60
|
+
}
|
|
61
|
+
let out = chromeWrapper.replace(CHROME_BODY_MARKER, body);
|
|
62
|
+
if (out.includes(CHROME_HEAD_MARKER)) out = out.replace(CHROME_HEAD_MARKER, head);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Stamp `nonce` onto every inline `<style>`/`<script>` so a strict CSP admits them. */
|
|
67
|
+
export function nonceInlineTags(html: string, nonce: string): string {
|
|
68
|
+
return html
|
|
69
|
+
.replaceAll("<style>", `<style nonce="${nonce}">`)
|
|
70
|
+
.replaceAll("<script", `<script nonce="${nonce}"`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Prefix same-origin root-absolute hrefs (`href="/…"`) with the tenant base path,
|
|
75
|
+
* so `/pricing/` becomes `/<domain>/pricing/` under path-prefix routing. No-op when
|
|
76
|
+
* basePath is empty (host-routed prod). Skips protocol-relative `//…` and, crucially,
|
|
77
|
+
* the GLOBAL static-asset namespaces served at root under both routing modes, so they
|
|
78
|
+
* must NOT get the tenant prefix:
|
|
79
|
+
* - `/tenants/…` — per-tenant static assets (logo, favicon, theme/mkt css);
|
|
80
|
+
* - `/shared/…` — platform-shipped shared assets (e.g. the shared commerce-chrome
|
|
81
|
+
* stylesheet, unit F2) that every tenant links.
|
|
82
|
+
* Asset `src="…"` isn't matched at all (href-only).
|
|
83
|
+
*/
|
|
84
|
+
export function prefixTenantLinks(html: string, basePath: string): string {
|
|
85
|
+
if (!basePath) return html;
|
|
86
|
+
return html.replace(/href="\/(?!\/|tenants\/|shared\/)/g, `href="${basePath}/`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build the HTML Response for a raw marketing page (nonce + base-path).
|
|
91
|
+
*
|
|
92
|
+
* Shared-chrome mechanism: a page is served in one of two ways —
|
|
93
|
+
* - FULL DOCUMENT (has <html>/doctype): served verbatim, as before.
|
|
94
|
+
* - BODY FRAGMENT + a tenant `chromeWrapper`: the fragment is spliced into the
|
|
95
|
+
* wrapper's CHROME_BODY_MARKER so chrome lives in ONE place, not copied into
|
|
96
|
+
* every page. A fragment with no wrapper still serves (unwrapped) for safety.
|
|
97
|
+
* Either way, the FINAL document gets nonce-stamped and base-path-prefixed, so the
|
|
98
|
+
* chrome's own inline <style>/<script> and root-absolute nav links are handled too.
|
|
99
|
+
*/
|
|
100
|
+
export function rawHtmlResponse(
|
|
101
|
+
html: string,
|
|
102
|
+
opts: { nonce: string; basePath: string; chromeWrapper?: string | null },
|
|
103
|
+
): Response {
|
|
104
|
+
let composed =
|
|
105
|
+
isFullDocument(html) || !opts.chromeWrapper
|
|
106
|
+
? html
|
|
107
|
+
: wrapFragmentInChrome(html, opts.chromeWrapper);
|
|
108
|
+
// DEV ONLY: these pages are served verbatim, bypassing Astro's render pipeline,
|
|
109
|
+
// so the Vite HMR client is never injected — a save would reach the server (see
|
|
110
|
+
// the tenantHotReload plugin in astro.config.mjs) but the browser would have no
|
|
111
|
+
// listener to act on the full-reload. Inject the client so save→reload works.
|
|
112
|
+
// Stamped with the page nonce by nonceInlineTags below; tree-shaken from prod.
|
|
113
|
+
if (import.meta.env.DEV) {
|
|
114
|
+
const client = `<script type="module" src="/@vite/client"></script>`;
|
|
115
|
+
composed = composed.includes("</head>")
|
|
116
|
+
? composed.replace("</head>", `${client}</head>`)
|
|
117
|
+
: `${client}${composed}`;
|
|
118
|
+
// Inline-AI edit loop's floating "✦ ask" affordance (WS5 G1) — same reason
|
|
119
|
+
// as the HMR client above, these pages bypass the Astro pipeline that
|
|
120
|
+
// inlineAiEditPlugin's transformIndexHtml hook (astro.config.mjs) covers
|
|
121
|
+
// for everything else, so it's injected here too. Unconditional in dev,
|
|
122
|
+
// matching the Vite client above: whether `tot dev` set an active tenant
|
|
123
|
+
// (TOT_DEV_TENANT) is a Node-process env var, invisible to this SSR code
|
|
124
|
+
// when it runs inside the Cloudflare/workerd dev runtime (see tot-dev.mjs's
|
|
125
|
+
// own note on this exact gap) — so the gate lives server-side instead, in
|
|
126
|
+
// inlineAiEditPlugin's Vite middleware, which DOES see the real env.
|
|
127
|
+
const widget = `<script type="module" src="/__tot/ai-widget-client.js"></script>`;
|
|
128
|
+
composed = composed.includes("</body>")
|
|
129
|
+
? composed.replace("</body>", `${widget}</body>`)
|
|
130
|
+
: `${composed}${widget}`;
|
|
131
|
+
}
|
|
132
|
+
const doc = prefixTenantLinks(nonceInlineTags(composed, opts.nonce), opts.basePath);
|
|
133
|
+
return new Response(doc, {
|
|
134
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory Fuse search index — the OFFLINE search floor.
|
|
3
|
+
*
|
|
4
|
+
* Used by FixtureToTClient.search (no D1 binding): typo-tolerant, relevance-ranked
|
|
5
|
+
* search over the bundled catalog snapshot, zero infra. When a D1 binding is wired
|
|
6
|
+
* (D1ToTClient), search instead goes through FTS5 — see lib/d1/catalog.ts and
|
|
7
|
+
* epics/edge-read-replica.md. This module is the portable floor an extracted
|
|
8
|
+
* merchant gets out of the box (docs/extraction-runbook.md).
|
|
9
|
+
*/
|
|
10
|
+
import Fuse, { type IFuseOptions } from "fuse.js";
|
|
11
|
+
import type { CatalogProduct } from "@tot/public-runtime";
|
|
12
|
+
|
|
13
|
+
export interface SearchHit {
|
|
14
|
+
product: CatalogProduct;
|
|
15
|
+
score: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SearchIndex {
|
|
19
|
+
search(query: string, limit?: number): SearchHit[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const FUSE_OPTIONS: IFuseOptions<CatalogProduct> = {
|
|
23
|
+
includeScore: true,
|
|
24
|
+
threshold: 0.38, // typo tolerance: lower = stricter
|
|
25
|
+
ignoreLocation: true,
|
|
26
|
+
minMatchCharLength: 2,
|
|
27
|
+
keys: [
|
|
28
|
+
{ name: "title", weight: 0.5 },
|
|
29
|
+
{ name: "vendor", weight: 0.15 },
|
|
30
|
+
{ name: "product_type", weight: 0.15 },
|
|
31
|
+
{ name: "tags", weight: 0.12 },
|
|
32
|
+
{ name: "description_text", weight: 0.08 },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function buildSearchIndex(products: CatalogProduct[]): SearchIndex {
|
|
37
|
+
// Defensive: only ever index active products (the draft canary must never leak).
|
|
38
|
+
const active = products.filter((p) => p.status === "active");
|
|
39
|
+
const fuse = new Fuse(active, FUSE_OPTIONS);
|
|
40
|
+
return {
|
|
41
|
+
search(query: string, limit = 12): SearchHit[] {
|
|
42
|
+
const q = query.trim();
|
|
43
|
+
if (!q) return [];
|
|
44
|
+
return fuse.search(q, { limit }).map((r) => ({ product: r.item, score: r.score ?? 1 }));
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Compact shape sent to the typeahead island (keeps client JSON tiny). */
|
|
50
|
+
export interface TypeaheadDoc {
|
|
51
|
+
handle: string;
|
|
52
|
+
title: string;
|
|
53
|
+
vendor?: string;
|
|
54
|
+
type?: string;
|
|
55
|
+
price: number;
|
|
56
|
+
currency: string;
|
|
57
|
+
image?: string;
|
|
58
|
+
tags: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function toTypeaheadDoc(p: CatalogProduct): TypeaheadDoc {
|
|
62
|
+
const doc: TypeaheadDoc = {
|
|
63
|
+
handle: p.handle,
|
|
64
|
+
title: p.title,
|
|
65
|
+
price: p.price_range.min,
|
|
66
|
+
currency: p.price_range.currency,
|
|
67
|
+
tags: p.tags,
|
|
68
|
+
};
|
|
69
|
+
if (p.vendor) doc.vendor = p.vendor;
|
|
70
|
+
if (p.product_type) doc.type = p.product_type;
|
|
71
|
+
const img = p.images[0];
|
|
72
|
+
if (img?.url) doc.image = img.url;
|
|
73
|
+
return doc;
|
|
74
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SectionSource — the single, typed contract every merchandising section binds
|
|
3
|
+
* to. A section component (ProductCarousel, ProductGrid, …) never talks to the
|
|
4
|
+
* catalog directly; it declares WHAT set of products it wants as a SectionSource
|
|
5
|
+
* and calls resolveSectionSource() to get them. This keeps the components
|
|
6
|
+
* tenant-agnostic and data-source-agnostic: swap the ToTClient backend (D1 vs
|
|
7
|
+
* fixture) and every section keeps working.
|
|
8
|
+
*
|
|
9
|
+
* Fidelity to a merchant's storefront comes from theme tokens + variant props +
|
|
10
|
+
* the source they point a section at — NEVER from forking a component per
|
|
11
|
+
* merchant. That's what keeps a tenant single-tenant-extractable.
|
|
12
|
+
*/
|
|
13
|
+
import type { CatalogProduct } from "@tot/public-runtime";
|
|
14
|
+
import type { ToTClient } from "@/lib/tot/ToTClient";
|
|
15
|
+
import { isOnSale } from "@/lib/tot/query";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Where a merchandising section gets its products. Exactly one shape per source.
|
|
19
|
+
* `handle` placeholders are product/collection handles (unique per tenant).
|
|
20
|
+
*/
|
|
21
|
+
export type SectionSource =
|
|
22
|
+
| { collection: string } // products in a collection (by handle)
|
|
23
|
+
| { tag: string } // products carrying a tag
|
|
24
|
+
| { featured: true } // the curated/featured set (getFeatured)
|
|
25
|
+
| { newest: true } // newest-first
|
|
26
|
+
| { bestSelling: true } // bestseller-tagged
|
|
27
|
+
| { onSale: true } // anything with a live compare-at markdown
|
|
28
|
+
| { handles: string[] } // an explicit, ordered handle list
|
|
29
|
+
| { related: string } // products related to a base product (by handle)
|
|
30
|
+
| { recentlyViewed: true }; // client-personalized — resolves empty server-side
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_SECTION_LIMIT = 12;
|
|
33
|
+
|
|
34
|
+
/** Bestseller signal is tag-based in Phase 1 (no order history at the edge). */
|
|
35
|
+
const BESTSELLER_TAGS = new Set(["bestseller", "best-seller", "best-sellers"]);
|
|
36
|
+
|
|
37
|
+
const isActive = (p: CatalogProduct) => p.status === "active";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a SectionSource to a concrete, active product list via the tenant's
|
|
41
|
+
* ToTClient. Always returns at most `limit` ACTIVE products (drafts/archived are
|
|
42
|
+
* excluded from every public surface — see ToTClient docs). Backend-agnostic:
|
|
43
|
+
* tag/onSale/bestSelling filter over the client's list methods rather than
|
|
44
|
+
* assuming a SQL facet, so the fixture and D1 backends behave identically.
|
|
45
|
+
*/
|
|
46
|
+
export async function resolveSectionSource(
|
|
47
|
+
source: SectionSource,
|
|
48
|
+
tot: ToTClient,
|
|
49
|
+
limit: number = DEFAULT_SECTION_LIMIT,
|
|
50
|
+
): Promise<CatalogProduct[]> {
|
|
51
|
+
if ("collection" in source) {
|
|
52
|
+
const { products } = await tot.listProducts({
|
|
53
|
+
collection: source.collection,
|
|
54
|
+
pageSize: limit,
|
|
55
|
+
});
|
|
56
|
+
return products.slice(0, limit);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if ("featured" in source) {
|
|
60
|
+
return (await tot.getFeatured(limit)).slice(0, limit);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if ("newest" in source) {
|
|
64
|
+
const { products } = await tot.listProducts({ sort: "newest", pageSize: limit });
|
|
65
|
+
return products.slice(0, limit);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if ("handles" in source) {
|
|
69
|
+
const found = await Promise.all(
|
|
70
|
+
source.handles.map((h) => tot.getProductByHandle(h)),
|
|
71
|
+
);
|
|
72
|
+
return found
|
|
73
|
+
.filter((p): p is CatalogProduct => !!p && isActive(p))
|
|
74
|
+
.slice(0, limit);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if ("tag" in source) {
|
|
78
|
+
const all = await tot.getAllActiveProducts();
|
|
79
|
+
return all.filter((p) => p.tags.includes(source.tag)).slice(0, limit);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if ("onSale" in source) {
|
|
83
|
+
const all = await tot.getAllActiveProducts();
|
|
84
|
+
return all.filter(isOnSale).slice(0, limit);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if ("bestSelling" in source) {
|
|
88
|
+
const all = await tot.getAllActiveProducts();
|
|
89
|
+
return all
|
|
90
|
+
.filter((p) => p.tags.some((t) => BESTSELLER_TAGS.has(t)))
|
|
91
|
+
.slice(0, limit);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ("related" in source) {
|
|
95
|
+
const base = await tot.getProductByHandle(source.related);
|
|
96
|
+
if (!base) return [];
|
|
97
|
+
const baseCollections = new Set(base.collections);
|
|
98
|
+
const baseTags = new Set(base.tags);
|
|
99
|
+
const all = await tot.getAllActiveProducts();
|
|
100
|
+
const scored = all
|
|
101
|
+
.filter((p) => p.handle !== base.handle)
|
|
102
|
+
.map((p) => {
|
|
103
|
+
const shared =
|
|
104
|
+
p.collections.filter((c) => baseCollections.has(c)).length +
|
|
105
|
+
p.tags.filter((t) => baseTags.has(t)).length;
|
|
106
|
+
return { p, shared };
|
|
107
|
+
})
|
|
108
|
+
.filter((s) => s.shared > 0)
|
|
109
|
+
.sort((a, b) => b.shared - a.shared);
|
|
110
|
+
return scored.slice(0, limit).map((s) => s.p);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// recentlyViewed: server has no per-visitor history; the client island
|
|
114
|
+
// (RecentlyViewed.tsx) hydrates this. Resolve empty so the section can decide
|
|
115
|
+
// to hide or defer to its island.
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Ready-made sources for the common merchandising rows. A preset carousel is
|
|
121
|
+
* just `<ProductCarousel source={SECTION_PRESETS.bestSellers} />` — there are no
|
|
122
|
+
* separate BestSellers/NewArrivals/OnSale components to maintain.
|
|
123
|
+
*/
|
|
124
|
+
export const SECTION_PRESETS = {
|
|
125
|
+
featured: { featured: true } as SectionSource,
|
|
126
|
+
bestSellers: { bestSelling: true } as SectionSource,
|
|
127
|
+
newArrivals: { newest: true } as SectionSource,
|
|
128
|
+
onSale: { onSale: true } as SectionSource,
|
|
129
|
+
} as const;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline security response headers (Epic Phase-2 S1).
|
|
3
|
+
*
|
|
4
|
+
* Applied to every storefront response in middleware and baked into cached pages.
|
|
5
|
+
* The Content-Security-Policy itself (default-src/frame-ancestors/base-uri/…) is
|
|
6
|
+
* built separately in @tot/private-controlplane `csp.ts`; these are the complementary
|
|
7
|
+
* transport / sniffing / referrer / feature-permission headers that the appraisal
|
|
8
|
+
* flagged as missing.
|
|
9
|
+
*/
|
|
10
|
+
export const SECURITY_HEADERS: Readonly<Record<string, string>> = {
|
|
11
|
+
// 2-year HSTS with preload — production is HTTPS-only behind Cloudflare;
|
|
12
|
+
// browsers ignore this header on plain http (localhost dev), so it's safe to
|
|
13
|
+
// always set.
|
|
14
|
+
"Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
|
|
15
|
+
"X-Content-Type-Options": "nosniff",
|
|
16
|
+
// Legacy backstop; modern browsers honour the CSP `frame-ancestors 'self'`.
|
|
17
|
+
"X-Frame-Options": "SAMEORIGIN",
|
|
18
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
19
|
+
// Lock powerful features off for the browse storefront. Verification and
|
|
20
|
+
// checkout (Powered by FoxyCart) run in their own surfaces that grant only
|
|
21
|
+
// what they need via per-frame Permissions-Policy.
|
|
22
|
+
"Permissions-Policy":
|
|
23
|
+
"camera=(), microphone=(), geolocation=(), browsing-topics=(), interest-cohort=()",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Set the baseline headers on a response, without clobbering anything already set. */
|
|
27
|
+
export function applySecurityHeaders(headers: Headers): void {
|
|
28
|
+
for (const [key, value] of Object.entries(SECURITY_HEADERS)) {
|
|
29
|
+
if (!headers.has(key)) headers.set(key, value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editorial content model (Storyblok seam).
|
|
3
|
+
*
|
|
4
|
+
* These are OUR types, decoupled from Storyblok's wire shape. The provider maps
|
|
5
|
+
* either source (Storyblok delivery API OR local JSON) into these, so pages
|
|
6
|
+
* never see a raw Storyblok story. The block schemas here mirror the component
|
|
7
|
+
* schemas that live in the Storyblok PARENT space (shared components); the
|
|
8
|
+
* pilot CHILD space holds the content instances. See DECISIONS.md for the
|
|
9
|
+
* parent/child + Shared Components plan.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type HomeBlock =
|
|
13
|
+
| HeroBlock
|
|
14
|
+
| PromoTilesBlock
|
|
15
|
+
| FeaturedCollectionsBlock
|
|
16
|
+
| FeaturedProductsBlock
|
|
17
|
+
| EditorialBlock
|
|
18
|
+
| NewsletterBlock;
|
|
19
|
+
|
|
20
|
+
export interface PromoTilesBlock {
|
|
21
|
+
component: "promo_tiles";
|
|
22
|
+
title?: string;
|
|
23
|
+
/** A row of promotional tiles (e.g. "New Additions", "On Sale") that link to
|
|
24
|
+
* collections — captured from the merchant's homepage. */
|
|
25
|
+
tiles: { label: string; href: string; image?: string }[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface HeroBlock {
|
|
29
|
+
component: "hero";
|
|
30
|
+
/** The "thesis" line — the most characteristic thing about the catalog. */
|
|
31
|
+
eyebrow?: string;
|
|
32
|
+
headline: string;
|
|
33
|
+
/** Split second line gets the signature oversized treatment. */
|
|
34
|
+
headlineAccent?: string;
|
|
35
|
+
subhead?: string;
|
|
36
|
+
ctaLabel?: string;
|
|
37
|
+
ctaHref?: string;
|
|
38
|
+
/** Optional handle of a product/collection to feature visually. */
|
|
39
|
+
featureCollection?: string;
|
|
40
|
+
/** Direct hero image URL (e.g. the merchant's own homepage hero slide). */
|
|
41
|
+
image?: string;
|
|
42
|
+
imageAlt?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FeaturedCollectionsBlock {
|
|
46
|
+
component: "featured_collections";
|
|
47
|
+
title?: string;
|
|
48
|
+
/** Collection handles to surface; empty = all. */
|
|
49
|
+
handles: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FeaturedProductsBlock {
|
|
53
|
+
component: "featured_products";
|
|
54
|
+
title?: string;
|
|
55
|
+
/** "featured" | "newest" | explicit handles. */
|
|
56
|
+
source: "featured" | "newest";
|
|
57
|
+
handles?: string[];
|
|
58
|
+
limit?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface EditorialBlock {
|
|
62
|
+
component: "editorial";
|
|
63
|
+
eyebrow?: string;
|
|
64
|
+
title: string;
|
|
65
|
+
body_html: string;
|
|
66
|
+
imageSeed?: string;
|
|
67
|
+
align?: "left" | "right";
|
|
68
|
+
ctaLabel?: string;
|
|
69
|
+
ctaHref?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface NewsletterBlock {
|
|
73
|
+
component: "newsletter";
|
|
74
|
+
title: string;
|
|
75
|
+
body?: string;
|
|
76
|
+
placeholder?: string;
|
|
77
|
+
ctaLabel?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Marketing blocks (non-commerce tenants). These render WITHOUT catalog data —
|
|
82
|
+
// a marketing tenant composes its pages entirely from these. They're
|
|
83
|
+
// tenant-agnostic: every color/space comes from theme tokens, and the copy
|
|
84
|
+
// lives in tenant content JSON. See components/marketing/*.
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/** A call-to-action button. `variant` picks primary/secondary styling; an
|
|
88
|
+
* `external` href (starts with http) opens the app/help/etc. surfaces. */
|
|
89
|
+
export interface MarketingCta {
|
|
90
|
+
label: string;
|
|
91
|
+
href: string;
|
|
92
|
+
variant?: "primary" | "secondary" | "ghost";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface MarketingHeroBlock {
|
|
96
|
+
component: "marketing_hero";
|
|
97
|
+
eyebrow?: string;
|
|
98
|
+
headline: string;
|
|
99
|
+
subhead?: string;
|
|
100
|
+
body?: string;
|
|
101
|
+
/** Short trust chips shown under the CTAs (e.g. "200+ countries"). */
|
|
102
|
+
trust?: string[];
|
|
103
|
+
ctas?: MarketingCta[];
|
|
104
|
+
/** Optional right-hand "product moment": a titled list of demo/log lines. */
|
|
105
|
+
panel?: { title?: string; lines: string[] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A horizontal strip of short proof tokens (countries, standards, ratings). */
|
|
109
|
+
export interface TrustBarBlock {
|
|
110
|
+
component: "trust_bar";
|
|
111
|
+
items: string[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Before/after comparison — the "one platform vs a patchwork" section. */
|
|
115
|
+
export interface SplitCompareBlock {
|
|
116
|
+
component: "split_compare";
|
|
117
|
+
eyebrow?: string;
|
|
118
|
+
title: string;
|
|
119
|
+
subhead?: string;
|
|
120
|
+
before: { title: string; items: string[] };
|
|
121
|
+
after: { title: string; items: string[] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Numbered/emoji journey steps. */
|
|
125
|
+
export interface StepsBlock {
|
|
126
|
+
component: "steps";
|
|
127
|
+
eyebrow?: string;
|
|
128
|
+
title: string;
|
|
129
|
+
subhead?: string;
|
|
130
|
+
steps: { icon?: string; label: string; detail?: string }[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A grid of capability/industry cards. `variant` tunes density/label style. */
|
|
134
|
+
export interface CardGridBlock {
|
|
135
|
+
component: "card_grid";
|
|
136
|
+
/** Optional anchor id so header nav can jump to this section (e.g. "platform"). */
|
|
137
|
+
anchor?: string;
|
|
138
|
+
eyebrow?: string;
|
|
139
|
+
title: string;
|
|
140
|
+
subhead?: string;
|
|
141
|
+
variant?: "capability" | "industry" | "feature";
|
|
142
|
+
cards: {
|
|
143
|
+
icon?: string;
|
|
144
|
+
title: string;
|
|
145
|
+
body?: string;
|
|
146
|
+
/** Small meta line (e.g. "Use case: account opening"). */
|
|
147
|
+
meta?: string;
|
|
148
|
+
href?: string;
|
|
149
|
+
}[];
|
|
150
|
+
/** Optional footnote of extra capabilities under the grid. */
|
|
151
|
+
footnote?: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A single fenced code sample. NEVER put secret keys here — server-only. */
|
|
155
|
+
export interface CodeSample {
|
|
156
|
+
language?: string;
|
|
157
|
+
code: string;
|
|
158
|
+
/** Optional caption/warning shown above the block. */
|
|
159
|
+
caption?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Integration story: platform logos/labels + an optional safe code sample. */
|
|
163
|
+
export interface IntegrationsBlock {
|
|
164
|
+
component: "integrations";
|
|
165
|
+
eyebrow?: string;
|
|
166
|
+
title: string;
|
|
167
|
+
subhead?: string;
|
|
168
|
+
platforms: { label: string; href?: string }[];
|
|
169
|
+
points?: string[];
|
|
170
|
+
code?: CodeSample;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Privacy / trust proof strip (e.g. "We will never sell your data"). */
|
|
174
|
+
export interface ProofStripBlock {
|
|
175
|
+
component: "proof_strip";
|
|
176
|
+
eyebrow?: string;
|
|
177
|
+
title: string;
|
|
178
|
+
body?: string;
|
|
179
|
+
items?: string[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface TestimonialsBlock {
|
|
183
|
+
component: "testimonials";
|
|
184
|
+
eyebrow?: string;
|
|
185
|
+
title?: string;
|
|
186
|
+
quotes: { stat?: string; quote: string; attribution?: string }[];
|
|
187
|
+
ratings?: string[];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface FaqBlock {
|
|
191
|
+
component: "faq";
|
|
192
|
+
eyebrow?: string;
|
|
193
|
+
title: string;
|
|
194
|
+
items: { q: string; a: string }[];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Final conversion band. */
|
|
198
|
+
export interface CtaBandBlock {
|
|
199
|
+
component: "cta_band";
|
|
200
|
+
title: string;
|
|
201
|
+
subhead?: string;
|
|
202
|
+
ctas?: MarketingCta[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export type MarketingBlock =
|
|
206
|
+
| MarketingHeroBlock
|
|
207
|
+
| TrustBarBlock
|
|
208
|
+
| SplitCompareBlock
|
|
209
|
+
| StepsBlock
|
|
210
|
+
| CardGridBlock
|
|
211
|
+
| IntegrationsBlock
|
|
212
|
+
| ProofStripBlock
|
|
213
|
+
| TestimonialsBlock
|
|
214
|
+
| FaqBlock
|
|
215
|
+
| CtaBandBlock;
|
|
216
|
+
|
|
217
|
+
/** Discriminators that identify a marketing block (vs a commerce HomeBlock). */
|
|
218
|
+
export const MARKETING_COMPONENTS = new Set<MarketingBlock["component"]>([
|
|
219
|
+
"marketing_hero",
|
|
220
|
+
"trust_bar",
|
|
221
|
+
"split_compare",
|
|
222
|
+
"steps",
|
|
223
|
+
"card_grid",
|
|
224
|
+
"integrations",
|
|
225
|
+
"proof_strip",
|
|
226
|
+
"testimonials",
|
|
227
|
+
"faq",
|
|
228
|
+
"cta_band",
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
export function isMarketingBlock(
|
|
232
|
+
block: { component: string },
|
|
233
|
+
): block is MarketingBlock {
|
|
234
|
+
return MARKETING_COMPONENTS.has(block.component as MarketingBlock["component"]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface HomeContent {
|
|
238
|
+
/** SEO + social. */
|
|
239
|
+
seoTitle?: string;
|
|
240
|
+
seoDescription?: string;
|
|
241
|
+
/** Commerce home blocks OR marketing blocks — the page branches on tenant
|
|
242
|
+
* siteType/features and renders the matching set. */
|
|
243
|
+
blocks: (HomeBlock | MarketingBlock)[];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface EditorialPage {
|
|
247
|
+
slug: string;
|
|
248
|
+
title: string;
|
|
249
|
+
seoTitle?: string;
|
|
250
|
+
seoDescription?: string;
|
|
251
|
+
/** Sanitized HTML body. */
|
|
252
|
+
body_html: string;
|
|
253
|
+
/** Optional hero eyebrow/intro. */
|
|
254
|
+
eyebrow?: string;
|
|
255
|
+
intro?: string;
|
|
256
|
+
/** When true, listed in the footer policy column. */
|
|
257
|
+
isPolicy?: boolean;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface SiteNavItem {
|
|
261
|
+
label: string;
|
|
262
|
+
href: string;
|
|
263
|
+
/** Mega-menu children, e.g. collection links grouped under a category. */
|
|
264
|
+
children?: { label: string; href: string }[];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface SiteChrome {
|
|
268
|
+
nav: SiteNavItem[];
|
|
269
|
+
/**
|
|
270
|
+
* Header conversion buttons (marketing tenants). Rendered in the header's
|
|
271
|
+
* right cluster in place of the commerce search/saved controls. Commerce
|
|
272
|
+
* tenants omit this and keep the search + saved affordances.
|
|
273
|
+
*/
|
|
274
|
+
headerCtas?: MarketingCta[];
|
|
275
|
+
footer: {
|
|
276
|
+
columns: { title: string; links: { label: string; href: string }[] }[];
|
|
277
|
+
social: { label: string; href: string }[];
|
|
278
|
+
tagline?: string;
|
|
279
|
+
/** Bottom-bar legal/compliance line (marketing tenants). Plain text. */
|
|
280
|
+
legal?: string;
|
|
281
|
+
};
|
|
282
|
+
announcement?: string;
|
|
283
|
+
/** Default legal/safety disclaimer HTML (fallback for unknown categories). */
|
|
284
|
+
disclaimer?: string;
|
|
285
|
+
/** Per-product-category disclaimer HTML, keyed by product_type (or
|
|
286
|
+
* compliance.product_category) — the source store renders a different legal
|
|
287
|
+
* block per type (sub-ohm on salts, battery on devices). */
|
|
288
|
+
disclaimers?: Record<string, string>;
|
|
289
|
+
}
|