@cimplify/cli 0.16.0 → 0.17.1

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 (104) hide show
  1. package/dist/{add-SXC6RUPQ.mjs → add-C3DPTVH3.mjs} +9 -9
  2. package/dist/{add-XQVQLD3F.mjs → add-JX2GXWXU.mjs} +3 -3
  3. package/dist/{ai-NQH5L4I6.mjs → ai-HUZTVJIQ.mjs} +9 -9
  4. package/dist/{assets-FZGVOQXH.mjs → assets-MPESJSUV.mjs} +3 -3
  5. package/dist/{auth-step-up-67LCPDNL.mjs → auth-step-up-VJAFGSW6.mjs} +4 -4
  6. package/dist/{build-J4ZUIOPX.mjs → build-E7RRQWVF.mjs} +21 -30
  7. package/dist/{cancel-VGJHZJ4J.mjs → cancel-UMKR274A.mjs} +3 -3
  8. package/dist/{capture-RAJ2B3BH.mjs → capture-TK4NIUYM.mjs} +19 -19
  9. package/dist/{chunk-DYWUPHV3.mjs → chunk-3JOJZ2MK.mjs} +2 -2
  10. package/dist/{chunk-LNMITJ4Z.mjs → chunk-3LJG22FS.mjs} +4 -4
  11. package/dist/{chunk-5HP2INMY.mjs → chunk-454RQ6NR.mjs} +5 -22
  12. package/dist/{chunk-54J3XR4T.mjs → chunk-6CQMRZ2W.mjs} +1 -1
  13. package/dist/{chunk-7JR6JCFF.mjs → chunk-6KKLF5KG.mjs} +4 -4
  14. package/dist/{chunk-NY4YT7O6.mjs → chunk-74QZQSCL.mjs} +2 -2
  15. package/dist/{chunk-AKACUJ3W.mjs → chunk-BGM7CH5G.mjs} +1 -1
  16. package/dist/{chunk-CPBESPWK.mjs → chunk-BTNVYAC4.mjs} +7 -26
  17. package/dist/{chunk-SAXGWS2K.mjs → chunk-ECGDRU56.mjs} +1 -1
  18. package/dist/{chunk-EDZSOL5O.mjs → chunk-EOOOXVHR.mjs} +1 -1
  19. package/dist/{chunk-TT4YI33G.mjs → chunk-GGPN4QYQ.mjs} +1 -1
  20. package/dist/{chunk-T4MW3PD4.mjs → chunk-HQPZQIWW.mjs} +1 -1
  21. package/dist/{chunk-KOD4GBQ5.mjs → chunk-I7GEHLDD.mjs} +10 -2
  22. package/dist/{chunk-JEVSFNPT.mjs → chunk-ITDAICBY.mjs} +6 -6
  23. package/dist/{chunk-SCKQ4XSD.mjs → chunk-JNCEKW2Y.mjs} +2 -2
  24. package/dist/{chunk-TYV2SVFN.mjs → chunk-JTYVGORV.mjs} +4 -4
  25. package/dist/{chunk-MEM3MDV4.mjs → chunk-LJDIQ4PF.mjs} +8 -8
  26. package/dist/{chunk-SS4JTMIV.mjs → chunk-LOH7C6ZO.mjs} +16 -8
  27. package/dist/{chunk-RP66A52P.mjs → chunk-MJMRIMR7.mjs} +3 -3
  28. package/dist/{chunk-ZUCDXRFI.mjs → chunk-N2WZJX5P.mjs} +3 -25
  29. package/dist/{chunk-SF44L2OD.mjs → chunk-NEB372WD.mjs} +1 -1
  30. package/dist/{chunk-7PQL7E7K.mjs → chunk-OPX7KPOU.mjs} +3 -3
  31. package/dist/{chunk-KMHORI4Y.mjs → chunk-PHUBMRAS.mjs} +6 -21
  32. package/dist/{chunk-EAV2P4N3.mjs → chunk-PQBALZUH.mjs} +4 -4
  33. package/dist/{chunk-XL54SQJ3.mjs → chunk-PVW5D5AW.mjs} +3 -3
  34. package/dist/{chunk-Q6CF4C3E.mjs → chunk-QQWYQ2HS.mjs} +5 -5
  35. package/dist/{chunk-G2ZEFSQJ.mjs → chunk-QVR6V6QP.mjs} +4 -4
  36. package/dist/{chunk-KNPYBZ6B.mjs → chunk-SZXO6YSX.mjs} +1 -1
  37. package/dist/{chunk-VSA44UKR.mjs → chunk-TYR4Q37L.mjs} +6 -6
  38. package/dist/{chunk-EOO4UOQD.mjs → chunk-V3R2NYQR.mjs} +1 -1
  39. package/dist/{chunk-I4HNHPEM.mjs → chunk-XASDB7YS.mjs} +1 -1
  40. package/dist/{chunk-CLQYPTJ2.mjs → chunk-XBQ4LO2N.mjs} +1 -1
  41. package/dist/{chunk-BYNE553A.mjs → chunk-XF3MGADK.mjs} +15 -14
  42. package/dist/{chunk-EGQIPYUN.mjs → chunk-XZG3DTXV.mjs} +5 -5
  43. package/dist/{chunk-47ZXSY47.mjs → chunk-Y4YGR2NY.mjs} +2 -2
  44. package/dist/{chunk-CNZAOVSK.mjs → chunk-ZG7NJLXN.mjs} +2 -2
  45. package/dist/{create-4S2EAUR3.mjs → create-3TGG7B3X.mjs} +5 -5
  46. package/dist/{credits-NJOAOPPW.mjs → credits-MEG7V5DI.mjs} +7 -7
  47. package/dist/{deploy-26Y4UGZZ.mjs → deploy-CNRORHJ7.mjs} +9 -9
  48. package/dist/dev-RXSBY7U4.mjs +8 -0
  49. package/dist/{dev-MOYURRWQ.mjs → dev-YLYC6B7R.mjs} +17 -17
  50. package/dist/dispatcher.mjs +54 -54
  51. package/dist/{doctor-E6ETD2GJ.mjs → doctor-SRXVQ4GT.mjs} +8 -8
  52. package/dist/{domains-LOTQMVOR.mjs → domains-JIHFKDWR.mjs} +4 -4
  53. package/dist/engine-runtime-fetch-QVKJVC3W.mjs +10 -0
  54. package/dist/{env-3PDFC5XF.mjs → env-LJ3DCAA5.mjs} +5 -5
  55. package/dist/env-R72AHBT2.mjs +19 -0
  56. package/dist/{explain-K5QEVWCD.mjs → explain-QHD4UZTB.mjs} +2 -2
  57. package/dist/{inspect-SJ4WBAXO.mjs → inspect-PYLYN45K.mjs} +18 -18
  58. package/dist/{inspect-Q3E2DHYY.mjs → inspect-ZOLZHNSZ.mjs} +27 -12
  59. package/dist/introspect-QJNR3YXJ.mjs +9 -0
  60. package/dist/{kernel-DEGWMHFL.mjs → kernel-IC4ZC54Z.mjs} +7 -7
  61. package/dist/{keys-W5SN3DPK.mjs → keys-WKOQ4KQE.mjs} +12 -12
  62. package/dist/{lines-XGJYTQ7P.mjs → lines-I6ZB2IOU.mjs} +8 -8
  63. package/dist/{link-NKUAVNM2.mjs → link-TE2FUX74.mjs} +4 -4
  64. package/dist/{lint-G4YPQSSO.mjs → lint-735IPMC6.mjs} +1 -1
  65. package/dist/{list-TDR7K2JB.mjs → list-CHYY2P4T.mjs} +6 -10
  66. package/dist/{lock-EJPW2LM3.mjs → lock-XVR3RRZ6.mjs} +7 -7
  67. package/dist/{login-AYLU7JU7.mjs → login-LW7CHZDZ.mjs} +5 -5
  68. package/dist/{logout-3M6VMHLH.mjs → logout-6U6OP5DM.mjs} +2 -2
  69. package/dist/{logs-4EL5SOXN.mjs → logs-HKH4H6PT.mjs} +3 -3
  70. package/dist/{logs-4QQGG2XY.mjs → logs-SCQC5HNO.mjs} +10 -10
  71. package/dist/{mcp-4R2R6E5M.mjs → mcp-CKGZK2BS.mjs} +7 -7
  72. package/dist/{mock-AK24GSP2.mjs → mock-5H3CVPHC.mjs} +1 -1
  73. package/dist/{outdated-JEKCVLTG.mjs → outdated-T7QW6F3Z.mjs} +9 -9
  74. package/dist/{port-SBZ7X3EH.mjs → port-24R7ZNP4.mjs} +10 -10
  75. package/dist/{projects-7JKTC5WJ.mjs → projects-HYITVEXS.mjs} +4 -4
  76. package/dist/{publish-QQCX47HK.mjs → publish-DZRR5A5X.mjs} +12 -12
  77. package/dist/{remix-NLLYD3BI.mjs → remix-4HVXRIEB.mjs} +1 -1
  78. package/dist/repo-FG3CMZQS.mjs +9 -0
  79. package/dist/{rollback-2WQ2CW7T.mjs → rollback-H7Q4XYGX.mjs} +13 -13
  80. package/dist/{rollback-ZGU32HGT.mjs → rollback-OM64O67F.mjs} +4 -4
  81. package/dist/{scene-HXARHUM7.mjs → scene-QTHT3ZPL.mjs} +4 -4
  82. package/dist/{status-C3ZSLZ7Q.mjs → status-5D4RR3MM.mjs} +3 -3
  83. package/dist/status-AKI6E3JK.mjs +18 -0
  84. package/dist/{test-ZPBWV4VX.mjs → test-W6MURUGZ.mjs} +21 -21
  85. package/dist/{testers-A3LMXOJJ.mjs → testers-EI32JB5F.mjs} +10 -10
  86. package/dist/{unlink-4G4FE4RJ.mjs → unlink-BND47IWW.mjs} +2 -2
  87. package/dist/{update-GHKKM5FS.mjs → update-EFL3OF44.mjs} +3 -3
  88. package/dist/{whoami-3QQN6G2A.mjs → whoami-WHLGCVRC.mjs} +3 -3
  89. package/dist/{workshop-CD74D2LK.mjs → workshop-F2T4IDOU.mjs} +4 -4
  90. package/package.json +2 -2
  91. package/templates/storefront-auto/package.json +1 -1
  92. package/templates/storefront-bakery/package.json +1 -1
  93. package/templates/storefront-fashion/package.json +1 -1
  94. package/templates/storefront-grocery/package.json +1 -1
  95. package/templates/storefront-pharmacy/package.json +1 -1
  96. package/templates/storefront-restaurant/package.json +1 -1
  97. package/templates/storefront-retail/package.json +1 -1
  98. package/templates/storefront-services/package.json +1 -1
  99. package/dist/dev-WL43Q7VD.mjs +0 -8
  100. package/dist/engine-runtime-fetch-CIE7S7PB.mjs +0 -10
  101. package/dist/env-MOTYOPM5.mjs +0 -19
  102. package/dist/introspect-7F34YN5V.mjs +0 -9
  103. package/dist/repo-Z4G4EW3N.mjs +0 -9
  104. package/dist/status-WEQOC25K.mjs +0 -18
@@ -238,7 +238,7 @@ function Field({
238
238
  letter-spacing: -0.012em;
239
239
  }
240
240
  }
241
- ` }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/reservations/reservations-client.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, useState } from "react";\nimport { useRouter } from "next/navigation";\nimport type { Product } from "@cimplify/sdk";\nimport { useCart } from "@cimplify/sdk/react";\n\nconst PARTY_SIZES = [2, 3, 4, 5, 6, 8, 10, 12];\n\n/**\n * Reservation flow. Reservations are modelled as service-type products in\n * the catalogue (e.g. "Two-top", "Four-top", "Long table"). The user picks\n * a seating option, date, time, and party size; we add to cart with the\n * reservation details as item notes; checkout finalises the booking.\n */\nexport function ReservationsClient({ options }: { options: Product[] }) {\n const router = useRouter();\n const { addItem } = useCart();\n const [selectedOption, setSelectedOption] = useState<Product | undefined>(options[0]);\n const [selectedDate, setSelectedDate] = useState<Date>(() => new Date());\n const [selectedSlotKey, setSelectedSlotKey] = useState<string | null>(null);\n const [partySize, setPartySize] = useState<number>(2);\n const [notes, setNotes] = useState("");\n const [submitting, setSubmitting] = useState(false);\n\n const dates = useMemo(() => {\n const now = new Date();\n return Array.from({ length: 14 }).map((_, i) => {\n const d = new Date(now);\n d.setDate(now.getDate() + i);\n d.setHours(0, 0, 0, 0);\n return d;\n });\n }, []);\n\n // Restaurant slots: lunch 12\u201314:30, dinner 18\u201321:30, every 30 min.\n const slots = useMemo(() => {\n const out: { date: Date; label: string; service: "Lunch" | "Dinner" }[] = [];\n for (let h = 12; h <= 14; h++) {\n for (const m of [0, 30]) {\n const d = new Date(selectedDate);\n d.setHours(h, m, 0, 0);\n out.push({ date: d, label: `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`, service: "Lunch" });\n }\n }\n for (let h = 18; h <= 21; h++) {\n for (const m of [0, 30]) {\n const d = new Date(selectedDate);\n d.setHours(h, m, 0, 0);\n out.push({ date: d, label: `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`, service: "Dinner" });\n }\n }\n return out;\n }, [selectedDate]);\n\n async function confirm() {\n if (!selectedOption || !selectedSlotKey) return;\n setSubmitting(true);\n try {\n const when = new Date(selectedSlotKey).toLocaleString();\n const note = `Party of ${partySize} \xB7 ${when}${notes ? ` \xB7 ${notes}` : ""}`;\n await addItem(selectedOption, 1, { specialInstructions: note });\n router.push("/checkout");\n } catch {\n setSubmitting(false);\n }\n }\n\n if (options.length === 0) {\n return (\n <p className="text-muted-foreground">\n No bookable seating options yet. Add Service-type products to your catalogue\n (e.g. "Two-top", "Four-top", "Long table for 12") to enable reservations.\n </p>\n );\n }\n\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n <div className="space-y-6">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Seating\n </p>\n <div className="space-y-2">\n {options.map((o) => {\n const active = selectedOption?.id === o.id;\n return (\n <button\n key={o.id}\n type="button"\n onClick={() => setSelectedOption(o)}\n className={[\n "w-full text-left rounded-2xl border p-4 transition-colors",\n active\n ? "border-primary bg-primary/5"\n : "border-border bg-card hover:border-foreground/30",\n ].join(" ")}\n >\n <p className="font-semibold text-sm m-0">{o.name}</p>\n {o.description && (\n <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">{o.description}</p>\n )}\n </button>\n );\n })}\n </div>\n </div>\n\n <div>\n <label\n htmlFor="party"\n className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground block mb-3"\n >\n Party size\n </label>\n <div className="grid grid-cols-4 gap-1.5">\n {PARTY_SIZES.map((p) => {\n const active = partySize === p;\n return (\n <button\n key={p}\n type="button"\n onClick={() => setPartySize(p)}\n className={[\n "py-2 rounded-md text-sm font-semibold transition-colors",\n active\n ? "bg-foreground text-background"\n : "bg-background border border-border hover:border-foreground",\n ].join(" ")}\n >\n {p}\n </button>\n );\n })}\n </div>\n </div>\n\n <div>\n <label\n htmlFor="notes"\n className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground block mb-3"\n >\n Notes (optional)\n </label>\n <textarea\n id="notes"\n rows={3}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder="Allergies, occasion, accessibility\u2026"\n className="w-full px-4 py-2.5 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm resize-y"\n />\n </div>\n </div>\n\n <div className="rounded-2xl border border-border bg-card p-6">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Date\n </p>\n <div className="grid grid-cols-7 gap-1.5 mb-6">\n {dates.map((d) => {\n const active = d.toDateString() === selectedDate.toDateString();\n return (\n <button\n key={d.toISOString()}\n type="button"\n onClick={() => {\n setSelectedDate(d);\n setSelectedSlotKey(null);\n }}\n className={[\n "flex flex-col items-center justify-center py-2 rounded-md transition-colors",\n active\n ? "bg-foreground text-background"\n : "bg-background hover:bg-muted text-foreground",\n ].join(" ")}\n >\n <span className="text-[10px] uppercase tracking-wider opacity-60">\n {d.toLocaleString(undefined, { weekday: "short" })}\n </span>\n <span className="text-base font-semibold tabular-nums">{d.getDate()}</span>\n </button>\n );\n })}\n </div>\n\n {(["Lunch", "Dinner"] as const).map((service) => (\n <div key={service} className="mb-5 last:mb-0">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-2">\n {service}\n </p>\n <div className="grid grid-cols-3 sm:grid-cols-5 gap-2">\n {slots\n .filter((s) => s.service === service)\n .map((s) => {\n const key = s.date.toISOString();\n const active = selectedSlotKey === key;\n return (\n <button\n key={key}\n type="button"\n onClick={() => setSelectedSlotKey(key)}\n className={[\n "py-2 rounded-md text-sm tabular-nums transition-colors",\n active\n ? "bg-primary text-primary-foreground"\n : "bg-background border border-border hover:border-primary",\n ].join(" ")}\n >\n {s.label}\n </button>\n );\n })}\n </div>\n </div>\n ))}\n\n <button\n type="button"\n onClick={confirm}\n disabled={!selectedOption || !selectedSlotKey || submitting}\n className="w-full mt-4 inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"\n >\n {submitting\n ? "Confirming\u2026"\n : selectedSlotKey\n ? `Reserve for ${partySize} on ${new Date(selectedSlotKey).toLocaleDateString()} at ${new Date(selectedSlotKey).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`\n : "Pick a slot to reserve"}\n </button>\n </div>\n </div>\n );\n}\n' }, { "path": "app/reservations/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { ReservationsClient } from "./reservations-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Reserve a table \u2014 ${brand.name}`,\n description: "Pick a date, time, and party size. We\'ll hold your table.",\n};\n\nexport const revalidate = 3600;\n\nasync function getSeatingOptions(): Promise<Product[]> {\n const client = getServerClient();\n const r = await client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n if (!r.ok) return [];\n // Reservation options live as service products in the catalogue (e.g.\n // "Two-top", "Four-top", "Long table"). Filter to type=service.\n return r.value.items.filter((p) => p.type === "service");\n}\n\nexport default async function ReservationsPage() {\n const options = await getSeatingOptions();\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-14">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Reservations\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-3 -tracking-[0.02em]">\n Hold a table.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-10 max-w-xl">\n Bookings open 21 days in advance. SMS confirmation within a minute. Free\n cancellation up to 4 hours before.\n </p>\n\n <Suspense fallback={<ReservationsSkeleton />}>\n <ReservationsClient options={options} />\n </Suspense>\n </article>\n );\n}\n\nfunction ReservationsSkeleton() {\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n <div className="space-y-2">\n {Array.from({ length: 4 }).map((_, i) => (\n <div key={i} className="h-16 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n <div className="rounded-2xl border border-border bg-card p-8">\n <div className="h-5 w-40 bg-muted rounded mb-4 animate-pulse" />\n <div className="grid grid-cols-3 gap-2">\n {Array.from({ length: 9 }).map((_, i) => (\n <div key={i} className="h-10 bg-muted rounded animate-pulse" />\n ))}\n </div>\n </div>\n </div>\n );\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Restaurant storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Lora serif + Inter** \u2014 warm, food-friendly headings + clean body.\n- **Forest green + cream** \u2014 deep sage primary, warm cream background.\n- **Mid radius**: `0.625rem`.\n- Schema.org `@type` is `Restaurant`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero with daily-special badge,\n collection strips, category grid\n shop/page.tsx Full menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. brunch, mains)\n categories/[slug]/page.tsx Category landing\n\n reservations/page.tsx \u2B50 Restaurant-specific: reservation widget\n (date + lunch/dinner slot + party size)\n backed by service-type "seating" products\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery), returns/page.tsx (refunds), accessibility/page.tsx\n terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, Restaurant JSON-LD |\n| `app/page.tsx` | `brand.hero` (daily-special badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (reservations, dietary, delivery) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery + pickup) |\n| `app/returns/page.tsx` | `brand.returns` (refund + cancellation) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/reservations/*` | derives seating from service-type products in the catalogue |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Restaurant-specific notes\n\n- **Reservations are service-type products.** Add seating options like "Two-top", "Four-top", "Long table for 12" to the catalogue with `type: "service"`. The `/reservations` page filters them in.\n- The reservation widget adds the chosen seating to cart with the date, time, party size, and notes as cart-item notes; checkout finalises the booking via Cimplify\'s order pipeline.\n- Schema.org `@type` is `Restaurant`. `servesCuisine` etc. can be added to the JSON-LD object in `app/layout.tsx` if needed.\n- Mock seed: `--seed restaurant` (Mama\'s Kitchen).\n\n## Known TODOs\n\n- `/reservations` widget assumes every slot is bookable. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to actually-free windows.\n- Contact form + newsletter fake their submits.\n- Hero / dish photography is Unsplash \u2014 replace with merchant photos.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed restaurant --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed restaurant",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.109.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
241
+ ` }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/reservations/reservations-client.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, useState } from "react";\nimport { useRouter } from "next/navigation";\nimport type { Product } from "@cimplify/sdk";\nimport { useCart } from "@cimplify/sdk/react";\n\nconst PARTY_SIZES = [2, 3, 4, 5, 6, 8, 10, 12];\n\n/**\n * Reservation flow. Reservations are modelled as service-type products in\n * the catalogue (e.g. "Two-top", "Four-top", "Long table"). The user picks\n * a seating option, date, time, and party size; we add to cart with the\n * reservation details as item notes; checkout finalises the booking.\n */\nexport function ReservationsClient({ options }: { options: Product[] }) {\n const router = useRouter();\n const { addItem } = useCart();\n const [selectedOption, setSelectedOption] = useState<Product | undefined>(options[0]);\n const [selectedDate, setSelectedDate] = useState<Date>(() => new Date());\n const [selectedSlotKey, setSelectedSlotKey] = useState<string | null>(null);\n const [partySize, setPartySize] = useState<number>(2);\n const [notes, setNotes] = useState("");\n const [submitting, setSubmitting] = useState(false);\n\n const dates = useMemo(() => {\n const now = new Date();\n return Array.from({ length: 14 }).map((_, i) => {\n const d = new Date(now);\n d.setDate(now.getDate() + i);\n d.setHours(0, 0, 0, 0);\n return d;\n });\n }, []);\n\n // Restaurant slots: lunch 12\u201314:30, dinner 18\u201321:30, every 30 min.\n const slots = useMemo(() => {\n const out: { date: Date; label: string; service: "Lunch" | "Dinner" }[] = [];\n for (let h = 12; h <= 14; h++) {\n for (const m of [0, 30]) {\n const d = new Date(selectedDate);\n d.setHours(h, m, 0, 0);\n out.push({ date: d, label: `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`, service: "Lunch" });\n }\n }\n for (let h = 18; h <= 21; h++) {\n for (const m of [0, 30]) {\n const d = new Date(selectedDate);\n d.setHours(h, m, 0, 0);\n out.push({ date: d, label: `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`, service: "Dinner" });\n }\n }\n return out;\n }, [selectedDate]);\n\n async function confirm() {\n if (!selectedOption || !selectedSlotKey) return;\n setSubmitting(true);\n try {\n const when = new Date(selectedSlotKey).toLocaleString();\n const note = `Party of ${partySize} \xB7 ${when}${notes ? ` \xB7 ${notes}` : ""}`;\n await addItem(selectedOption, 1, { specialInstructions: note });\n router.push("/checkout");\n } catch {\n setSubmitting(false);\n }\n }\n\n if (options.length === 0) {\n return (\n <p className="text-muted-foreground">\n No bookable seating options yet. Add Service-type products to your catalogue\n (e.g. "Two-top", "Four-top", "Long table for 12") to enable reservations.\n </p>\n );\n }\n\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n <div className="space-y-6">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Seating\n </p>\n <div className="space-y-2">\n {options.map((o) => {\n const active = selectedOption?.id === o.id;\n return (\n <button\n key={o.id}\n type="button"\n onClick={() => setSelectedOption(o)}\n className={[\n "w-full text-left rounded-2xl border p-4 transition-colors",\n active\n ? "border-primary bg-primary/5"\n : "border-border bg-card hover:border-foreground/30",\n ].join(" ")}\n >\n <p className="font-semibold text-sm m-0">{o.name}</p>\n {o.description && (\n <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">{o.description}</p>\n )}\n </button>\n );\n })}\n </div>\n </div>\n\n <div>\n <label\n htmlFor="party"\n className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground block mb-3"\n >\n Party size\n </label>\n <div className="grid grid-cols-4 gap-1.5">\n {PARTY_SIZES.map((p) => {\n const active = partySize === p;\n return (\n <button\n key={p}\n type="button"\n onClick={() => setPartySize(p)}\n className={[\n "py-2 rounded-md text-sm font-semibold transition-colors",\n active\n ? "bg-foreground text-background"\n : "bg-background border border-border hover:border-foreground",\n ].join(" ")}\n >\n {p}\n </button>\n );\n })}\n </div>\n </div>\n\n <div>\n <label\n htmlFor="notes"\n className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground block mb-3"\n >\n Notes (optional)\n </label>\n <textarea\n id="notes"\n rows={3}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder="Allergies, occasion, accessibility\u2026"\n className="w-full px-4 py-2.5 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm resize-y"\n />\n </div>\n </div>\n\n <div className="rounded-2xl border border-border bg-card p-6">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Date\n </p>\n <div className="grid grid-cols-7 gap-1.5 mb-6">\n {dates.map((d) => {\n const active = d.toDateString() === selectedDate.toDateString();\n return (\n <button\n key={d.toISOString()}\n type="button"\n onClick={() => {\n setSelectedDate(d);\n setSelectedSlotKey(null);\n }}\n className={[\n "flex flex-col items-center justify-center py-2 rounded-md transition-colors",\n active\n ? "bg-foreground text-background"\n : "bg-background hover:bg-muted text-foreground",\n ].join(" ")}\n >\n <span className="text-[10px] uppercase tracking-wider opacity-60">\n {d.toLocaleString(undefined, { weekday: "short" })}\n </span>\n <span className="text-base font-semibold tabular-nums">{d.getDate()}</span>\n </button>\n );\n })}\n </div>\n\n {(["Lunch", "Dinner"] as const).map((service) => (\n <div key={service} className="mb-5 last:mb-0">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-2">\n {service}\n </p>\n <div className="grid grid-cols-3 sm:grid-cols-5 gap-2">\n {slots\n .filter((s) => s.service === service)\n .map((s) => {\n const key = s.date.toISOString();\n const active = selectedSlotKey === key;\n return (\n <button\n key={key}\n type="button"\n onClick={() => setSelectedSlotKey(key)}\n className={[\n "py-2 rounded-md text-sm tabular-nums transition-colors",\n active\n ? "bg-primary text-primary-foreground"\n : "bg-background border border-border hover:border-primary",\n ].join(" ")}\n >\n {s.label}\n </button>\n );\n })}\n </div>\n </div>\n ))}\n\n <button\n type="button"\n onClick={confirm}\n disabled={!selectedOption || !selectedSlotKey || submitting}\n className="w-full mt-4 inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"\n >\n {submitting\n ? "Confirming\u2026"\n : selectedSlotKey\n ? `Reserve for ${partySize} on ${new Date(selectedSlotKey).toLocaleDateString()} at ${new Date(selectedSlotKey).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`\n : "Pick a slot to reserve"}\n </button>\n </div>\n </div>\n );\n}\n' }, { "path": "app/reservations/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { ReservationsClient } from "./reservations-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Reserve a table \u2014 ${brand.name}`,\n description: "Pick a date, time, and party size. We\'ll hold your table.",\n};\n\nexport const revalidate = 3600;\n\nasync function getSeatingOptions(): Promise<Product[]> {\n const client = getServerClient();\n const r = await client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n if (!r.ok) return [];\n // Reservation options live as service products in the catalogue (e.g.\n // "Two-top", "Four-top", "Long table"). Filter to type=service.\n return r.value.items.filter((p) => p.type === "service");\n}\n\nexport default async function ReservationsPage() {\n const options = await getSeatingOptions();\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-14">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Reservations\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-3 -tracking-[0.02em]">\n Hold a table.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-10 max-w-xl">\n Bookings open 21 days in advance. SMS confirmation within a minute. Free\n cancellation up to 4 hours before.\n </p>\n\n <Suspense fallback={<ReservationsSkeleton />}>\n <ReservationsClient options={options} />\n </Suspense>\n </article>\n );\n}\n\nfunction ReservationsSkeleton() {\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n <div className="space-y-2">\n {Array.from({ length: 4 }).map((_, i) => (\n <div key={i} className="h-16 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n <div className="rounded-2xl border border-border bg-card p-8">\n <div className="h-5 w-40 bg-muted rounded mb-4 animate-pulse" />\n <div className="grid grid-cols-3 gap-2">\n {Array.from({ length: 9 }).map((_, i) => (\n <div key={i} className="h-10 bg-muted rounded animate-pulse" />\n ))}\n </div>\n </div>\n </div>\n );\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Restaurant storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Lora serif + Inter** \u2014 warm, food-friendly headings + clean body.\n- **Forest green + cream** \u2014 deep sage primary, warm cream background.\n- **Mid radius**: `0.625rem`.\n- Schema.org `@type` is `Restaurant`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero with daily-special badge,\n collection strips, category grid\n shop/page.tsx Full menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. brunch, mains)\n categories/[slug]/page.tsx Category landing\n\n reservations/page.tsx \u2B50 Restaurant-specific: reservation widget\n (date + lunch/dinner slot + party size)\n backed by service-type "seating" products\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery), returns/page.tsx (refunds), accessibility/page.tsx\n terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, Restaurant JSON-LD |\n| `app/page.tsx` | `brand.hero` (daily-special badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (reservations, dietary, delivery) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery + pickup) |\n| `app/returns/page.tsx` | `brand.returns` (refund + cancellation) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/reservations/*` | derives seating from service-type products in the catalogue |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Restaurant-specific notes\n\n- **Reservations are service-type products.** Add seating options like "Two-top", "Four-top", "Long table for 12" to the catalogue with `type: "service"`. The `/reservations` page filters them in.\n- The reservation widget adds the chosen seating to cart with the date, time, party size, and notes as cart-item notes; checkout finalises the booking via Cimplify\'s order pipeline.\n- Schema.org `@type` is `Restaurant`. `servesCuisine` etc. can be added to the JSON-LD object in `app/layout.tsx` if needed.\n- Mock seed: `--seed restaurant` (Mama\'s Kitchen).\n\n## Known TODOs\n\n- `/reservations` widget assumes every slot is bookable. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to actually-free windows.\n- Contact form + newsletter fake their submits.\n- Hero / dish photography is Unsplash \u2014 replace with merchant photos.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed restaurant --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed restaurant",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.110.2",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
242
242
  * Canonical absolute URL. The platform overwrites this at build time with the
243
243
  * storefront's primary domain; the default only applies to local builds. It's
244
244
  * per-deploy config, not per-request, so it stays a static constant \u2014 keeping
@@ -815,7 +815,7 @@ function Field({
815
815
  </div>
816
816
  );
817
817
  }
818
- ` }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n return { kind: "cancelled" };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n });\n}\n' }, { "path": "app/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { collection: colRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\nexport const revalidate = 3600;\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <h1 className="font-serif text-3xl mt-0 mb-3">Thanks \u2014 your order is confirmed</h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code>\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* Serene Spa \u2014 calm sand + sage palette, soft contrast.\n Tweak primary to retheme everything. */\n@theme {\n --color-background: oklch(0.98 0.012 90);\n --color-foreground: oklch(0.22 0.02 140);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.22 0.02 140);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.22 0.02 140);\n --color-primary: oklch(0.46 0.06 150);\n --color-primary-foreground: oklch(0.99 0.01 90);\n --color-secondary: oklch(0.93 0.025 90);\n --color-secondary-foreground: oklch(0.3 0.04 140);\n --color-muted: oklch(0.95 0.018 90);\n --color-muted-foreground: oklch(0.5 0.02 140);\n --color-accent: oklch(0.9 0.035 90);\n --color-accent-foreground: oklch(0.3 0.04 140);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.91 0.018 90);\n --color-input: oklch(0.93 0.018 90);\n --color-ring: oklch(0.46 0.06 150);\n --radius: 1rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-serif);\n letter-spacing: -0.015em;\n font-weight: 500;\n }\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/book/book-client.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\nimport type { Product } from "@cimplify/sdk";\nimport type { AvailableSlot } from "@cimplify/sdk";\nimport { DateSlotPicker, Price, useCart } from "@cimplify/sdk/react";\nimport { brand } from "@/lib/brand";\n\n/**\n * Booking flow:\n * 1. Pick a treatment (left rail) \u2014 drives the SDK availability fetch.\n * 2. SDK <DateSlotPicker> handles date + slot selection, fetching real\n * availability via `useServiceAvailability` against the configured\n * backend (mock in dev, Cimplify scheduling API in prod).\n * 3. Add to cart with the chosen slot as a cart-item note;\n * Cimplify Checkout finalises the booking.\n */\nexport function BookClient({ treatments }: { treatments: Product[] }) {\n const router = useRouter();\n const { addItem } = useCart();\n const [selectedTreatment, setSelectedTreatment] = useState<Product | undefined>(treatments[0]);\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [submitting, setSubmitting] = useState(false);\n\n async function confirm() {\n if (!selectedTreatment || !selectedSlot) return;\n setSubmitting(true);\n try {\n const slotLabel = new Date(selectedSlot.start_time).toLocaleString(undefined, {\n weekday: "short",\n month: "short",\n day: "numeric",\n hour: "2-digit",\n minute: "2-digit",\n });\n await addItem(selectedTreatment, 1, {\n specialInstructions: `Booked for ${slotLabel}`,\n });\n router.push("/checkout");\n } catch {\n setSubmitting(false);\n }\n }\n\n if (treatments.length === 0) {\n return (\n <p className="text-muted-foreground">\n No bookable treatments yet. Add a Service-type product to your catalogue first.\n </p>\n );\n }\n\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n {/* Treatments */}\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Treatment\n </p>\n <div className="space-y-2">\n {treatments.map((t) => {\n const active = selectedTreatment?.id === t.id;\n return (\n <button\n key={t.id}\n type="button"\n onClick={() => {\n setSelectedTreatment(t);\n setSelectedSlot(null);\n }}\n className={[\n "w-full text-left rounded-2xl border p-4 transition-colors",\n active\n ? "border-primary bg-primary/5"\n : "border-border bg-card hover:border-foreground/30",\n ].join(" ")}\n >\n <div className="flex items-center justify-between gap-3">\n <div className="min-w-0">\n <p className="font-semibold text-sm m-0 truncate">{t.name}</p>\n <p className="text-xs text-muted-foreground m-0">\n {t.duration_minutes ? `${t.duration_minutes} min \xB7 ` : ""}\n <Price amount={t.default_price} />\n </p>\n </div>\n {active && (\n <span className="grid place-items-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs">\n \u2713\n </span>\n )}\n </div>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Date + slots \u2014 SDK <DateSlotPicker> fetches real availability */}\n <div className="rounded-2xl border border-border bg-card p-4 sm:p-6">\n {selectedTreatment ? (\n <DateSlotPicker\n serviceId={selectedTreatment.id}\n selectedSlot={selectedSlot}\n onSlotSelect={(slot) => setSelectedSlot(slot)}\n daysToShow={selectedTreatment.scheduling_mode === "multi_day" ? 14 : 7}\n schedulingMode={selectedTreatment.scheduling_mode}\n durationUnit={selectedTreatment.duration_unit}\n durationValue={selectedTreatment.duration_value}\n contactPhone={brand.contact.phone}\n />\n ) : (\n <p className="text-sm text-muted-foreground">Pick a treatment to see availability.</p>\n )}\n\n <button\n type="button"\n onClick={confirm}\n disabled={!selectedTreatment || !selectedSlot || submitting}\n className="w-full mt-6 inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"\n >\n {submitting\n ? "Confirming\u2026"\n : selectedSlot\n ? `Book ${selectedTreatment?.name} at ${new Date(selectedSlot.start_time).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`\n : "Pick a slot to book"}\n </button>\n </div>\n </div>\n );\n}\n' }, { "path": "app/book/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { BookClient } from "./book-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Book a treatment \u2014 ${brand.name}`,\n description: "Pick a treatment, pick a slot, you\'re booked.",\n};\n\nexport const revalidate = 3600;\n\nasync function getTreatments(): Promise<Product[]> {\n const client = getServerClient();\n const r = await client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n if (!r.ok) return [];\n // Booking flow only handles service-typed products.\n return r.value.items.filter((p) => p.type === "service");\n}\n\nexport default async function BookPage() {\n const treatments = await getTreatments();\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n Book a treatment\n </p>\n <h1 className="text-[clamp(2rem,5vw,3rem)] font-semibold mb-3 -tracking-[0.02em]">\n Pick a treatment.<br />Pick a slot.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-10 max-w-xl">\n Available windows for the next two weeks. Confirmation by SMS within a minute of\n booking. Free cancellation up to 24 hours before.\n </p>\n\n <Suspense fallback={<BookSkeleton />}>\n <BookClient treatments={treatments} />\n </Suspense>\n </article>\n );\n}\n\nfunction BookSkeleton() {\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n <div className="space-y-2">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-16 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n <div className="rounded-2xl border border-border bg-card p-8">\n <div className="h-5 w-40 bg-muted rounded mb-4 animate-pulse" />\n <div className="grid grid-cols-3 gap-2">\n {Array.from({ length: 9 }).map((_, i) => (\n <div key={i} className="h-10 bg-muted rounded animate-pulse" />\n ))}\n </div>\n </div>\n </div>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Services / spa / wellness storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Fraunces serif + Inter** \u2014 warm, slightly editorial headings.\n- **Sand + sage palette** \u2014 warm cream background, deep sage primary.\n- **Generous radius**: `1rem` \u2014 calm, soft.\n- Lower-contrast than retail / fashion.\n- Schema.org `@type` is `BeautySalon`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero ("Now booking"), service strips,\n category grid, newsletter\n shop/page.tsx Treatment menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing (e.g. memberships, packages)\n\n book/page.tsx \u2B50 Services-specific: full booking widget\n (treatment + 14-day date + 30-min slot grid)\n \u2192 adds to cart with slot as note \u2192 /checkout\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx (find a booking)\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (what to expect at the studio), returns/page.tsx (cancellations),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, BeautySalon JSON-LD |\n| `app/page.tsx` | `brand.hero` ("Now booking") |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (booking, contraindications, memberships) |\n| `app/shipping/page.tsx` | `brand.shipping` (what to expect at the studio) |\n| `app/returns/page.tsx` | `brand.returns` (cancellation policy) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` ("find a booking") |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/book/*` | derives bookable services from product list (`type: "service"`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Services-specific notes\n\n- **Bookable services are `type: "service"` products.** The `/book` page filters the product list to type=service and treats them as treatments. Their `duration_minutes` is shown on the chip.\n- Cart-item notes carry the booked time slot \u2014 checkout sends them through Cimplify\'s order pipeline. The merchant\'s downstream system reads the note to schedule the appointment.\n- Mock seed: `--seed services` (Serene Spa).\n\n## Known TODOs\n\n- `/book` widget shows every slot as available. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to genuinely-free windows.\n- Contact + newsletter fake submits.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for service scheduling, booking widgets, and add-on selectors** \u2014 the SDK already gets price math, slot matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add product-page\ncimplify add booking-card\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** (`scheduled_start`, `scheduled_end`, `staff_id`, `customer_inputs`, etc.) unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed services --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed services",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.109.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
818
+ ` }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n return { kind: "cancelled" };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n });\n}\n' }, { "path": "app/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { collection: colRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\nexport const revalidate = 3600;\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <h1 className="font-serif text-3xl mt-0 mb-3">Thanks \u2014 your order is confirmed</h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code>\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* Serene Spa \u2014 calm sand + sage palette, soft contrast.\n Tweak primary to retheme everything. */\n@theme {\n --color-background: oklch(0.98 0.012 90);\n --color-foreground: oklch(0.22 0.02 140);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.22 0.02 140);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.22 0.02 140);\n --color-primary: oklch(0.46 0.06 150);\n --color-primary-foreground: oklch(0.99 0.01 90);\n --color-secondary: oklch(0.93 0.025 90);\n --color-secondary-foreground: oklch(0.3 0.04 140);\n --color-muted: oklch(0.95 0.018 90);\n --color-muted-foreground: oklch(0.5 0.02 140);\n --color-accent: oklch(0.9 0.035 90);\n --color-accent-foreground: oklch(0.3 0.04 140);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.91 0.018 90);\n --color-input: oklch(0.93 0.018 90);\n --color-ring: oklch(0.46 0.06 150);\n --radius: 1rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-serif);\n letter-spacing: -0.015em;\n font-weight: 500;\n }\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/book/book-client.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\nimport type { Product } from "@cimplify/sdk";\nimport type { AvailableSlot } from "@cimplify/sdk";\nimport { DateSlotPicker, Price, useCart } from "@cimplify/sdk/react";\nimport { brand } from "@/lib/brand";\n\n/**\n * Booking flow:\n * 1. Pick a treatment (left rail) \u2014 drives the SDK availability fetch.\n * 2. SDK <DateSlotPicker> handles date + slot selection, fetching real\n * availability via `useServiceAvailability` against the configured\n * backend (mock in dev, Cimplify scheduling API in prod).\n * 3. Add to cart with the chosen slot as a cart-item note;\n * Cimplify Checkout finalises the booking.\n */\nexport function BookClient({ treatments }: { treatments: Product[] }) {\n const router = useRouter();\n const { addItem } = useCart();\n const [selectedTreatment, setSelectedTreatment] = useState<Product | undefined>(treatments[0]);\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [submitting, setSubmitting] = useState(false);\n\n async function confirm() {\n if (!selectedTreatment || !selectedSlot) return;\n setSubmitting(true);\n try {\n const slotLabel = new Date(selectedSlot.start_time).toLocaleString(undefined, {\n weekday: "short",\n month: "short",\n day: "numeric",\n hour: "2-digit",\n minute: "2-digit",\n });\n await addItem(selectedTreatment, 1, {\n specialInstructions: `Booked for ${slotLabel}`,\n });\n router.push("/checkout");\n } catch {\n setSubmitting(false);\n }\n }\n\n if (treatments.length === 0) {\n return (\n <p className="text-muted-foreground">\n No bookable treatments yet. Add a Service-type product to your catalogue first.\n </p>\n );\n }\n\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n {/* Treatments */}\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Treatment\n </p>\n <div className="space-y-2">\n {treatments.map((t) => {\n const active = selectedTreatment?.id === t.id;\n return (\n <button\n key={t.id}\n type="button"\n onClick={() => {\n setSelectedTreatment(t);\n setSelectedSlot(null);\n }}\n className={[\n "w-full text-left rounded-2xl border p-4 transition-colors",\n active\n ? "border-primary bg-primary/5"\n : "border-border bg-card hover:border-foreground/30",\n ].join(" ")}\n >\n <div className="flex items-center justify-between gap-3">\n <div className="min-w-0">\n <p className="font-semibold text-sm m-0 truncate">{t.name}</p>\n <p className="text-xs text-muted-foreground m-0">\n {t.duration_minutes ? `${t.duration_minutes} min \xB7 ` : ""}\n <Price amount={t.default_price} />\n </p>\n </div>\n {active && (\n <span className="grid place-items-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs">\n \u2713\n </span>\n )}\n </div>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Date + slots \u2014 SDK <DateSlotPicker> fetches real availability */}\n <div className="rounded-2xl border border-border bg-card p-4 sm:p-6">\n {selectedTreatment ? (\n <DateSlotPicker\n serviceId={selectedTreatment.id}\n selectedSlot={selectedSlot}\n onSlotSelect={(slot) => setSelectedSlot(slot)}\n daysToShow={selectedTreatment.scheduling_mode === "multi_day" ? 14 : 7}\n schedulingMode={selectedTreatment.scheduling_mode}\n durationUnit={selectedTreatment.duration_unit}\n durationValue={selectedTreatment.duration_value}\n contactPhone={brand.contact.phone}\n />\n ) : (\n <p className="text-sm text-muted-foreground">Pick a treatment to see availability.</p>\n )}\n\n <button\n type="button"\n onClick={confirm}\n disabled={!selectedTreatment || !selectedSlot || submitting}\n className="w-full mt-6 inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"\n >\n {submitting\n ? "Confirming\u2026"\n : selectedSlot\n ? `Book ${selectedTreatment?.name} at ${new Date(selectedSlot.start_time).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`\n : "Pick a slot to book"}\n </button>\n </div>\n </div>\n );\n}\n' }, { "path": "app/book/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { BookClient } from "./book-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Book a treatment \u2014 ${brand.name}`,\n description: "Pick a treatment, pick a slot, you\'re booked.",\n};\n\nexport const revalidate = 3600;\n\nasync function getTreatments(): Promise<Product[]> {\n const client = getServerClient();\n const r = await client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n if (!r.ok) return [];\n // Booking flow only handles service-typed products.\n return r.value.items.filter((p) => p.type === "service");\n}\n\nexport default async function BookPage() {\n const treatments = await getTreatments();\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n Book a treatment\n </p>\n <h1 className="text-[clamp(2rem,5vw,3rem)] font-semibold mb-3 -tracking-[0.02em]">\n Pick a treatment.<br />Pick a slot.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-10 max-w-xl">\n Available windows for the next two weeks. Confirmation by SMS within a minute of\n booking. Free cancellation up to 24 hours before.\n </p>\n\n <Suspense fallback={<BookSkeleton />}>\n <BookClient treatments={treatments} />\n </Suspense>\n </article>\n );\n}\n\nfunction BookSkeleton() {\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n <div className="space-y-2">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-16 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n <div className="rounded-2xl border border-border bg-card p-8">\n <div className="h-5 w-40 bg-muted rounded mb-4 animate-pulse" />\n <div className="grid grid-cols-3 gap-2">\n {Array.from({ length: 9 }).map((_, i) => (\n <div key={i} className="h-10 bg-muted rounded animate-pulse" />\n ))}\n </div>\n </div>\n </div>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Services / spa / wellness storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Fraunces serif + Inter** \u2014 warm, slightly editorial headings.\n- **Sand + sage palette** \u2014 warm cream background, deep sage primary.\n- **Generous radius**: `1rem` \u2014 calm, soft.\n- Lower-contrast than retail / fashion.\n- Schema.org `@type` is `BeautySalon`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero ("Now booking"), service strips,\n category grid, newsletter\n shop/page.tsx Treatment menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing (e.g. memberships, packages)\n\n book/page.tsx \u2B50 Services-specific: full booking widget\n (treatment + 14-day date + 30-min slot grid)\n \u2192 adds to cart with slot as note \u2192 /checkout\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx (find a booking)\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (what to expect at the studio), returns/page.tsx (cancellations),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, BeautySalon JSON-LD |\n| `app/page.tsx` | `brand.hero` ("Now booking") |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (booking, contraindications, memberships) |\n| `app/shipping/page.tsx` | `brand.shipping` (what to expect at the studio) |\n| `app/returns/page.tsx` | `brand.returns` (cancellation policy) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` ("find a booking") |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/book/*` | derives bookable services from product list (`type: "service"`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Services-specific notes\n\n- **Bookable services are `type: "service"` products.** The `/book` page filters the product list to type=service and treats them as treatments. Their `duration_minutes` is shown on the chip.\n- Cart-item notes carry the booked time slot \u2014 checkout sends them through Cimplify\'s order pipeline. The merchant\'s downstream system reads the note to schedule the appointment.\n- Mock seed: `--seed services` (Serene Spa).\n\n## Known TODOs\n\n- `/book` widget shows every slot as available. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to genuinely-free windows.\n- Contact + newsletter fake submits.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for service scheduling, booking widgets, and add-on selectors** \u2014 the SDK already gets price math, slot matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add product-page\ncimplify add booking-card\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** (`scheduled_start`, `scheduled_end`, `staff_id`, `customer_inputs`, etc.) unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed services --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed services",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.110.2",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
819
819
  * Canonical absolute URL. The platform overwrites this at build time with the
820
820
  * storefront's primary domain; the default only applies to local builds. It's
821
821
  * per-deploy config, not per-request, so it stays a static constant \u2014 keeping
@@ -1411,7 +1411,7 @@ function Field({
1411
1411
  </div>
1412
1412
  );
1413
1413
  }
1414
- ` }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n return { kind: "cancelled" };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n });\n}\n' }, { "path": "app/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { collection: colRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\nexport const revalidate = 3600;\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <h1 className="font-serif text-3xl mt-0 mb-3">Thanks \u2014 your order is confirmed</h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code>\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n/* Scan the SDK\'s bundled JS so utility classes used inside its components\n land in the compiled stylesheet alongside our own. */\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* The SDK declares its design tokens with `@theme reference`, meaning the\n consumer must supply the actual values. Warm bakery palette below \u2014 swap\n these to retheme the entire SDK + storefront in one place. */\n@theme {\n --color-background: oklch(0.985 0.007 80);\n --color-foreground: oklch(0.18 0.01 60);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.18 0.01 60);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.18 0.01 60);\n --color-primary: oklch(0.45 0.16 40);\n --color-primary-foreground: oklch(0.99 0 0);\n --color-secondary: oklch(0.95 0.02 80);\n --color-secondary-foreground: oklch(0.25 0.04 50);\n --color-muted: oklch(0.96 0.01 80);\n --color-muted-foreground: oklch(0.5 0.02 60);\n --color-accent: oklch(0.92 0.05 70);\n --color-accent-foreground: oklch(0.3 0.06 40);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.9 0.02 80);\n --color-input: oklch(0.92 0.02 80);\n --color-ring: oklch(0.6 0.1 40);\n --radius: 0.75rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-serif);\n letter-spacing: -0.01em;\n }\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Bakery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\nTo rebrand this storefront end-to-end as a different merchant:\n\n1. **Edit `lib/brand.ts`.** Every visible string reads from this file.\n2. **Edit `app/globals.css`** \u2014 the `@theme { \u2026 }` block holds the design tokens (palette, radius, font references).\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nThat is the entire rebrand. Do not modify any `.tsx` file in `app/` or `components/` for content changes \u2014 they are design-only and read from `brand`.\n\n## Rules\n\n- \u2705 All content edits \u2192 `lib/brand.ts`\n- \u2705 All design-token edits \u2192 `app/globals.css` `@theme` block\n- \u2705 Pages use **ISR**: `export const revalidate = <seconds>` + SDK reads with `cacheOptions: { revalidate, tags }`\n- \u2705 Client islands (header active link, cart pill, product modal) live behind `<Suspense>`\n- \u274C Don't hardcode strings. If a string isn't in `brand`, add a new field to the `Brand` interface.\n- \u274C Don't enable `cacheComponents: true` in `next.config.ts`. We deploy on Cloudflare Workers; `'use cache'` postponed state blows past CF's 128MB zlib limit.\n- \u274C Don't add `'use cache'`, `cacheTag`, or `cacheLife` anywhere. Use SDK `cacheOptions` instead.\n- \u274C Don't use `unstable_cache` \u2014 gone in Next 16.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero, collection strips, category grid\n shop/page.tsx Full catalogue (SDK <CataloguePage/>)\n search/page.tsx Search (SDK <SearchPage/>)\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx SDK <CartPage/>\n checkout/page.tsx SDK <CheckoutPage/>\n orders/[id]/page.tsx Post-checkout confirmation (also target of /track-order)\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx redirect \u2192 hosted OAuth, then /account\n signup/page.tsx redirect \u2192 hosted OAuth, then /account\n\n contact/page.tsx Contact form (Server Action wiring TODO; currently fakes submit)\n track-order/page.tsx Guest order lookup \u2192 /orders/[id]\n\n about/page.tsx Brand story\n faq/page.tsx FAQ\n shipping/page.tsx Standalone shipping policy\n returns/page.tsx Standalone returns policy\n accessibility/page.tsx Accessibility statement (WCAG 2.1 AA)\n terms/page.tsx Terms of Service\n privacy/page.tsx Privacy Policy\n\n sitemap-page/page.tsx Human-readable HTML sitemap\n sitemap.ts XML sitemap (search engines)\n robots.ts robots.txt\n llms.txt/route.ts LLM-friendly Markdown index\n opensearch.xml/route.ts Browser address-bar search description\n error.tsx, not-found.tsx Global boundaries\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (Organization JSON-LD), `metadataBase` |\n| `app/page.tsx` | `brand.hero` (Hero badge / title / subtitle) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/login/page.tsx`, `app/signup/page.tsx` | `brand.account` (metadata only \u2014 both redirect to `/account`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `app/opensearch.xml/route.ts` | `brand.shortName`, `brand.name` |\n| `app/sitemap.ts`, `robots.ts` | derives URLs from product/category data |\n| `app/not-found.tsx` | `brand.name` (page title) |\n| `components/header.tsx` | `brand.shortName`, `brand.microTag`, `brand.header.nav` |\n| `components/footer.tsx` | `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Bakery-specific notes\n\n- Product detail uses a **URL-driven modal** (`?product=<slug>`), not a static `/products/[slug]` route. Fits impulse-purchase food UX.\n- Schema.org `@type` is `Bakery` \u2014 set in `brand.schemaType`.\n- Mock seed: `--seed default`. To preview a different industry, edit `dev:mock` in `package.json`.\n\n## Known TODOs (not blocking, but worth knowing)\n\n- `app/contact/contact-form.tsx` fakes a successful submit. In production, wire a Server Action that calls `client.support.sendMessage(...)` from `@cimplify/sdk/server`.\n- `components/newsletter.tsx` (homepage strip) similarly fakes submit. Wire to a real list provider.\n- Hero / lookbook imagery is Unsplash placeholder \u2014 replace with merchant assets.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev # boots mock + Next dev server\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed default --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed default",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.109.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
1414
+ ` }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n return { kind: "cancelled" };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n });\n}\n' }, { "path": "app/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { collection: colRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\nexport const revalidate = 3600;\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <h1 className="font-serif text-3xl mt-0 mb-3">Thanks \u2014 your order is confirmed</h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code>\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n/* Scan the SDK\'s bundled JS so utility classes used inside its components\n land in the compiled stylesheet alongside our own. */\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* The SDK declares its design tokens with `@theme reference`, meaning the\n consumer must supply the actual values. Warm bakery palette below \u2014 swap\n these to retheme the entire SDK + storefront in one place. */\n@theme {\n --color-background: oklch(0.985 0.007 80);\n --color-foreground: oklch(0.18 0.01 60);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.18 0.01 60);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.18 0.01 60);\n --color-primary: oklch(0.45 0.16 40);\n --color-primary-foreground: oklch(0.99 0 0);\n --color-secondary: oklch(0.95 0.02 80);\n --color-secondary-foreground: oklch(0.25 0.04 50);\n --color-muted: oklch(0.96 0.01 80);\n --color-muted-foreground: oklch(0.5 0.02 60);\n --color-accent: oklch(0.92 0.05 70);\n --color-accent-foreground: oklch(0.3 0.06 40);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.9 0.02 80);\n --color-input: oklch(0.92 0.02 80);\n --color-ring: oklch(0.6 0.1 40);\n --radius: 0.75rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-serif);\n letter-spacing: -0.01em;\n }\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Bakery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\nTo rebrand this storefront end-to-end as a different merchant:\n\n1. **Edit `lib/brand.ts`.** Every visible string reads from this file.\n2. **Edit `app/globals.css`** \u2014 the `@theme { \u2026 }` block holds the design tokens (palette, radius, font references).\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nThat is the entire rebrand. Do not modify any `.tsx` file in `app/` or `components/` for content changes \u2014 they are design-only and read from `brand`.\n\n## Rules\n\n- \u2705 All content edits \u2192 `lib/brand.ts`\n- \u2705 All design-token edits \u2192 `app/globals.css` `@theme` block\n- \u2705 Pages use **ISR**: `export const revalidate = <seconds>` + SDK reads with `cacheOptions: { revalidate, tags }`\n- \u2705 Client islands (header active link, cart pill, product modal) live behind `<Suspense>`\n- \u274C Don't hardcode strings. If a string isn't in `brand`, add a new field to the `Brand` interface.\n- \u274C Don't enable `cacheComponents: true` in `next.config.ts`. We deploy on Cloudflare Workers; `'use cache'` postponed state blows past CF's 128MB zlib limit.\n- \u274C Don't add `'use cache'`, `cacheTag`, or `cacheLife` anywhere. Use SDK `cacheOptions` instead.\n- \u274C Don't use `unstable_cache` \u2014 gone in Next 16.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero, collection strips, category grid\n shop/page.tsx Full catalogue (SDK <CataloguePage/>)\n search/page.tsx Search (SDK <SearchPage/>)\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx SDK <CartPage/>\n checkout/page.tsx SDK <CheckoutPage/>\n orders/[id]/page.tsx Post-checkout confirmation (also target of /track-order)\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx redirect \u2192 hosted OAuth, then /account\n signup/page.tsx redirect \u2192 hosted OAuth, then /account\n\n contact/page.tsx Contact form (Server Action wiring TODO; currently fakes submit)\n track-order/page.tsx Guest order lookup \u2192 /orders/[id]\n\n about/page.tsx Brand story\n faq/page.tsx FAQ\n shipping/page.tsx Standalone shipping policy\n returns/page.tsx Standalone returns policy\n accessibility/page.tsx Accessibility statement (WCAG 2.1 AA)\n terms/page.tsx Terms of Service\n privacy/page.tsx Privacy Policy\n\n sitemap-page/page.tsx Human-readable HTML sitemap\n sitemap.ts XML sitemap (search engines)\n robots.ts robots.txt\n llms.txt/route.ts LLM-friendly Markdown index\n opensearch.xml/route.ts Browser address-bar search description\n error.tsx, not-found.tsx Global boundaries\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (Organization JSON-LD), `metadataBase` |\n| `app/page.tsx` | `brand.hero` (Hero badge / title / subtitle) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/login/page.tsx`, `app/signup/page.tsx` | `brand.account` (metadata only \u2014 both redirect to `/account`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `app/opensearch.xml/route.ts` | `brand.shortName`, `brand.name` |\n| `app/sitemap.ts`, `robots.ts` | derives URLs from product/category data |\n| `app/not-found.tsx` | `brand.name` (page title) |\n| `components/header.tsx` | `brand.shortName`, `brand.microTag`, `brand.header.nav` |\n| `components/footer.tsx` | `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Bakery-specific notes\n\n- Product detail uses a **URL-driven modal** (`?product=<slug>`), not a static `/products/[slug]` route. Fits impulse-purchase food UX.\n- Schema.org `@type` is `Bakery` \u2014 set in `brand.schemaType`.\n- Mock seed: `--seed default`. To preview a different industry, edit `dev:mock` in `package.json`.\n\n## Known TODOs (not blocking, but worth knowing)\n\n- `app/contact/contact-form.tsx` fakes a successful submit. In production, wire a Server Action that calls `client.support.sendMessage(...)` from `@cimplify/sdk/server`.\n- `components/newsletter.tsx` (homepage strip) similarly fakes submit. Wire to a real list provider.\n- Hero / lookbook imagery is Unsplash placeholder \u2014 replace with merchant assets.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev # boots mock + Next dev server\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed default --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed default",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.110.2",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
1415
1415
  * Canonical absolute URL. The platform overwrites this at build time with the
1416
1416
  * storefront's primary domain; the default only applies to local builds. It's
1417
1417
  * per-deploy config, not per-request, so it stays a static constant \u2014 keeping
@@ -2269,7 +2269,7 @@ function Field({
2269
2269
  font-weight: 600;
2270
2270
  }
2271
2271
  }
2272
- ` }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-background transition-colors">Shop</Link>\n <span>/</span>\n <span className="text-background/90">{category.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {category.name}\n </h1>\n {category.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {category.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s no longer in stock. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Browse the shop\n </Link>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { AutoHero } from "@/components/auto-hero";\nimport { FitmentFinder } from "@/components/fitment-finder";\nimport { TrustBar } from "@/components/trust-bar";\nimport { ServiceBrief } from "@/components/service-brief";\nimport { Newsletter } from "@/components/newsletter";\nimport { SectionHeading } from "@/components/section-heading";\nimport { StoreProductCard } from "@/components/store-product-card";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: brand.hero.title,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const productsRes = await client.catalogue.getProducts(\n { limit: 12 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n const allProducts: Product[] = productsRes.ok ? productsRes.value.items : [];\n\n return {\n mostOrdered: allProducts.slice(0, 8),\n };\n}\n\nexport default async function HomePage() {\n const { mostOrdered } = await getHomeData();\n\n return (\n <>\n <AutoHero />\n\n <Suspense fallback={<FitmentFinderSkeleton />}>\n <FitmentFinder />\n </Suspense>\n\n <TrustBar />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Most ordered"\n title="What our garages keep on the shelf."\n description="The parts Driveline ships daily \u2014 oil, filters, brake pads, batteries. Every fitment verified by our parts team."\n link={{ label: "Shop everything", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={8} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {mostOrdered.map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <ServiceBrief />\n\n <Newsletter />\n </>\n );\n}\n\nfunction GridSkeleton({ count }: { count: number }) {\n return (\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n );\n}\n\nfunction FitmentFinderSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 -mt-10 sm:-mt-14 relative z-10">\n <div className="bg-card border border-border rounded-2xl p-6 sm:p-8 shadow-[0_8px_30px_rgb(0_0_0/0.06)]">\n <div className="h-7 w-48 bg-muted rounded mb-3 animate-pulse" />\n <div className="h-5 w-80 bg-muted rounded mb-6 animate-pulse" />\n <div className="grid grid-cols-1 md:grid-cols-[1.2fr_1.2fr_1fr_auto] gap-3">\n <div className="h-11 bg-muted rounded-xl animate-pulse" />\n <div className="h-11 bg-muted rounded-xl animate-pulse" />\n <div className="h-11 bg-muted rounded-xl animate-pulse" />\n <div className="h-11 w-36 bg-primary/30 rounded-xl animate-pulse" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Auto-parts storefront template\n\nIf you are an AI agent working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string, plus the `fitments` make/model/year tree.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` block for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_*` env vars.\n\n## Aesthetic\n\n- **Inter + JetBrains Mono** \u2014 clean modern sans, mono-spaced labels for spec-feel.\n- **Carbon-black foreground + sport-red accent.**\n- **Sharp corners**: `0.5rem` \u2014 workshop-modern.\n- **`AutoPartsStore`** is the Schema.org `@type` (set in `brand.schemaType`).\n\n## Page surface\n\n```\napp/\n page.tsx AutoHero + FitmentFinder + TrustBar + Most ordered\n grid + ServiceBrief + Newsletter\n shop/page.tsx SDK <CataloguePage/>. Reads `?fits=<tag>` to\n filter by fitment tag.\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page (Product JSON-LD)\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx SDK\n account/* + login/signup\n contact/, track-order/, about, faq, terms, privacy, shipping, returns,\n accessibility/, sitemap-page/ Brand-driven content pages\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (AutoPartsStore JSON-LD) |\n| `app/page.tsx` | `brand.hero`, `brand.fitments`, `brand.serviceBrief`, `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/auto-hero.tsx` | `brand.hero`, hard-coded trust-chip labels |\n| `components/fitment-finder.tsx` | `brand.fitments` (makes/models/years tree) |\n| `components/service-brief.tsx` | `brand.serviceBrief` (3 cards) |\n| `components/header.tsx`, `components/footer.tsx` | `brand.shortName`, `brand.header.nav`, `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/trust-bar.tsx` | `brand.trustItems` |\n| `components/newsletter.tsx` | `brand.newsletter` |\n\n## Auto-specific notes\n\n- **Fitment is tag-based, not a custom_attribute.** Each product carries `tags: ["fits:toyota", "fits:toyota:corolla"]` etc. or `["fits:universal"]`. The shop page reads `?fits=<tag>` and filters by tag prefix match. Universal-fit products bypass the filter.\n- **Adding a vehicle**: edit `brand.fitments.makes` in `lib/brand.ts`. Add `{ slug, name, models: [...] }`. Slug becomes the URL fragment.\n- **Adding a part with new fitment**: tag the product `fits:<make>` and (optionally) `fits:<make>:<model>`. No schema change needed.\n- **Partner-workshop fitting**: the mock doesn\'t model this; the storefront copy describes it. To wire it, eject the cart drawer and add a "Add fitting (GH\u20B580)" toggle that adds a service line.\n\n## Known TODOs\n\n- Contact form + newsletter signup currently fake their submit. Wire to a real provider.\n- The fitment finder uses `useRouter().push` to a query-string URL; the catalogue page-side filter is left as an exercise for the merchant (read the `fits` param and pass it as a SDK `getProducts({ tags: [...] })` filter).\n\n## Mock seed\n\nWired to `--seed auto` (Driveline Auto Parts). Edit `dev:mock` in `package.json` to preview another seed.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, fitment-tag filtering, services with scheduling) \u2014 the SDK already gets price math, axis matching, tag filtering, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed auto --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed auto",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.109.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **auto** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Carbon-black + sport-red palette, Inter + JetBrains Mono typography, workshop-modern aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed auto` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Driveline Auto Parts.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Auto-specific behaviour\n\n- **Fitment finder** (`components/fitment-finder.tsx`): Make \u2192 Model \u2192 Year selector on the home page. Submitting pushes `?fits=fits:<make>:<model>` onto `/shop`; the shop page filters the catalogue by matching `tags` on `MockProduct`. Products tagged `fits:universal` always appear.\n- Vehicle data lives in `brand.fitments` (lib/brand.ts) \u2014 add a make/model/year by editing that array.\n- `Bosch`, `Mobil 1`, `Castrol`, `NGK`, `Brembo`, `Bridgestone` brand strip \u2014 swap to your own authorised brands.\n- `AutoPartsStore` schema.org `@type` is set in `brand.schemaType` so JSON-LD on every page identifies the business correctly.\n\n## Switch the seed\n\n```bash\ncimplify-mock --seed pharmacy # Wellspring Pharmacy\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
2272
+ ` }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-background transition-colors">Shop</Link>\n <span>/</span>\n <span className="text-background/90">{category.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {category.name}\n </h1>\n {category.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {category.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s no longer in stock. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Browse the shop\n </Link>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { AutoHero } from "@/components/auto-hero";\nimport { FitmentFinder } from "@/components/fitment-finder";\nimport { TrustBar } from "@/components/trust-bar";\nimport { ServiceBrief } from "@/components/service-brief";\nimport { Newsletter } from "@/components/newsletter";\nimport { SectionHeading } from "@/components/section-heading";\nimport { StoreProductCard } from "@/components/store-product-card";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: brand.hero.title,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const productsRes = await client.catalogue.getProducts(\n { limit: 12 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n const allProducts: Product[] = productsRes.ok ? productsRes.value.items : [];\n\n return {\n mostOrdered: allProducts.slice(0, 8),\n };\n}\n\nexport default async function HomePage() {\n const { mostOrdered } = await getHomeData();\n\n return (\n <>\n <AutoHero />\n\n <Suspense fallback={<FitmentFinderSkeleton />}>\n <FitmentFinder />\n </Suspense>\n\n <TrustBar />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Most ordered"\n title="What our garages keep on the shelf."\n description="The parts Driveline ships daily \u2014 oil, filters, brake pads, batteries. Every fitment verified by our parts team."\n link={{ label: "Shop everything", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={8} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {mostOrdered.map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <ServiceBrief />\n\n <Newsletter />\n </>\n );\n}\n\nfunction GridSkeleton({ count }: { count: number }) {\n return (\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n );\n}\n\nfunction FitmentFinderSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 -mt-10 sm:-mt-14 relative z-10">\n <div className="bg-card border border-border rounded-2xl p-6 sm:p-8 shadow-[0_8px_30px_rgb(0_0_0/0.06)]">\n <div className="h-7 w-48 bg-muted rounded mb-3 animate-pulse" />\n <div className="h-5 w-80 bg-muted rounded mb-6 animate-pulse" />\n <div className="grid grid-cols-1 md:grid-cols-[1.2fr_1.2fr_1fr_auto] gap-3">\n <div className="h-11 bg-muted rounded-xl animate-pulse" />\n <div className="h-11 bg-muted rounded-xl animate-pulse" />\n <div className="h-11 bg-muted rounded-xl animate-pulse" />\n <div className="h-11 w-36 bg-primary/30 rounded-xl animate-pulse" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Auto-parts storefront template\n\nIf you are an AI agent working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string, plus the `fitments` make/model/year tree.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` block for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_*` env vars.\n\n## Aesthetic\n\n- **Inter + JetBrains Mono** \u2014 clean modern sans, mono-spaced labels for spec-feel.\n- **Carbon-black foreground + sport-red accent.**\n- **Sharp corners**: `0.5rem` \u2014 workshop-modern.\n- **`AutoPartsStore`** is the Schema.org `@type` (set in `brand.schemaType`).\n\n## Page surface\n\n```\napp/\n page.tsx AutoHero + FitmentFinder + TrustBar + Most ordered\n grid + ServiceBrief + Newsletter\n shop/page.tsx SDK <CataloguePage/>. Reads `?fits=<tag>` to\n filter by fitment tag.\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page (Product JSON-LD)\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx SDK\n account/* + login/signup\n contact/, track-order/, about, faq, terms, privacy, shipping, returns,\n accessibility/, sitemap-page/ Brand-driven content pages\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (AutoPartsStore JSON-LD) |\n| `app/page.tsx` | `brand.hero`, `brand.fitments`, `brand.serviceBrief`, `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/auto-hero.tsx` | `brand.hero`, hard-coded trust-chip labels |\n| `components/fitment-finder.tsx` | `brand.fitments` (makes/models/years tree) |\n| `components/service-brief.tsx` | `brand.serviceBrief` (3 cards) |\n| `components/header.tsx`, `components/footer.tsx` | `brand.shortName`, `brand.header.nav`, `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/trust-bar.tsx` | `brand.trustItems` |\n| `components/newsletter.tsx` | `brand.newsletter` |\n\n## Auto-specific notes\n\n- **Fitment is tag-based, not a custom_attribute.** Each product carries `tags: ["fits:toyota", "fits:toyota:corolla"]` etc. or `["fits:universal"]`. The shop page reads `?fits=<tag>` and filters by tag prefix match. Universal-fit products bypass the filter.\n- **Adding a vehicle**: edit `brand.fitments.makes` in `lib/brand.ts`. Add `{ slug, name, models: [...] }`. Slug becomes the URL fragment.\n- **Adding a part with new fitment**: tag the product `fits:<make>` and (optionally) `fits:<make>:<model>`. No schema change needed.\n- **Partner-workshop fitting**: the mock doesn\'t model this; the storefront copy describes it. To wire it, eject the cart drawer and add a "Add fitting (GH\u20B580)" toggle that adds a service line.\n\n## Known TODOs\n\n- Contact form + newsletter signup currently fake their submit. Wire to a real provider.\n- The fitment finder uses `useRouter().push` to a query-string URL; the catalogue page-side filter is left as an exercise for the merchant (read the `fits` param and pass it as a SDK `getProducts({ tags: [...] })` filter).\n\n## Mock seed\n\nWired to `--seed auto` (Driveline Auto Parts). Edit `dev:mock` in `package.json` to preview another seed.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, fitment-tag filtering, services with scheduling) \u2014 the SDK already gets price math, axis matching, tag filtering, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed auto --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed auto",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.110.2",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **auto** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Carbon-black + sport-red palette, Inter + JetBrains Mono typography, workshop-modern aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed auto` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Driveline Auto Parts.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Auto-specific behaviour\n\n- **Fitment finder** (`components/fitment-finder.tsx`): Make \u2192 Model \u2192 Year selector on the home page. Submitting pushes `?fits=fits:<make>:<model>` onto `/shop`; the shop page filters the catalogue by matching `tags` on `MockProduct`. Products tagged `fits:universal` always appear.\n- Vehicle data lives in `brand.fitments` (lib/brand.ts) \u2014 add a make/model/year by editing that array.\n- `Bosch`, `Mobil 1`, `Castrol`, `NGK`, `Brembo`, `Bridgestone` brand strip \u2014 swap to your own authorised brands.\n- `AutoPartsStore` schema.org `@type` is set in `brand.schemaType` so JSON-LD on every page identifies the business correctly.\n\n## Switch the seed\n\n```bash\ncimplify-mock --seed pharmacy # Wellspring Pharmacy\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
2273
2273
  * Canonical absolute URL. The platform overwrites this at build time with the
2274
2274
  * storefront's primary domain; the default only applies to local builds. It's
2275
2275
  * per-deploy config, not per-request, so it stays a static constant \u2014 keeping
@@ -3301,7 +3301,7 @@ function Field({
3301
3301
  font-weight: 600;
3302
3302
  }
3303
3303
  }
3304
- ` }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-background transition-colors">Shop</Link>\n <span>/</span>\n <span className="text-background/90">{category.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {category.name}\n </h1>\n {category.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {category.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s no longer in stock. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Browse the shop\n </Link>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Collection, type Product } from "@cimplify/sdk/server";\nimport { FeatureHero } from "@/components/feature-hero";\nimport { CategoryTiles } from "@/components/category-tiles";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { PromoBanner } from "@/components/promo-banner";\nimport { TrustBar } from "@/components/trust-bar";\nimport { BrandMarquee } from "@/components/brand-marquee";\nimport { TradeInCta } from "@/components/trade-in-cta";\nimport { Newsletter } from "@/components/newsletter";\nimport { SectionHeading } from "@/components/section-heading";\nimport { StoreProductCard } from "@/components/store-product-card";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: brand.hero.title,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\ninterface CollectionWithProducts {\n collection: Collection;\n products: Product[];\n}\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes, productsRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getProducts(\n { limit: 12 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n const allProducts = productsRes.ok ? productsRes.value.items : [];\n\n const collectionsWithProducts: CollectionWithProducts[] = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n featured: allProducts.slice(0, 4),\n newArrivals: allProducts.slice(4, 12),\n };\n}\n\nconst HERO_FALLBACK_IMAGE =\n "https://images.unsplash.com/photo-1592899677977-9c10ca588bbd?w=1200&h=1200&fit=crop&auto=format&q=85";\n\nexport default async function HomePage() {\n const { collections, categories, featured, newArrivals } = await getHomeData();\n const heroProduct = featured[0];\n\n return (\n <>\n <FeatureHero\n eyebrow="Featured \xB7 in stock"\n badge="Just landed"\n title={brand.hero.title}\n description={heroProduct?.description ?? brand.hero.subtitle}\n primaryCta={{\n label: heroProduct ? `Shop ${heroProduct.name}` : brand.hero.primaryCtaLabel,\n href: heroProduct ? `/products/${heroProduct.slug ?? heroProduct.id}` : "/shop",\n }}\n secondaryCta={\n brand.hero.secondaryCtaLabel && brand.hero.secondaryCtaHref\n ? { label: brand.hero.secondaryCtaLabel, href: brand.hero.secondaryCtaHref }\n : undefined\n }\n imageUrl={heroProduct?.image_url ?? heroProduct?.images?.[0] ?? HERO_FALLBACK_IMAGE}\n imageAlt={heroProduct?.name ?? "Featured product"}\n />\n\n <TrustBar />\n\n <Suspense fallback={<CategoryTilesSkeleton />}>\n <CategoryTiles categories={categories} />\n </Suspense>\n\n <PromoBanner />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Just dropped"\n title="New this week."\n description="Hand-picked launches and freshly restocked staples."\n link={{ label: "Browse all", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(0, 4).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <BrandMarquee />\n\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n\n <TradeInCta />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Best sellers"\n title="What everyone&apos;s buying."\n link={{ label: "See more", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(4, 8).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <Newsletter />\n </>\n );\n}\n\nfunction CategoryTilesSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="h-8 w-64 bg-muted rounded mb-8 animate-pulse" />\n <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="h-40 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction GridSkeleton({ count }: { count: number }) {\n return (\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-12">\n <h2 className="text-[26px] font-semibold m-0 mb-5 -tracking-[0.02em]">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Retail / electronics storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` block for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\n## Aesthetic\n\n- **Inter + JetBrains Mono** \u2014 clean modern sans, mono-spaced labels for spec-feel.\n- **Cool navy + electric blue**: dark foreground, electric primary.\n- **Sharp corners**: `0.5rem` \u2014 modern retail.\n- **Section heroes** in dark with mono-spaced eyebrow labels.\n\n## Page surface\n\n```\napp/\n page.tsx Multi-section home \u2014 split hero, trust bar, category tiles,\n promo banner, \"Just dropped\" grid, brand strip, collections,\n trade-in CTA, best sellers, newsletter\n shop/page.tsx SDK <CataloguePage/> with custom hero\n search/page.tsx Search (SDK <SearchPage/>)\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page (Product JSON-LD)\n\n cart/page.tsx SDK <CartPage/>\n checkout/page.tsx SDK <CheckoutPage/>\n orders/[id]/page.tsx Post-checkout confirmation\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx Branded contact form\n track-order/page.tsx Guest lookup \u2192 /orders/[id]\n\n about/page.tsx, faq/page.tsx Brand pages\n shipping/page.tsx, returns/page.tsx, accessibility/page.tsx Standalone policies\n terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx HTML sitemap\n sitemap.ts, robots.ts XML sitemap, robots.txt\n llms.txt/route.ts LLM-friendly site index\n opensearch.xml/route.ts Browser search description\n error.tsx, not-found.tsx Global boundaries\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (Store JSON-LD) |\n| `app/page.tsx` | `brand.hero`, `brand.trustItems`, `brand.brandStrip`, `brand.promo`, `brand.tradeIn`, `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` (warranty + returns + same-day delivery copy) |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` (Product JSON-LD: brand, offer, availability) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `app/opensearch.xml/route.ts` | `brand.shortName`, `brand.name` |\n| `components/header.tsx` | `brand.shortName`, `brand.microTag`, `brand.header.nav` |\n| `components/footer.tsx` | `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/promo-banner.tsx` | `brand.promo` (renders nothing if absent) |\n| `components/trade-in-cta.tsx` | `brand.tradeIn` (renders nothing if absent) |\n| `components/brand-marquee.tsx` | `brand.brandStrip` |\n| `components/trust-bar.tsx` | `brand.trustItems` |\n| `components/newsletter.tsx` | `brand.newsletter` |\n\n## Retail-specific notes\n\n- Product detail is a **full `/products/[slug]` page** with image gallery, customizer, related products rail, and Product JSON-LD. Consideration purchases (phones, laptops, headphones) need real estate. Don't replace with a modal.\n- Schema.org `@type` is `Store`.\n- Optional sections render conditionally: set `brand.promo`, `brand.tradeIn`, `brand.trustItems`, or `brand.brandStrip` to `undefined` to hide them.\n- `app/page.tsx` uses an Unsplash fallback hero (`HERO_FALLBACK_IMAGE`) \u2014 replace with the merchant's own product photography.\n\n## Known TODOs\n\n- Contact form + newsletter signup currently fake their submit. Wire `client.support.sendMessage(...)` for contact and a real list provider for newsletter.\n- Trust-bar / brand-marquee imagery / wordmarks are placeholders \u2014 swap for the merchant's real partners.\n\n## Mock seed\n\nWired to `--seed retail` (Currents Electronics). Edit `dev:mock` in `package.json` to preview another seed.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed retail --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed retail",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.109.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **retail** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Cool navy + electric blue palette, Inter + JetBrains Mono typography, modern electronics-store aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed retail` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Currents Electronics.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch the seed\n\nThis template is wired to the `retail` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery # warm food/pastry\ncimplify init my-store --template restaurant # coming soon\ncimplify init my-store --template services # coming soon\ncimplify init my-store --template grocery # coming soon\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
3304
+ ` }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-background transition-colors">Shop</Link>\n <span>/</span>\n <span className="text-background/90">{category.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {category.name}\n </h1>\n {category.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {category.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s no longer in stock. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Browse the shop\n </Link>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Collection, type Product } from "@cimplify/sdk/server";\nimport { FeatureHero } from "@/components/feature-hero";\nimport { CategoryTiles } from "@/components/category-tiles";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { PromoBanner } from "@/components/promo-banner";\nimport { TrustBar } from "@/components/trust-bar";\nimport { BrandMarquee } from "@/components/brand-marquee";\nimport { TradeInCta } from "@/components/trade-in-cta";\nimport { Newsletter } from "@/components/newsletter";\nimport { SectionHeading } from "@/components/section-heading";\nimport { StoreProductCard } from "@/components/store-product-card";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: brand.hero.title,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\ninterface CollectionWithProducts {\n collection: Collection;\n products: Product[];\n}\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes, productsRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getProducts(\n { limit: 12 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n const allProducts = productsRes.ok ? productsRes.value.items : [];\n\n const collectionsWithProducts: CollectionWithProducts[] = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n featured: allProducts.slice(0, 4),\n newArrivals: allProducts.slice(4, 12),\n };\n}\n\nconst HERO_FALLBACK_IMAGE =\n "https://images.unsplash.com/photo-1592899677977-9c10ca588bbd?w=1200&h=1200&fit=crop&auto=format&q=85";\n\nexport default async function HomePage() {\n const { collections, categories, featured, newArrivals } = await getHomeData();\n const heroProduct = featured[0];\n\n return (\n <>\n <FeatureHero\n eyebrow="Featured \xB7 in stock"\n badge="Just landed"\n title={brand.hero.title}\n description={heroProduct?.description ?? brand.hero.subtitle}\n primaryCta={{\n label: heroProduct ? `Shop ${heroProduct.name}` : brand.hero.primaryCtaLabel,\n href: heroProduct ? `/products/${heroProduct.slug ?? heroProduct.id}` : "/shop",\n }}\n secondaryCta={\n brand.hero.secondaryCtaLabel && brand.hero.secondaryCtaHref\n ? { label: brand.hero.secondaryCtaLabel, href: brand.hero.secondaryCtaHref }\n : undefined\n }\n imageUrl={heroProduct?.image_url ?? heroProduct?.images?.[0] ?? HERO_FALLBACK_IMAGE}\n imageAlt={heroProduct?.name ?? "Featured product"}\n />\n\n <TrustBar />\n\n <Suspense fallback={<CategoryTilesSkeleton />}>\n <CategoryTiles categories={categories} />\n </Suspense>\n\n <PromoBanner />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Just dropped"\n title="New this week."\n description="Hand-picked launches and freshly restocked staples."\n link={{ label: "Browse all", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(0, 4).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <BrandMarquee />\n\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n\n <TradeInCta />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Best sellers"\n title="What everyone&apos;s buying."\n link={{ label: "See more", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(4, 8).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <Newsletter />\n </>\n );\n}\n\nfunction CategoryTilesSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="h-8 w-64 bg-muted rounded mb-8 animate-pulse" />\n <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="h-40 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction GridSkeleton({ count }: { count: number }) {\n return (\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-12">\n <h2 className="text-[26px] font-semibold m-0 mb-5 -tracking-[0.02em]">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Retail / electronics storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` block for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\n## Aesthetic\n\n- **Inter + JetBrains Mono** \u2014 clean modern sans, mono-spaced labels for spec-feel.\n- **Cool navy + electric blue**: dark foreground, electric primary.\n- **Sharp corners**: `0.5rem` \u2014 modern retail.\n- **Section heroes** in dark with mono-spaced eyebrow labels.\n\n## Page surface\n\n```\napp/\n page.tsx Multi-section home \u2014 split hero, trust bar, category tiles,\n promo banner, \"Just dropped\" grid, brand strip, collections,\n trade-in CTA, best sellers, newsletter\n shop/page.tsx SDK <CataloguePage/> with custom hero\n search/page.tsx Search (SDK <SearchPage/>)\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page (Product JSON-LD)\n\n cart/page.tsx SDK <CartPage/>\n checkout/page.tsx SDK <CheckoutPage/>\n orders/[id]/page.tsx Post-checkout confirmation\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx Branded contact form\n track-order/page.tsx Guest lookup \u2192 /orders/[id]\n\n about/page.tsx, faq/page.tsx Brand pages\n shipping/page.tsx, returns/page.tsx, accessibility/page.tsx Standalone policies\n terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx HTML sitemap\n sitemap.ts, robots.ts XML sitemap, robots.txt\n llms.txt/route.ts LLM-friendly site index\n opensearch.xml/route.ts Browser search description\n error.tsx, not-found.tsx Global boundaries\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (Store JSON-LD) |\n| `app/page.tsx` | `brand.hero`, `brand.trustItems`, `brand.brandStrip`, `brand.promo`, `brand.tradeIn`, `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` (warranty + returns + same-day delivery copy) |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` (Product JSON-LD: brand, offer, availability) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `app/opensearch.xml/route.ts` | `brand.shortName`, `brand.name` |\n| `components/header.tsx` | `brand.shortName`, `brand.microTag`, `brand.header.nav` |\n| `components/footer.tsx` | `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/promo-banner.tsx` | `brand.promo` (renders nothing if absent) |\n| `components/trade-in-cta.tsx` | `brand.tradeIn` (renders nothing if absent) |\n| `components/brand-marquee.tsx` | `brand.brandStrip` |\n| `components/trust-bar.tsx` | `brand.trustItems` |\n| `components/newsletter.tsx` | `brand.newsletter` |\n\n## Retail-specific notes\n\n- Product detail is a **full `/products/[slug]` page** with image gallery, customizer, related products rail, and Product JSON-LD. Consideration purchases (phones, laptops, headphones) need real estate. Don't replace with a modal.\n- Schema.org `@type` is `Store`.\n- Optional sections render conditionally: set `brand.promo`, `brand.tradeIn`, `brand.trustItems`, or `brand.brandStrip` to `undefined` to hide them.\n- `app/page.tsx` uses an Unsplash fallback hero (`HERO_FALLBACK_IMAGE`) \u2014 replace with the merchant's own product photography.\n\n## Known TODOs\n\n- Contact form + newsletter signup currently fake their submit. Wire `client.support.sendMessage(...)` for contact and a real list provider for newsletter.\n- Trust-bar / brand-marquee imagery / wordmarks are placeholders \u2014 swap for the merchant's real partners.\n\n## Mock seed\n\nWired to `--seed retail` (Currents Electronics). Edit `dev:mock` in `package.json` to preview another seed.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed retail --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed retail",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.110.2",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **retail** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Cool navy + electric blue palette, Inter + JetBrains Mono typography, modern electronics-store aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed retail` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Currents Electronics.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch the seed\n\nThis template is wired to the `retail` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery # warm food/pastry\ncimplify init my-store --template restaurant # coming soon\ncimplify init my-store --template services # coming soon\ncimplify init my-store --template grocery # coming soon\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
3305
3305
  * Canonical absolute URL. The platform overwrites this at build time with the
3306
3306
  * storefront's primary domain; the default only applies to local builds. It's
3307
3307
  * per-deploy config, not per-request, so it stays a static constant \u2014 keeping
@@ -4261,7 +4261,7 @@ function Field({
4261
4261
  font-weight: 600;
4262
4262
  }
4263
4263
  }
4264
- ` }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-background transition-colors">Shop</Link>\n <span>/</span>\n <span className="text-background/90">{category.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {category.name}\n </h1>\n {category.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {category.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s no longer in stock. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Browse the shop\n </Link>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { PharmacyHero } from "@/components/pharmacy-hero";\nimport { UrgentCtas } from "@/components/urgent-ctas";\nimport { SymptomFinder } from "@/components/symptom-finder";\nimport { TrustBar } from "@/components/trust-bar";\nimport { HealthBrief } from "@/components/health-brief";\nimport { Newsletter } from "@/components/newsletter";\nimport { SectionHeading } from "@/components/section-heading";\nimport { StoreProductCard } from "@/components/store-product-card";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: brand.hero.title,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const productsRes = await client.catalogue.getProducts(\n { limit: 12 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n const allProducts: Product[] = productsRes.ok ? productsRes.value.items : [];\n\n return {\n mostDispensed: allProducts.slice(0, 8),\n };\n}\n\nexport default async function HomePage() {\n const { mostDispensed } = await getHomeData();\n\n return (\n <>\n <PharmacyHero />\n\n <UrgentCtas />\n\n <SymptomFinder />\n\n <TrustBar />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Most dispensed"\n title="What our pharmacists hand out daily."\n description="The OTC essentials Wellspring fills the most \u2014 across pain relief, cold & flu, baby care, and vitamins."\n link={{ label: "Shop everything", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={8} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {mostDispensed.map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <HealthBrief />\n\n <Newsletter />\n </>\n );\n}\n\nfunction GridSkeleton({ count }: { count: number }) {\n return (\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Pharmacy storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` block for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\n## Aesthetic\n\n- **Inter + JetBrains Mono** \u2014 clean clinical sans, mono-spaced labels for trust.\n- **Soft white + calm teal accent** \u2014 medical without being sterile.\n- **Sharp corners**: `0.5rem` \u2014 modern community pharmacy.\n- **`Pharmacy`** is the Schema.org `@type` (set in `brand.schemaType`).\n\n## Page surface\n\n```\napp/\n page.tsx Multi-section home \u2014 featured hero, trust bar, category tiles,\n pharmacist-consult promo, \"Just in stock\" grid, brand strip,\n collections, prescription-upload CTA, best sellers, newsletter\n shop/page.tsx SDK <CataloguePage/> with custom hero\n search/page.tsx Search (SDK <SearchPage/>)\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page with prescription-upload fields\n (auto-rendered from product.input_fields)\n\n cart/page.tsx SDK <CartPage/>\n checkout/page.tsx SDK <CheckoutPage/>\n orders/[id]/page.tsx Post-checkout confirmation\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx Pharmacist consult form\n track-order/page.tsx Guest lookup \u2192 /orders/[id]\n\n about/page.tsx, faq/page.tsx Brand pages\n shipping/page.tsx, returns/page.tsx, accessibility/page.tsx Standalone policies\n terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx HTML sitemap\n sitemap.ts, robots.ts XML sitemap, robots.txt\n llms.txt/route.ts LLM-friendly site index\n opensearch.xml/route.ts Browser search description\n error.tsx, not-found.tsx Global boundaries\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (Pharmacy JSON-LD) |\n| `app/page.tsx` | `brand.hero`, `brand.trustItems`, `brand.brandStrip`, `brand.promo`, `brand.tradeIn` (repurposed as prescription CTA), `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery + pickup + cold-chain copy) |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` (Product JSON-LD: brand, offer, availability) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `app/opensearch.xml/route.ts` | `brand.shortName`, `brand.name` |\n| `components/header.tsx` | `brand.shortName`, `brand.microTag`, `brand.header.nav` |\n| `components/footer.tsx` | `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/promo-banner.tsx` | `brand.promo` (renders nothing if absent) |\n| `components/trade-in-cta.tsx` | `brand.tradeIn` (repurposed as prescription CTA; renders nothing if absent) |\n| `components/brand-marquee.tsx` | `brand.brandStrip` |\n| `components/trust-bar.tsx` | `brand.trustItems` |\n| `components/newsletter.tsx` | `brand.newsletter` |\n\n## Pharmacy-specific notes\n\n- **Prescription / consent / DOB inputs**: prescription-required products (e.g. `amoxicillin-rx`, `insulin-rx`, `vaccination-consent`) ship with `input_fields` on the catalogue payload. The SDK `<ProductPage/>` reads `product.input_fields` and auto-renders file upload, consent signature, age checkbox, and DOB inputs. The payload is forwarded with the cart line \u2014 your backend (or the mock) decides whether to accept the order before allowing checkout.\n- **`brand.tradeIn` is the prescription-upload CTA.** The `trade-in-cta.tsx` component is unchanged from the retail template \u2014 only the brand copy changed. If you'd rather use a dedicated component, eject it and rename.\n- **Schema.org `@type`** is `Pharmacy` \u2014 semantically meaningful for Google Business and JSON-LD parsers. If your business is broader (clinic + pharmacy), switch to `LocalBusiness`.\n- **Controlled substances** (Schedule II) usually require in-person collection. The mock doesn't enforce this \u2014 your backend or a Cimplify rule should reject those cart lines from delivery-method=ship orders.\n\n## Known TODOs\n\n- Contact form + newsletter signup currently fake their submit. Wire `client.support.sendMessage(...)` for contact and a real list provider for newsletter.\n- Trust-bar / brand-marquee imagery / wordmarks are placeholders \u2014 swap for the real partners you're authorised to display.\n\n## Mock seed\n\nWired to `--seed pharmacy` (Wellspring Pharmacy). Edit `dev:mock` in `package.json` to preview another seed.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, prescription inputs, services with scheduling) \u2014 the SDK already gets price math, axis matching, input-field rendering, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed pharmacy --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed pharmacy",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.109.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": '# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **pharmacy** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Clinical white + calm teal palette, Inter + JetBrains Mono typography, trustworthy community-pharmacy aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed pharmacy` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Wellspring Pharmacy.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/>\n trade-in-cta.tsx # repurposed as a prescription-upload CTA\n collection-strip.tsx # horizontal product strip\n category-tiles.tsx # SDK category grid\nlib/\n brand.ts # every visible string (pharmacy copy)\n cart.ts # useCartCount() for the header pill\n```\n\n## Pharmacy-specific behaviour\n\n- Prescription-required products (insulin, amoxicillin, etc.) ship with `input_fields` on the catalogue payload \u2014 the SDK product page renders prescription upload + consent signature + DOB inputs automatically. Look at how `paracetamol`, `amoxicillin-rx`, and `baby-formula` differ in the mock seed.\n- `brand.tradeIn` is repurposed as the "Send us your script" CTA on the home page.\n- `Pharmacy` schema.org `@type` is set in `brand.schemaType` so JSON-LD on every page identifies the business correctly.\n\n## Switch the seed\n\nThis template is wired to the `pharmacy` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama\'s Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\ncimplify-mock --seed retail # Currents Electronics\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery\ncimplify init my-store --template restaurant\ncimplify init my-store --template services\ncimplify init my-store --template grocery\ncimplify init my-store --template fashion\ncimplify init my-store --template retail\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
4264
+ ` }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-background transition-colors">Shop</Link>\n <span>/</span>\n <span className="text-background/90">{category.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {category.name}\n </h1>\n {category.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {category.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s no longer in stock. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Browse the shop\n </Link>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { PharmacyHero } from "@/components/pharmacy-hero";\nimport { UrgentCtas } from "@/components/urgent-ctas";\nimport { SymptomFinder } from "@/components/symptom-finder";\nimport { TrustBar } from "@/components/trust-bar";\nimport { HealthBrief } from "@/components/health-brief";\nimport { Newsletter } from "@/components/newsletter";\nimport { SectionHeading } from "@/components/section-heading";\nimport { StoreProductCard } from "@/components/store-product-card";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: brand.hero.title,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const productsRes = await client.catalogue.getProducts(\n { limit: 12 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n const allProducts: Product[] = productsRes.ok ? productsRes.value.items : [];\n\n return {\n mostDispensed: allProducts.slice(0, 8),\n };\n}\n\nexport default async function HomePage() {\n const { mostDispensed } = await getHomeData();\n\n return (\n <>\n <PharmacyHero />\n\n <UrgentCtas />\n\n <SymptomFinder />\n\n <TrustBar />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Most dispensed"\n title="What our pharmacists hand out daily."\n description="The OTC essentials Wellspring fills the most \u2014 across pain relief, cold & flu, baby care, and vitamins."\n link={{ label: "Shop everything", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={8} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {mostDispensed.map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <HealthBrief />\n\n <Newsletter />\n </>\n );\n}\n\nfunction GridSkeleton({ count }: { count: number }) {\n return (\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Pharmacy storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` block for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\n## Aesthetic\n\n- **Inter + JetBrains Mono** \u2014 clean clinical sans, mono-spaced labels for trust.\n- **Soft white + calm teal accent** \u2014 medical without being sterile.\n- **Sharp corners**: `0.5rem` \u2014 modern community pharmacy.\n- **`Pharmacy`** is the Schema.org `@type` (set in `brand.schemaType`).\n\n## Page surface\n\n```\napp/\n page.tsx Multi-section home \u2014 featured hero, trust bar, category tiles,\n pharmacist-consult promo, \"Just in stock\" grid, brand strip,\n collections, prescription-upload CTA, best sellers, newsletter\n shop/page.tsx SDK <CataloguePage/> with custom hero\n search/page.tsx Search (SDK <SearchPage/>)\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page with prescription-upload fields\n (auto-rendered from product.input_fields)\n\n cart/page.tsx SDK <CartPage/>\n checkout/page.tsx SDK <CheckoutPage/>\n orders/[id]/page.tsx Post-checkout confirmation\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx Pharmacist consult form\n track-order/page.tsx Guest lookup \u2192 /orders/[id]\n\n about/page.tsx, faq/page.tsx Brand pages\n shipping/page.tsx, returns/page.tsx, accessibility/page.tsx Standalone policies\n terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx HTML sitemap\n sitemap.ts, robots.ts XML sitemap, robots.txt\n llms.txt/route.ts LLM-friendly site index\n opensearch.xml/route.ts Browser search description\n error.tsx, not-found.tsx Global boundaries\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (Pharmacy JSON-LD) |\n| `app/page.tsx` | `brand.hero`, `brand.trustItems`, `brand.brandStrip`, `brand.promo`, `brand.tradeIn` (repurposed as prescription CTA), `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery + pickup + cold-chain copy) |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` (Product JSON-LD: brand, offer, availability) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `app/opensearch.xml/route.ts` | `brand.shortName`, `brand.name` |\n| `components/header.tsx` | `brand.shortName`, `brand.microTag`, `brand.header.nav` |\n| `components/footer.tsx` | `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/promo-banner.tsx` | `brand.promo` (renders nothing if absent) |\n| `components/trade-in-cta.tsx` | `brand.tradeIn` (repurposed as prescription CTA; renders nothing if absent) |\n| `components/brand-marquee.tsx` | `brand.brandStrip` |\n| `components/trust-bar.tsx` | `brand.trustItems` |\n| `components/newsletter.tsx` | `brand.newsletter` |\n\n## Pharmacy-specific notes\n\n- **Prescription / consent / DOB inputs**: prescription-required products (e.g. `amoxicillin-rx`, `insulin-rx`, `vaccination-consent`) ship with `input_fields` on the catalogue payload. The SDK `<ProductPage/>` reads `product.input_fields` and auto-renders file upload, consent signature, age checkbox, and DOB inputs. The payload is forwarded with the cart line \u2014 your backend (or the mock) decides whether to accept the order before allowing checkout.\n- **`brand.tradeIn` is the prescription-upload CTA.** The `trade-in-cta.tsx` component is unchanged from the retail template \u2014 only the brand copy changed. If you'd rather use a dedicated component, eject it and rename.\n- **Schema.org `@type`** is `Pharmacy` \u2014 semantically meaningful for Google Business and JSON-LD parsers. If your business is broader (clinic + pharmacy), switch to `LocalBusiness`.\n- **Controlled substances** (Schedule II) usually require in-person collection. The mock doesn't enforce this \u2014 your backend or a Cimplify rule should reject those cart lines from delivery-method=ship orders.\n\n## Known TODOs\n\n- Contact form + newsletter signup currently fake their submit. Wire `client.support.sendMessage(...)` for contact and a real list provider for newsletter.\n- Trust-bar / brand-marquee imagery / wordmarks are placeholders \u2014 swap for the real partners you're authorised to display.\n\n## Mock seed\n\nWired to `--seed pharmacy` (Wellspring Pharmacy). Edit `dev:mock` in `package.json` to preview another seed.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, prescription inputs, services with scheduling) \u2014 the SDK already gets price math, axis matching, input-field rendering, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed pharmacy --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed pharmacy",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.110.2",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": '# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **pharmacy** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Clinical white + calm teal palette, Inter + JetBrains Mono typography, trustworthy community-pharmacy aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed pharmacy` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Wellspring Pharmacy.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/>\n trade-in-cta.tsx # repurposed as a prescription-upload CTA\n collection-strip.tsx # horizontal product strip\n category-tiles.tsx # SDK category grid\nlib/\n brand.ts # every visible string (pharmacy copy)\n cart.ts # useCartCount() for the header pill\n```\n\n## Pharmacy-specific behaviour\n\n- Prescription-required products (insulin, amoxicillin, etc.) ship with `input_fields` on the catalogue payload \u2014 the SDK product page renders prescription upload + consent signature + DOB inputs automatically. Look at how `paracetamol`, `amoxicillin-rx`, and `baby-formula` differ in the mock seed.\n- `brand.tradeIn` is repurposed as the "Send us your script" CTA on the home page.\n- `Pharmacy` schema.org `@type` is set in `brand.schemaType` so JSON-LD on every page identifies the business correctly.\n\n## Switch the seed\n\nThis template is wired to the `pharmacy` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama\'s Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\ncimplify-mock --seed retail # Currents Electronics\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery\ncimplify init my-store --template restaurant\ncimplify init my-store --template services\ncimplify init my-store --template grocery\ncimplify init my-store --template fashion\ncimplify init my-store --template retail\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
4265
4265
  * Canonical absolute URL. The platform overwrites this at build time with the
4266
4266
  * storefront's primary domain; the default only applies to local builds. It's
4267
4267
  * per-deploy config, not per-request, so it stays a static constant \u2014 keeping
@@ -5296,7 +5296,7 @@ function Field({
5296
5296
  font-weight: 700;
5297
5297
  }
5298
5298
  }
5299
- ` }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Grocery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Inter sans only** (no serif) \u2014 friendly, readable for dense product grids.\n- **Fresh green + cream** \u2014 vibrant green primary, warm cream background.\n- **Generous radius**: `0.875rem` \u2014 softer than retail.\n- **Bold, slightly heavy** headings.\n- Schema.org `@type` is `GroceryStore`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero (\"Free delivery over GH\u20B5150\"),\n collection strips, category grid, newsletter\n shop/page.tsx Aisle (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. weekly box, deals)\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery + slots), returns/page.tsx (quality guarantee),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, GroceryStore JSON-LD |\n| `app/page.tsx` | `brand.hero` (free-delivery badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (delivery slots, sourcing, subscription boxes) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery slots + free-over threshold) |\n| `app/returns/page.tsx` | `brand.returns` (quality guarantee) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Grocery-specific notes\n\n- **Subscription boxes** are modelled as products with `billing_plans` (renders the SubscriptionCard variant). The basket-box landing lives at `/categories/baskets`.\n- **Weight-based pricing**: SDK supports `quantity_pricing` tiers per product, but most grocery products are sold by unit/kg. Real weight-at-pack pricing happens on the backend pricing rule.\n- Mock seed: `--seed grocery` (FreshMart).\n\n## Known TODOs\n\n- Contact + newsletter fake submits.\n- Hero / category / product imagery is Unsplash placeholder \u2014 replace with merchant photography.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed grocery --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed grocery",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.109.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
5299
+ ` }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Grocery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Inter sans only** (no serif) \u2014 friendly, readable for dense product grids.\n- **Fresh green + cream** \u2014 vibrant green primary, warm cream background.\n- **Generous radius**: `0.875rem` \u2014 softer than retail.\n- **Bold, slightly heavy** headings.\n- Schema.org `@type` is `GroceryStore`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero (\"Free delivery over GH\u20B5150\"),\n collection strips, category grid, newsletter\n shop/page.tsx Aisle (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. weekly box, deals)\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery + slots), returns/page.tsx (quality guarantee),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, GroceryStore JSON-LD |\n| `app/page.tsx` | `brand.hero` (free-delivery badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (delivery slots, sourcing, subscription boxes) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery slots + free-over threshold) |\n| `app/returns/page.tsx` | `brand.returns` (quality guarantee) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Grocery-specific notes\n\n- **Subscription boxes** are modelled as products with `billing_plans` (renders the SubscriptionCard variant). The basket-box landing lives at `/categories/baskets`.\n- **Weight-based pricing**: SDK supports `quantity_pricing` tiers per product, but most grocery products are sold by unit/kg. Real weight-at-pack pricing happens on the backend pricing rule.\n- Mock seed: `--seed grocery` (FreshMart).\n\n## Known TODOs\n\n- Contact + newsletter fake submits.\n- Hero / category / product imagery is Unsplash placeholder \u2014 replace with merchant photography.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed grocery --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed grocery",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.110.2",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
5300
5300
  * Canonical absolute URL. The platform overwrites this at build time with the
5301
5301
  * storefront's primary domain; the default only applies to local builds. It's
5302
5302
  * per-deploy config, not per-request, so it stays a static constant \u2014 keeping
@@ -5912,7 +5912,7 @@ function Field({
5912
5912
  </div>
5913
5913
  );
5914
5914
  }
5915
- ` }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/products/[slug]/product-detail.tsx", "kind": "text", "content": '"use client";\n\nimport Image from "next/image";\nimport { ProductPage as SdkProductPage, useCart } from "@cimplify/sdk/react";\nimport type { Product, ProductWithDetails } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the product detail page.\n *\n * - Receives a server-fetched `ProductWithDetails` (no client refetch).\n * - Renders the SDK `<ProductPage>` which picks the right layout\n * (Default / Wholesale / Service / Bundle / Composite) automatically.\n * - On add-to-cart success, routes to `/cart`.\n * - Custom Next.js Image renderer for optimised, lazy-loaded gallery shots.\n * - Renders a "You may also like" rail of in-category products below.\n */\nexport function ProductDetail({\n product,\n related,\n}: {\n product: ProductWithDetails;\n related: Product[];\n}) {\n const { addItem } = useCart();\n\n return (\n <>\n <SdkProductPage\n product={product}\n showRelated={false}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={1200}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-8 sm:py-10"\n />\n\n {related.length > 0 && (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-16 border-t border-border mt-8">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n You may also like\n </p>\n <h2 className="text-[clamp(1.5rem,2.5vw,2rem)] font-bold m-0 -tracking-[0.025em]">\n More from this category.\n </h2>\n </div>\n </div>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {related.map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </section>\n )}\n </>\n );\n}\n' }, { "path": "app/products/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { notFound } from "next/navigation";\nimport Link from "next/link";\nimport {\n getServerClient,\n tags,\n type Product,\n type ProductWithDetails,\n} from "@cimplify/sdk/server";\nimport { ProductDetail } from "./product-detail";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\nimport { buildProductOffer } from "@/lib/product-offer";\n\n// Pre-enumerate every product slug at build time so Next can emit cacheable\n// responses (s-maxage instead of no-store) for the route. Without this, every\n// PDP is treated as fully runtime-dynamic and CF edge skips it. Placeholder\n// fallback keeps the build alive if the catalogue API isn\'t reachable at\n// build time; the page handler already calls notFound() on unknown slugs.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getProducts({ limit: 10_000 });\n if (!r.ok || r.value.items.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.items.map((p) => ({ slug: p.slug ?? p.id }));\n}\n\nexport const revalidate = 3600;\n\nfunction productLd(product: ProductWithDetails, SITE_URL: string) {\n const image = product.image_url ?? product.images?.[0];\n return {\n "@context": "https://schema.org",\n "@type": "Product",\n name: product.name,\n description: product.description ?? undefined,\n image: image ? [image] : undefined,\n sku: product.id,\n brand: { "@type": "Brand", name: brand.name },\n offers: buildProductOffer(product, {\n url: `${SITE_URL}/products/${product.slug ?? product.id}`,\n currency: brand.currency,\n }),\n };\n}\n\ninterface ProductData {\n product: ProductWithDetails;\n related: Product[];\n}\n\ntype ProductResult =\n | { ok: true; data: ProductData }\n | { ok: false; code: string };\n\nasync function getProduct(slug: string): Promise<ProductResult> {\n const client = getServerClient();\n const r = await client.catalogue.getProductBySlug(slug, {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.product(slug), tags.products()],\n },\n });\n if (!r.ok) return { ok: false, code: r.error.code };\n\n const product = r.value;\n // Tag with the resolved ID: every product change dispatches tags.product(id); bulk updates skip the slug.\n // See https://cimplify.dev/docs/sdk/revalidation#dynamic-routes--tag-by-id-never-by-slug\n const related = product.category_id\n ? await client.catalogue\n .getCategoryProducts(product.category_id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.product(product.id),\n tags.categoryProducts(product.category_id),\n ],\n },\n })\n .then((res) =>\n res.ok\n ? (\n ((res.value as { items?: Product[] }).items ??\n (res.value as Product[])) as Product[]\n ).filter((p) => p.id !== product.id).slice(0, 4)\n : [],\n )\n : [];\n\n return { ok: true, data: { product, related } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const siteUrl = await getSiteUrl();\n const result = await getProduct(slug);\n if (!result.ok) return {};\n const { product } = result.data;\n const image = product.image_url ?? product.images?.[0];\n return {\n title: `${product.name} \u2014 ${brand.name}`,\n description: product.description ?? undefined,\n openGraph: {\n title: product.name,\n description: product.description ?? undefined,\n images: image ? [{ url: image }] : undefined,\n type: "website",\n },\n };\n}\n\nexport default async function ProductPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<ProductSkeleton />}>\n <ProductContent params={params} />\n </Suspense>\n );\n}\n\nasync function ProductContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const siteUrl = await getSiteUrl();\n const result = await getProduct(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n // Transient SDK error \u2014 render the soft skeleton; ISR will retry on next request.\n return <ProductSkeleton />;\n }\n const data = result.data;\n\n return (\n <>\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(productLd(data.product, siteUrl)) }}\n />\n <nav\n aria-label="Breadcrumb"\n className="max-w-7xl mx-auto px-6 sm:px-8 pt-6 text-[12px] font-mono text-muted-foreground flex items-center gap-2"\n >\n <Link href="/" className="hover:text-foreground transition-colors">\n Home\n </Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-foreground transition-colors">\n Shop\n </Link>\n {data.product.category?.slug && (\n <>\n <span>/</span>\n <Link\n href={`/categories/${data.product.category.slug}`}\n className="hover:text-foreground transition-colors"\n >\n {data.product.category.name}\n </Link>\n </>\n )}\n <span>/</span>\n <span className="text-foreground/90 truncate">{data.product.name}</span>\n </nav>\n <ProductDetail product={data.product} related={data.related} />\n </>\n );\n}\n\nfunction ProductSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 items-start">\n <div className="aspect-square bg-muted rounded-3xl animate-pulse" />\n <div className="space-y-4">\n <div className="h-3 w-24 bg-muted rounded animate-pulse" />\n <div className="h-10 w-3/4 bg-muted rounded animate-pulse" />\n <div className="h-7 w-32 bg-muted rounded animate-pulse" />\n <div className="h-20 w-full bg-muted rounded animate-pulse mt-6" />\n <div className="h-12 w-full bg-muted rounded animate-pulse mt-4" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n return { kind: "cancelled" };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n });\n}\n' }, { "path": "app/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { collection: colRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <span className="text-background/90">Collections</span>\n <span>/</span>\n <span className="text-background/90">{collection.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {collection.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\nexport const revalidate = 3600;\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <h1 className="text-3xl mt-0 mb-3 font-bold -tracking-[0.025em]">\n Order confirmed.\n </h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code> \u2014 you&apos;ll\n get an SMS with tracking within 30 minutes.\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* Studio FRX \u2014 Nike/Aritzia-style streetwear palette. High-contrast\n neutrals with one electric accent. Tweak primary to retheme everything. */\n@theme {\n --color-background: oklch(0.99 0 0);\n --color-foreground: oklch(0.12 0 0);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.12 0 0);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.12 0 0);\n --color-primary: oklch(0.7 0.24 30);\n --color-primary-foreground: oklch(0.99 0 0);\n --color-secondary: oklch(0.96 0 0);\n --color-secondary-foreground: oklch(0.18 0 0);\n --color-muted: oklch(0.96 0 0);\n --color-muted-foreground: oklch(0.45 0 0);\n --color-accent: oklch(0.92 0 0);\n --color-accent-foreground: oklch(0.18 0 0);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.9 0 0);\n --color-input: oklch(0.93 0 0);\n --color-ring: oklch(0.7 0.24 30);\n --radius: 0.125rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-display);\n letter-spacing: -0.04em;\n font-weight: 800;\n line-height: 0.95;\n }\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-background transition-colors">Shop</Link>\n <span>/</span>\n <span className="text-background/90">{category.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {category.name}\n </h1>\n {category.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {category.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s no longer in stock. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Browse the shop\n </Link>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/lookbook/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Image from "next/image";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Lookbook \u2014 ${brand.name}`,\n description: "Editorial photography from each Studio FRX drop.",\n};\n\ninterface LookbookEntry {\n drop: string;\n title: string;\n hero: string;\n tiles: string[];\n byline: string;\n date: string;\n}\n\n// Each entry is a drop; tiles are the editorial shots. Replace the URLs\n// with your own asset bucket / CDN before going live.\nconst ENTRIES: LookbookEntry[] = [\n {\n drop: "Drop 04",\n title: "Built for now.",\n date: "Spring 2026",\n byline: "Photographed by Selasi Adjei in Jamestown, Accra.",\n hero: "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1483985988355-763728e1935b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1485518882345-15568b007407?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 03",\n title: "Heavyweight, hand-screened.",\n date: "Winter 2025",\n byline: "Shot at the Tema dye works.",\n hero: "https://images.unsplash.com/photo-1467043153537-a4fba2cd39ef?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1576566588028-4147f3842f27?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1542838686-37da4a9fd1b3?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 02",\n title: "From the studio floor.",\n date: "Autumn 2025",\n byline: "Studio days, Osu.",\n hero: "https://images.unsplash.com/photo-1525507119028-ed4c629a60a3?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1512436991641-6745cdb1723f?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1490114538077-0a7f8cb49891?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n];\n\nexport default function LookbookPage() {\n return (\n <article>\n <section className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-primary mb-3">\n Lookbook\n </p>\n <h1 className="font-display text-[clamp(3rem,9vw,7rem)] uppercase mb-4 -tracking-[0.04em] leading-[0.92]">\n The drops, in full.\n </h1>\n <p className="text-base text-muted-foreground max-w-2xl leading-relaxed">\n Each drop is photographed in the room it\'s built in. Editorials below;\n full collection in the shop.\n </p>\n </div>\n </section>\n\n {ENTRIES.map((entry, i) => (\n <section key={entry.drop} className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-16">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {entry.drop} \xB7 {entry.date}\n </p>\n <h2 className="font-display text-[clamp(2rem,5vw,3.5rem)] uppercase m-0 -tracking-[0.04em] leading-[0.95]">\n {entry.title}\n </h2>\n <p className="text-sm text-muted-foreground mt-2">{entry.byline}</p>\n </div>\n <Link\n href="/shop"\n className="hidden sm:inline-flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-foreground hover:text-primary transition-colors whitespace-nowrap"\n >\n Shop {entry.drop} \u2192\n </Link>\n </div>\n\n <div className="relative w-full aspect-[16/9] rounded-2xl overflow-hidden bg-muted mb-3">\n <Image\n src={entry.hero}\n alt={`${entry.drop} hero \u2014 ${entry.title}`}\n fill\n sizes="(min-width: 1280px) 1280px, 100vw"\n className="object-cover"\n priority={i === 0}\n />\n </div>\n\n <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">\n {entry.tiles.map((src, ti) => (\n <div\n key={src}\n className="relative aspect-[3/4] rounded-2xl overflow-hidden bg-muted"\n >\n <Image\n src={src}\n alt={`${entry.drop} editorial ${ti + 1}`}\n fill\n sizes="(min-width: 768px) 33vw, 100vw"\n className="object-cover"\n />\n </div>\n ))}\n </div>\n </div>\n </section>\n ))}\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Collection, type Product } from "@cimplify/sdk/server";\nimport { FeatureHero } from "@/components/feature-hero";\nimport { CategoryTiles } from "@/components/category-tiles";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { PromoBanner } from "@/components/promo-banner";\nimport { TrustBar } from "@/components/trust-bar";\nimport { BrandMarquee } from "@/components/brand-marquee";\nimport { TradeInCta } from "@/components/trade-in-cta";\nimport { Newsletter } from "@/components/newsletter";\nimport { SectionHeading } from "@/components/section-heading";\nimport { StoreProductCard } from "@/components/store-product-card";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: brand.hero.title,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\ninterface CollectionWithProducts {\n collection: Collection;\n products: Product[];\n}\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes, productsRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getProducts(\n { limit: 12 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n const allProducts = productsRes.ok ? productsRes.value.items : [];\n\n const collectionsWithProducts: CollectionWithProducts[] = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n featured: allProducts.slice(0, 4),\n newArrivals: allProducts.slice(4, 12),\n };\n}\n\n// Verified-reachable Unsplash fashion editorial \u2014 replace with your own asset.\nconst HERO_FALLBACK_IMAGE =\n "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=2000&h=1400&fit=crop&auto=format&q=85";\n\nexport default async function HomePage() {\n const { collections, categories, featured, newArrivals } = await getHomeData();\n const heroProduct = featured[0];\n\n return (\n <>\n <FeatureHero\n eyebrow="Featured \xB7 in stock"\n badge="Just landed"\n title={brand.hero.title}\n description={heroProduct?.description ?? brand.hero.subtitle}\n primaryCta={{\n label: heroProduct ? `Shop ${heroProduct.name}` : brand.hero.primaryCtaLabel,\n href: heroProduct ? `/products/${heroProduct.slug ?? heroProduct.id}` : "/shop",\n }}\n secondaryCta={\n brand.hero.secondaryCtaLabel && brand.hero.secondaryCtaHref\n ? { label: brand.hero.secondaryCtaLabel, href: brand.hero.secondaryCtaHref }\n : undefined\n }\n imageUrl={heroProduct?.image_url ?? heroProduct?.images?.[0] ?? HERO_FALLBACK_IMAGE}\n imageAlt={heroProduct?.name ?? "Featured product"}\n />\n\n <TrustBar />\n\n <Suspense fallback={<CategoryTilesSkeleton />}>\n <CategoryTiles categories={categories} />\n </Suspense>\n\n <PromoBanner />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Just dropped"\n title="New this week."\n description="Hand-picked launches and freshly restocked staples."\n link={{ label: "Browse all", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(0, 4).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <BrandMarquee />\n\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n\n <TradeInCta />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Best sellers"\n title="What everyone&apos;s buying."\n link={{ label: "See more", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(4, 8).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <Newsletter />\n </>\n );\n}\n\nfunction CategoryTilesSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="h-8 w-64 bg-muted rounded mb-8 animate-pulse" />\n <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="h-40 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction GridSkeleton({ count }: { count: number }) {\n return (\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-12">\n <h2 className="text-[26px] font-semibold m-0 mb-5 -tracking-[0.02em]">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "playwright.config.ts", "kind": "text", "content": 'import { defineConfig, devices } from "@playwright/test";\n\n/**\n * Visual regression config \u2014 boots `bun dev` (mock + next), runs against\n * a stable Chromium, snapshots key pages.\n *\n * Run:\n * bun run check:visual # compare against baseline\n * bun run check:visual --update-snapshots # accept new baseline\n *\n * Baselines live in `e2e/__snapshots__/`. Commit them.\n */\nexport default defineConfig({\n testDir: "./e2e",\n fullyParallel: false,\n forbidOnly: Boolean(process.env.CI),\n retries: process.env.CI ? 1 : 0,\n workers: 1,\n reporter: process.env.CI ? "github" : "list",\n\n use: {\n baseURL: "http://localhost:3000",\n trace: "retain-on-failure",\n // Viewport snapshots are noisy across OS / browser builds. Lock here.\n viewport: { width: 1280, height: 800 },\n },\n\n projects: [\n {\n name: "chromium",\n use: { ...devices["Desktop Chrome"], viewport: { width: 1280, height: 800 } },\n },\n ],\n\n expect: {\n // Anti-flake: allow tiny rendering differences (font hinting across runners).\n toHaveScreenshot: { maxDiffPixelRatio: 0.01 },\n },\n\n webServer: {\n command: "bun dev",\n url: "http://localhost:3000",\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n stdout: "ignore",\n stderr: "pipe",\n },\n});\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Fashion / streetwear storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\n## Aesthetic\n\nStreetwear / lookbook-led brands (Nike, Aritzia, Highsnobiety, drop culture):\n\n- **Anton display + Inter** \u2014 uppercase condensed display, clean body sans.\n- **High-contrast palette** \u2014 near-black foreground on near-white background, electric primary (default coral, `oklch(0.7 0.24 30)`).\n- **Sharp corners**: `0.125rem` \u2014 minimal rounding.\n- **Editorial full-bleed hero** with image gradient overlay.\n- **Block-letter header** \u2014 no logo mark, just the shortName in display type.\n- Schema.org `@type` is `Store`.\n\n## Page surface\n\n```\napp/\n page.tsx Multi-section home \u2014 full-bleed editorial hero,\n trust bar, category tiles, promo banner,\n "Just dropped" grid, brand strip, collections,\n studio-collective CTA, best sellers, newsletter\n shop/page.tsx SDK <CataloguePage/> with custom hero\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (drops)\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page (Product JSON-LD)\n\n size-guide/page.tsx \u2B50 Fashion-specific: chest/length/shoulder + waist/hip/inseam\n tables, "how to measure" callout\n lookbook/page.tsx \u2B50 Fashion-specific: editorial multi-drop lookbook \u2014\n hero + 3-up tile galleries per drop, "Shop Drop X" CTAs\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (DHL Express worldwide), returns/page.tsx (30 days),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, Store JSON-LD |\n| `app/page.tsx` | `brand.hero`, `brand.trustItems`, `brand.brandStrip`, `brand.promo`, `brand.tradeIn` (= studio-collective copy), `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (sizing, drops, returns, customs) |\n| `app/shipping/page.tsx` | `brand.shipping` (worldwide DHL, customs) |\n| `app/returns/page.tsx` | `brand.returns` (30 days, free US/UK/EU) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` (Product JSON-LD) |\n| `app/size-guide/page.tsx` | currently has its size charts inlined \u2014 hoist to `brand.sizeGuide` if you want agents to edit them |\n| `app/lookbook/page.tsx` | currently has the lookbook entries inlined (image URLs + drop names) \u2014 hoist to `brand.lookbook` for agent editing |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/promo-banner.tsx`, `trade-in-cta.tsx`, `brand-marquee.tsx`, `trust-bar.tsx`, `newsletter.tsx` | corresponding optional sections in `brand` |\n\n## Fashion-specific notes\n\n- **Hero is full-bleed editorial.** Replace `HERO_FALLBACK_IMAGE` in `app/page.tsx` with the merchant\'s drop campaign shot for maximum impact.\n- **Drop badges and "limited run" copy** drive urgency. Keep `brand.hero.badge` and `brand.promo.badge` short and time-bounded.\n- **`brand.tradeIn`** is repurposed as the "Studio collective" / membership programme \u2014 three-step access flow.\n- **Product detail uses the page-driven model** (`/products/[slug]`) \u2014 fashion is a consideration purchase (sizing, fit, fabric details), not impulse.\n- **`brandStrip.headline` is `"Stocked at"`** with editorial publication wordmarks (Vogue, Highsnobiety, etc.) \u2014 typography-led, no logo licensing required.\n\n## Known TODOs\n\n- `app/size-guide/page.tsx` has its size charts inlined as constants. Move them to `brand.sizeGuide` (typed) so agents can edit per-merchant size charts in one file.\n- `app/lookbook/page.tsx` has its drop entries inlined. Move to `brand.lookbook` for agent editing \u2014 each drop becomes `{ drop, title, date, byline, hero, tiles[] }`.\n- Contact form + newsletter fake submits.\n\n## Mock seed\n\nWired to `--seed fashion` (Studio FRX apparel catalogue \u2014 14 products with size variants XS\u20132XL, Drop 04 + Best Sellers collections, full-bleed product imagery served from `static-tmp.cimplify.io`).\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, you\'ll likely want to lean on the SDK\'s prebuilt components rather than reinvent. Particularly for **product customization** (variants, add-ons, bundles, composites, services with scheduling), the SDK already gets the price math, variant axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add variant-selector # copies into ./components/\ncimplify add cart-drawer\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK\'s mock and the backend lens.\n\nIf you\'re considering a from-scratch rebuild of the customizer, read these SDK source files first to understand the contract you must reproduce:\n- `src/react/product-customizer.tsx` \u2014 top-level state machine + final `onAddToCart` payload assembly\n- `src/react/variant-selector.tsx` \u2014 multi-axis matching via `display_attributes`; default-variant init runs in a `useEffect` and is easy to break\n- `src/react/{add-on,bundle,composite}-selector.tsx` \u2014 group constraints, exclusivity rules, per-component variants\n- `src/mock/domain/cart/{index,pricing}.ts` \u2014 the contract: `computeUnitPrice`, `computeBundlePrice`, `computeCompositePrice` (4 modes), `computeLineKey`\n\nFull ejection + customization rules are in the SDK-level [`AGENTS.md`](../../AGENTS.md) under "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "e2e/visual.spec.ts", "kind": "text", "content": 'import { test, expect } from "@playwright/test";\n\n/**\n * Visual regression on the storefront\'s key surfaces. Catches CSS / layout\n * drift that the contract harness can\'t see.\n *\n * Anti-flake notes:\n * - We disable animations (CSS transitions can leave half-frame snapshots).\n * - We mask images (Unsplash / CDN URLs occasionally render slight pixel\n * differences). The container layout is what we\'re regressing on, not\n * image bytes.\n * - Fonts are served by `next/font` and locked at build time, but if you\n * see flake on a fresh CI runner, `await page.evaluate(() => document.fonts.ready)`\n * before the snapshot.\n */\ntest.describe("visual regression", () => {\n test.beforeEach(async ({ page }) => {\n await page.addStyleTag({\n content: `*, *::before, *::after { animation: none !important; transition: none !important; }`,\n });\n });\n\n test("homepage", async ({ page }) => {\n await page.goto("/");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("homepage.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("shop / catalogue", async ({ page }) => {\n await page.goto("/shop");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("shop.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("cart drawer after adding a product", async ({ page }) => {\n await page.goto("/products/studio-tee-natural");\n await page.evaluate(() => document.fonts.ready);\n // Click whatever the storefront\'s primary Add-to-Cart button is.\n await page.getByRole("button", { name: /add to cart/i }).first().click();\n // Drawer auto-opens via CartDrawerProvider\'s openOnAdd watcher.\n await expect(page.getByRole("dialog", { name: /cart/i })).toBeVisible();\n await expect(page).toHaveScreenshot("cart-drawer.png", {\n mask: [page.locator("img")],\n });\n });\n});\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed fashion --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed fashion",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check:visual": "playwright test",\n "check:visual:update": "playwright test --update-snapshots",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.109.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@playwright/test": "^1.50.0",\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **retail** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Cool navy + electric blue palette, Inter + JetBrains Mono typography, modern electronics-store aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed retail` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Currents Electronics.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch the seed\n\nThis template is wired to the `retail` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery # warm food/pastry\ncimplify init my-store --template restaurant # coming soon\ncimplify init my-store --template services # coming soon\ncimplify init my-store --template grocery # coming soon\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
5915
+ ` }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/products/[slug]/product-detail.tsx", "kind": "text", "content": '"use client";\n\nimport Image from "next/image";\nimport { ProductPage as SdkProductPage, useCart } from "@cimplify/sdk/react";\nimport type { Product, ProductWithDetails } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the product detail page.\n *\n * - Receives a server-fetched `ProductWithDetails` (no client refetch).\n * - Renders the SDK `<ProductPage>` which picks the right layout\n * (Default / Wholesale / Service / Bundle / Composite) automatically.\n * - On add-to-cart success, routes to `/cart`.\n * - Custom Next.js Image renderer for optimised, lazy-loaded gallery shots.\n * - Renders a "You may also like" rail of in-category products below.\n */\nexport function ProductDetail({\n product,\n related,\n}: {\n product: ProductWithDetails;\n related: Product[];\n}) {\n const { addItem } = useCart();\n\n return (\n <>\n <SdkProductPage\n product={product}\n showRelated={false}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={1200}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-8 sm:py-10"\n />\n\n {related.length > 0 && (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-16 border-t border-border mt-8">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n You may also like\n </p>\n <h2 className="text-[clamp(1.5rem,2.5vw,2rem)] font-bold m-0 -tracking-[0.025em]">\n More from this category.\n </h2>\n </div>\n </div>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {related.map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </section>\n )}\n </>\n );\n}\n' }, { "path": "app/products/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { notFound } from "next/navigation";\nimport Link from "next/link";\nimport {\n getServerClient,\n tags,\n type Product,\n type ProductWithDetails,\n} from "@cimplify/sdk/server";\nimport { ProductDetail } from "./product-detail";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\nimport { buildProductOffer } from "@/lib/product-offer";\n\n// Pre-enumerate every product slug at build time so Next can emit cacheable\n// responses (s-maxage instead of no-store) for the route. Without this, every\n// PDP is treated as fully runtime-dynamic and CF edge skips it. Placeholder\n// fallback keeps the build alive if the catalogue API isn\'t reachable at\n// build time; the page handler already calls notFound() on unknown slugs.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getProducts({ limit: 10_000 });\n if (!r.ok || r.value.items.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.items.map((p) => ({ slug: p.slug ?? p.id }));\n}\n\nexport const revalidate = 3600;\n\nfunction productLd(product: ProductWithDetails, SITE_URL: string) {\n const image = product.image_url ?? product.images?.[0];\n return {\n "@context": "https://schema.org",\n "@type": "Product",\n name: product.name,\n description: product.description ?? undefined,\n image: image ? [image] : undefined,\n sku: product.id,\n brand: { "@type": "Brand", name: brand.name },\n offers: buildProductOffer(product, {\n url: `${SITE_URL}/products/${product.slug ?? product.id}`,\n currency: brand.currency,\n }),\n };\n}\n\ninterface ProductData {\n product: ProductWithDetails;\n related: Product[];\n}\n\ntype ProductResult =\n | { ok: true; data: ProductData }\n | { ok: false; code: string };\n\nasync function getProduct(slug: string): Promise<ProductResult> {\n const client = getServerClient();\n const r = await client.catalogue.getProductBySlug(slug, {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.product(slug), tags.products()],\n },\n });\n if (!r.ok) return { ok: false, code: r.error.code };\n\n const product = r.value;\n // Tag with the resolved ID: every product change dispatches tags.product(id); bulk updates skip the slug.\n // See https://cimplify.dev/docs/sdk/revalidation#dynamic-routes--tag-by-id-never-by-slug\n const related = product.category_id\n ? await client.catalogue\n .getCategoryProducts(product.category_id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.product(product.id),\n tags.categoryProducts(product.category_id),\n ],\n },\n })\n .then((res) =>\n res.ok\n ? (\n ((res.value as { items?: Product[] }).items ??\n (res.value as Product[])) as Product[]\n ).filter((p) => p.id !== product.id).slice(0, 4)\n : [],\n )\n : [];\n\n return { ok: true, data: { product, related } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const siteUrl = await getSiteUrl();\n const result = await getProduct(slug);\n if (!result.ok) return {};\n const { product } = result.data;\n const image = product.image_url ?? product.images?.[0];\n return {\n title: `${product.name} \u2014 ${brand.name}`,\n description: product.description ?? undefined,\n openGraph: {\n title: product.name,\n description: product.description ?? undefined,\n images: image ? [{ url: image }] : undefined,\n type: "website",\n },\n };\n}\n\nexport default async function ProductPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<ProductSkeleton />}>\n <ProductContent params={params} />\n </Suspense>\n );\n}\n\nasync function ProductContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const siteUrl = await getSiteUrl();\n const result = await getProduct(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n // Transient SDK error \u2014 render the soft skeleton; ISR will retry on next request.\n return <ProductSkeleton />;\n }\n const data = result.data;\n\n return (\n <>\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(productLd(data.product, siteUrl)) }}\n />\n <nav\n aria-label="Breadcrumb"\n className="max-w-7xl mx-auto px-6 sm:px-8 pt-6 text-[12px] font-mono text-muted-foreground flex items-center gap-2"\n >\n <Link href="/" className="hover:text-foreground transition-colors">\n Home\n </Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-foreground transition-colors">\n Shop\n </Link>\n {data.product.category?.slug && (\n <>\n <span>/</span>\n <Link\n href={`/categories/${data.product.category.slug}`}\n className="hover:text-foreground transition-colors"\n >\n {data.product.category.name}\n </Link>\n </>\n )}\n <span>/</span>\n <span className="text-foreground/90 truncate">{data.product.name}</span>\n </nav>\n <ProductDetail product={data.product} related={data.related} />\n </>\n );\n}\n\nfunction ProductSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 items-start">\n <div className="aspect-square bg-muted rounded-3xl animate-pulse" />\n <div className="space-y-4">\n <div className="h-3 w-24 bg-muted rounded animate-pulse" />\n <div className="h-10 w-3/4 bg-muted rounded animate-pulse" />\n <div className="h-7 w-32 bg-muted rounded animate-pulse" />\n <div className="h-20 w-full bg-muted rounded animate-pulse mt-6" />\n <div className="h-12 w-full bg-muted rounded animate-pulse mt-4" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n return { kind: "cancelled" };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n });\n}\n' }, { "path": "app/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { collection: colRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <span className="text-background/90">Collections</span>\n <span>/</span>\n <span className="text-background/90">{collection.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {collection.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\nexport const revalidate = 3600;\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <h1 className="text-3xl mt-0 mb-3 font-bold -tracking-[0.025em]">\n Order confirmed.\n </h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code> \u2014 you&apos;ll\n get an SMS with tracking within 30 minutes.\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* Studio FRX \u2014 Nike/Aritzia-style streetwear palette. High-contrast\n neutrals with one electric accent. Tweak primary to retheme everything. */\n@theme {\n --color-background: oklch(0.99 0 0);\n --color-foreground: oklch(0.12 0 0);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.12 0 0);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.12 0 0);\n --color-primary: oklch(0.7 0.24 30);\n --color-primary-foreground: oklch(0.99 0 0);\n --color-secondary: oklch(0.96 0 0);\n --color-secondary-foreground: oklch(0.18 0 0);\n --color-muted: oklch(0.96 0 0);\n --color-muted-foreground: oklch(0.45 0 0);\n --color-accent: oklch(0.92 0 0);\n --color-accent-foreground: oklch(0.18 0 0);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.9 0 0);\n --color-input: oklch(0.93 0 0);\n --color-ring: oklch(0.7 0.24 30);\n --radius: 0.125rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-display);\n letter-spacing: -0.04em;\n font-weight: 800;\n line-height: 0.95;\n }\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { category: catRes.value, products } };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-background transition-colors">Shop</Link>\n <span>/</span>\n <span className="text-background/90">{category.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {category.name}\n </h1>\n {category.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {category.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s no longer in stock. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Browse the shop\n </Link>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/lookbook/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Image from "next/image";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Lookbook \u2014 ${brand.name}`,\n description: "Editorial photography from each Studio FRX drop.",\n};\n\ninterface LookbookEntry {\n drop: string;\n title: string;\n hero: string;\n tiles: string[];\n byline: string;\n date: string;\n}\n\n// Each entry is a drop; tiles are the editorial shots. Replace the URLs\n// with your own asset bucket / CDN before going live.\nconst ENTRIES: LookbookEntry[] = [\n {\n drop: "Drop 04",\n title: "Built for now.",\n date: "Spring 2026",\n byline: "Photographed by Selasi Adjei in Jamestown, Accra.",\n hero: "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1483985988355-763728e1935b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1485518882345-15568b007407?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 03",\n title: "Heavyweight, hand-screened.",\n date: "Winter 2025",\n byline: "Shot at the Tema dye works.",\n hero: "https://images.unsplash.com/photo-1467043153537-a4fba2cd39ef?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1576566588028-4147f3842f27?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1542838686-37da4a9fd1b3?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 02",\n title: "From the studio floor.",\n date: "Autumn 2025",\n byline: "Studio days, Osu.",\n hero: "https://images.unsplash.com/photo-1525507119028-ed4c629a60a3?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1512436991641-6745cdb1723f?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1490114538077-0a7f8cb49891?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n];\n\nexport default function LookbookPage() {\n return (\n <article>\n <section className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-primary mb-3">\n Lookbook\n </p>\n <h1 className="font-display text-[clamp(3rem,9vw,7rem)] uppercase mb-4 -tracking-[0.04em] leading-[0.92]">\n The drops, in full.\n </h1>\n <p className="text-base text-muted-foreground max-w-2xl leading-relaxed">\n Each drop is photographed in the room it\'s built in. Editorials below;\n full collection in the shop.\n </p>\n </div>\n </section>\n\n {ENTRIES.map((entry, i) => (\n <section key={entry.drop} className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-16">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {entry.drop} \xB7 {entry.date}\n </p>\n <h2 className="font-display text-[clamp(2rem,5vw,3.5rem)] uppercase m-0 -tracking-[0.04em] leading-[0.95]">\n {entry.title}\n </h2>\n <p className="text-sm text-muted-foreground mt-2">{entry.byline}</p>\n </div>\n <Link\n href="/shop"\n className="hidden sm:inline-flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-foreground hover:text-primary transition-colors whitespace-nowrap"\n >\n Shop {entry.drop} \u2192\n </Link>\n </div>\n\n <div className="relative w-full aspect-[16/9] rounded-2xl overflow-hidden bg-muted mb-3">\n <Image\n src={entry.hero}\n alt={`${entry.drop} hero \u2014 ${entry.title}`}\n fill\n sizes="(min-width: 1280px) 1280px, 100vw"\n className="object-cover"\n priority={i === 0}\n />\n </div>\n\n <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">\n {entry.tiles.map((src, ti) => (\n <div\n key={src}\n className="relative aspect-[3/4] rounded-2xl overflow-hidden bg-muted"\n >\n <Image\n src={src}\n alt={`${entry.drop} editorial ${ti + 1}`}\n fill\n sizes="(min-width: 768px) 33vw, 100vw"\n className="object-cover"\n />\n </div>\n ))}\n </div>\n </div>\n </section>\n ))}\n </article>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags, type Collection, type Product } from "@cimplify/sdk/server";\nimport { FeatureHero } from "@/components/feature-hero";\nimport { CategoryTiles } from "@/components/category-tiles";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { PromoBanner } from "@/components/promo-banner";\nimport { TrustBar } from "@/components/trust-bar";\nimport { BrandMarquee } from "@/components/brand-marquee";\nimport { TradeInCta } from "@/components/trade-in-cta";\nimport { Newsletter } from "@/components/newsletter";\nimport { SectionHeading } from "@/components/section-heading";\nimport { StoreProductCard } from "@/components/store-product-card";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: brand.hero.title,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\ninterface CollectionWithProducts {\n collection: Collection;\n products: Product[];\n}\n\nasync function getHomeData() {\n const client = getServerClient();\n const [colRes, catRes, productsRes] = await Promise.all([\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getProducts(\n { limit: 12 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n const allProducts = productsRes.ok ? productsRes.value.items : [];\n\n const collectionsWithProducts: CollectionWithProducts[] = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(\n col.id,\n undefined,\n {\n cacheOptions: {\n revalidate: 3600,\n tags: [tags.products(), tags.collectionProducts(col.id)],\n },\n },\n );\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n featured: allProducts.slice(0, 4),\n newArrivals: allProducts.slice(4, 12),\n };\n}\n\n// Verified-reachable Unsplash fashion editorial \u2014 replace with your own asset.\nconst HERO_FALLBACK_IMAGE =\n "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=2000&h=1400&fit=crop&auto=format&q=85";\n\nexport default async function HomePage() {\n const { collections, categories, featured, newArrivals } = await getHomeData();\n const heroProduct = featured[0];\n\n return (\n <>\n <FeatureHero\n eyebrow="Featured \xB7 in stock"\n badge="Just landed"\n title={brand.hero.title}\n description={heroProduct?.description ?? brand.hero.subtitle}\n primaryCta={{\n label: heroProduct ? `Shop ${heroProduct.name}` : brand.hero.primaryCtaLabel,\n href: heroProduct ? `/products/${heroProduct.slug ?? heroProduct.id}` : "/shop",\n }}\n secondaryCta={\n brand.hero.secondaryCtaLabel && brand.hero.secondaryCtaHref\n ? { label: brand.hero.secondaryCtaLabel, href: brand.hero.secondaryCtaHref }\n : undefined\n }\n imageUrl={heroProduct?.image_url ?? heroProduct?.images?.[0] ?? HERO_FALLBACK_IMAGE}\n imageAlt={heroProduct?.name ?? "Featured product"}\n />\n\n <TrustBar />\n\n <Suspense fallback={<CategoryTilesSkeleton />}>\n <CategoryTiles categories={categories} />\n </Suspense>\n\n <PromoBanner />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Just dropped"\n title="New this week."\n description="Hand-picked launches and freshly restocked staples."\n link={{ label: "Browse all", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(0, 4).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <BrandMarquee />\n\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n\n <TradeInCta />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Best sellers"\n title="What everyone&apos;s buying."\n link={{ label: "See more", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(4, 8).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <Newsletter />\n </>\n );\n}\n\nfunction CategoryTilesSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="h-8 w-64 bg-muted rounded mb-8 animate-pulse" />\n <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="h-40 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction GridSkeleton({ count }: { count: number }) {\n return (\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-12">\n <h2 className="text-[26px] font-semibold m-0 mb-5 -tracking-[0.02em]">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport const revalidate = 3600;\n\nasync function getShopData() {\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 50 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (ISR-cached in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\n// Cache Components (\'use cache\' + cacheTag/cacheLife) require Node-specific\n// setTimeout atomicity and serialize a postponed state that routinely exceeds\n// CF Workers\' 128MB zlib limit. We\'re on Cloudflare Workers via opennext, so\n// we stay on Next 16\'s "Previous Model" \u2014 `fetch.next.{revalidate,tags}` via\n// the SDK\'s `cacheOptions`, plus `export const revalidate` per page. See\n// https://nextjs.org/docs/app/guides/caching-without-cache-components.\nconst nextConfig: NextConfig = {\n // A stray lockfile in a parent folder makes Next infer the wrong root.\n outputFileTracingRoot: process.cwd(),\n async redirects() {\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": "playwright.config.ts", "kind": "text", "content": 'import { defineConfig, devices } from "@playwright/test";\n\n/**\n * Visual regression config \u2014 boots `bun dev` (mock + next), runs against\n * a stable Chromium, snapshots key pages.\n *\n * Run:\n * bun run check:visual # compare against baseline\n * bun run check:visual --update-snapshots # accept new baseline\n *\n * Baselines live in `e2e/__snapshots__/`. Commit them.\n */\nexport default defineConfig({\n testDir: "./e2e",\n fullyParallel: false,\n forbidOnly: Boolean(process.env.CI),\n retries: process.env.CI ? 1 : 0,\n workers: 1,\n reporter: process.env.CI ? "github" : "list",\n\n use: {\n baseURL: "http://localhost:3000",\n trace: "retain-on-failure",\n // Viewport snapshots are noisy across OS / browser builds. Lock here.\n viewport: { width: 1280, height: 800 },\n },\n\n projects: [\n {\n name: "chromium",\n use: { ...devices["Desktop Chrome"], viewport: { width: 1280, height: 800 } },\n },\n ],\n\n expect: {\n // Anti-flake: allow tiny rendering differences (font hinting across runners).\n toHaveScreenshot: { maxDiffPixelRatio: 0.01 },\n },\n\n webServer: {\n command: "bun dev",\n url: "http://localhost:3000",\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n stdout: "ignore",\n stderr: "pipe",\n },\n});\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Fashion / streetwear storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\n## Aesthetic\n\nStreetwear / lookbook-led brands (Nike, Aritzia, Highsnobiety, drop culture):\n\n- **Anton display + Inter** \u2014 uppercase condensed display, clean body sans.\n- **High-contrast palette** \u2014 near-black foreground on near-white background, electric primary (default coral, `oklch(0.7 0.24 30)`).\n- **Sharp corners**: `0.125rem` \u2014 minimal rounding.\n- **Editorial full-bleed hero** with image gradient overlay.\n- **Block-letter header** \u2014 no logo mark, just the shortName in display type.\n- Schema.org `@type` is `Store`.\n\n## Page surface\n\n```\napp/\n page.tsx Multi-section home \u2014 full-bleed editorial hero,\n trust bar, category tiles, promo banner,\n "Just dropped" grid, brand strip, collections,\n studio-collective CTA, best sellers, newsletter\n shop/page.tsx SDK <CataloguePage/> with custom hero\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (drops)\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page (Product JSON-LD)\n\n size-guide/page.tsx \u2B50 Fashion-specific: chest/length/shoulder + waist/hip/inseam\n tables, "how to measure" callout\n lookbook/page.tsx \u2B50 Fashion-specific: editorial multi-drop lookbook \u2014\n hero + 3-up tile galleries per drop, "Shop Drop X" CTAs\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <AccountDashboardPage /> (native, Server Component)\n account/orders/page.tsx <AccountOrdersPage /> (native)\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (DHL Express worldwide), returns/page.tsx (30 days),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, Store JSON-LD |\n| `app/page.tsx` | `brand.hero`, `brand.trustItems`, `brand.brandStrip`, `brand.promo`, `brand.tradeIn` (= studio-collective copy), `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (sizing, drops, returns, customs) |\n| `app/shipping/page.tsx` | `brand.shipping` (worldwide DHL, customs) |\n| `app/returns/page.tsx` | `brand.returns` (30 days, free US/UK/EU) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (SDK account pages render the UI) |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` (Product JSON-LD) |\n| `app/size-guide/page.tsx` | currently has its size charts inlined \u2014 hoist to `brand.sizeGuide` if you want agents to edit them |\n| `app/lookbook/page.tsx` | currently has the lookbook entries inlined (image URLs + drop names) \u2014 hoist to `brand.lookbook` for agent editing |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/promo-banner.tsx`, `trade-in-cta.tsx`, `brand-marquee.tsx`, `trust-bar.tsx`, `newsletter.tsx` | corresponding optional sections in `brand` |\n\n## Fashion-specific notes\n\n- **Hero is full-bleed editorial.** Replace `HERO_FALLBACK_IMAGE` in `app/page.tsx` with the merchant\'s drop campaign shot for maximum impact.\n- **Drop badges and "limited run" copy** drive urgency. Keep `brand.hero.badge` and `brand.promo.badge` short and time-bounded.\n- **`brand.tradeIn`** is repurposed as the "Studio collective" / membership programme \u2014 three-step access flow.\n- **Product detail uses the page-driven model** (`/products/[slug]`) \u2014 fashion is a consideration purchase (sizing, fit, fabric details), not impulse.\n- **`brandStrip.headline` is `"Stocked at"`** with editorial publication wordmarks (Vogue, Highsnobiety, etc.) \u2014 typography-led, no logo licensing required.\n\n## Known TODOs\n\n- `app/size-guide/page.tsx` has its size charts inlined as constants. Move them to `brand.sizeGuide` (typed) so agents can edit per-merchant size charts in one file.\n- `app/lookbook/page.tsx` has its drop entries inlined. Move to `brand.lookbook` for agent editing \u2014 each drop becomes `{ drop, title, date, byline, hero, tiles[] }`.\n- Contact form + newsletter fake submits.\n\n## Mock seed\n\nWired to `--seed fashion` (Studio FRX apparel catalogue \u2014 14 products with size variants XS\u20132XL, Drop 04 + Best Sellers collections, full-bleed product imagery served from `static-tmp.cimplify.io`).\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, you\'ll likely want to lean on the SDK\'s prebuilt components rather than reinvent. Particularly for **product customization** (variants, add-ons, bundles, composites, services with scheduling), the SDK already gets the price math, variant axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add variant-selector # copies into ./components/\ncimplify add cart-drawer\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK\'s mock and the backend lens.\n\nIf you\'re considering a from-scratch rebuild of the customizer, read these SDK source files first to understand the contract you must reproduce:\n- `src/react/product-customizer.tsx` \u2014 top-level state machine + final `onAddToCart` payload assembly\n- `src/react/variant-selector.tsx` \u2014 multi-axis matching via `display_attributes`; default-variant init runs in a `useEffect` and is easy to break\n- `src/react/{add-on,bundle,composite}-selector.tsx` \u2014 group constraints, exclusivity rules, per-component variants\n- `src/mock/domain/cart/{index,pricing}.ts` \u2014 the contract: `computeUnitPrice`, `computeBundlePrice`, `computeCompositePrice` (4 modes), `computeLineKey`\n\nFull ejection + customization rules are in the SDK-level [`AGENTS.md`](../../AGENTS.md) under "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "e2e/visual.spec.ts", "kind": "text", "content": 'import { test, expect } from "@playwright/test";\n\n/**\n * Visual regression on the storefront\'s key surfaces. Catches CSS / layout\n * drift that the contract harness can\'t see.\n *\n * Anti-flake notes:\n * - We disable animations (CSS transitions can leave half-frame snapshots).\n * - We mask images (Unsplash / CDN URLs occasionally render slight pixel\n * differences). The container layout is what we\'re regressing on, not\n * image bytes.\n * - Fonts are served by `next/font` and locked at build time, but if you\n * see flake on a fresh CI runner, `await page.evaluate(() => document.fonts.ready)`\n * before the snapshot.\n */\ntest.describe("visual regression", () => {\n test.beforeEach(async ({ page }) => {\n await page.addStyleTag({\n content: `*, *::before, *::after { animation: none !important; transition: none !important; }`,\n });\n });\n\n test("homepage", async ({ page }) => {\n await page.goto("/");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("homepage.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("shop / catalogue", async ({ page }) => {\n await page.goto("/shop");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("shop.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("cart drawer after adding a product", async ({ page }) => {\n await page.goto("/products/studio-tee-natural");\n await page.evaluate(() => document.fonts.ready);\n // Click whatever the storefront\'s primary Add-to-Cart button is.\n await page.getByRole("button", { name: /add to cart/i }).first().click();\n // Drawer auto-opens via CartDrawerProvider\'s openOnAdd watcher.\n await expect(page.getByRole("dialog", { name: /cart/i })).toBeVisible();\n await expect(page).toHaveScreenshot("cart-drawer.png", {\n mask: [page.locator("img")],\n });\n });\n});\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed fashion --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed fashion",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check:visual": "playwright test",\n "check:visual:update": "playwright test --update-snapshots",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.110.2",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@playwright/test": "^1.50.0",\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **retail** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Cool navy + electric blue palette, Inter + JetBrains Mono typography, modern electronics-store aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed retail` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Currents Electronics.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch the seed\n\nThis template is wired to the `retail` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery # warm food/pastry\ncimplify init my-store --template restaurant # coming soon\ncimplify init my-store --template services # coming soon\ncimplify init my-store --template grocery # coming soon\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": `/**
5916
5916
  * Canonical absolute URL. The platform overwrites this at build time with the
5917
5917
  * storefront's primary domain; the default only applies to local builds. It's
5918
5918
  * per-deploy config, not per-request, so it stays a static constant \u2014 keeping