@cimplify/sdk 0.40.1 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dispatcher.mjs +3 -2
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/templates/storefront-bakery/.env.example +5 -0
- package/templates/storefront-bakery/AGENTS.md +57 -0
- package/templates/storefront-bakery/app/about/page.tsx +21 -44
- package/templates/storefront-bakery/app/categories/[slug]/page.tsx +2 -1
- package/templates/storefront-bakery/app/collections/[slug]/page.tsx +2 -1
- package/templates/storefront-bakery/app/error.tsx +60 -0
- package/templates/storefront-bakery/app/faq/page.tsx +13 -89
- package/templates/storefront-bakery/app/layout.tsx +37 -2
- package/templates/storefront-bakery/app/llms.txt/route.ts +94 -0
- package/templates/storefront-bakery/app/not-found.tsx +39 -0
- package/templates/storefront-bakery/app/page.tsx +6 -6
- package/templates/storefront-bakery/app/privacy/page.tsx +24 -68
- package/templates/storefront-bakery/app/robots.ts +18 -0
- package/templates/storefront-bakery/app/shop/page.tsx +3 -2
- package/templates/storefront-bakery/app/sitemap.ts +62 -0
- package/templates/storefront-bakery/app/terms/page.tsx +24 -82
- package/templates/storefront-bakery/components/footer.tsx +88 -130
- package/templates/storefront-bakery/components/header.tsx +8 -16
- package/templates/storefront-bakery/lib/brand.ts +453 -0
- package/templates/storefront-fashion/.env.example +16 -0
- package/templates/storefront-fashion/AGENTS.md +52 -0
- package/templates/storefront-fashion/README.md +77 -0
- package/templates/storefront-fashion/app/about/page.tsx +41 -0
- package/templates/storefront-fashion/app/cart/page.tsx +9 -0
- package/templates/storefront-fashion/app/categories/[slug]/listing-client.tsx +19 -0
- package/templates/storefront-fashion/app/categories/[slug]/page.tsx +130 -0
- package/templates/storefront-fashion/app/checkout/page.tsx +17 -0
- package/templates/storefront-fashion/app/collections/[slug]/listing-client.tsx +20 -0
- package/templates/storefront-fashion/app/collections/[slug]/page.tsx +130 -0
- package/templates/storefront-fashion/app/error.tsx +61 -0
- package/templates/storefront-fashion/app/faq/page.tsx +46 -0
- package/templates/storefront-fashion/app/globals.css +46 -0
- package/templates/storefront-fashion/app/layout.tsx +76 -0
- package/templates/storefront-fashion/app/llms.txt/route.ts +94 -0
- package/templates/storefront-fashion/app/not-found.tsx +39 -0
- package/templates/storefront-fashion/app/orders/[id]/page.tsx +24 -0
- package/templates/storefront-fashion/app/page.tsx +183 -0
- package/templates/storefront-fashion/app/privacy/page.tsx +44 -0
- package/templates/storefront-fashion/app/products/[slug]/page.tsx +165 -0
- package/templates/storefront-fashion/app/products/[slug]/product-detail.tsx +71 -0
- package/templates/storefront-fashion/app/robots.ts +18 -0
- package/templates/storefront-fashion/app/shop/page.tsx +63 -0
- package/templates/storefront-fashion/app/shop/shop-client.tsx +32 -0
- package/templates/storefront-fashion/app/sitemap.ts +59 -0
- package/templates/storefront-fashion/app/terms/page.tsx +44 -0
- package/templates/storefront-fashion/components/brand-marquee.tsx +27 -0
- package/templates/storefront-fashion/components/cart-pill.tsx +32 -0
- package/templates/storefront-fashion/components/category-grid.tsx +28 -0
- package/templates/storefront-fashion/components/category-tiles.tsx +104 -0
- package/templates/storefront-fashion/components/collection-strip.tsx +45 -0
- package/templates/storefront-fashion/components/feature-hero.tsx +82 -0
- package/templates/storefront-fashion/components/footer.tsx +153 -0
- package/templates/storefront-fashion/components/header.tsx +43 -0
- package/templates/storefront-fashion/components/hero.tsx +28 -0
- package/templates/storefront-fashion/components/nav-link.tsx +20 -0
- package/templates/storefront-fashion/components/newsletter.tsx +50 -0
- package/templates/storefront-fashion/components/promo-banner.tsx +41 -0
- package/templates/storefront-fashion/components/providers.tsx +31 -0
- package/templates/storefront-fashion/components/section-heading.tsx +37 -0
- package/templates/storefront-fashion/components/store-product-card.tsx +83 -0
- package/templates/storefront-fashion/components/trade-in-cta.tsx +54 -0
- package/templates/storefront-fashion/components/trust-bar.tsx +66 -0
- package/templates/storefront-fashion/lib/brand.ts +421 -0
- package/templates/storefront-fashion/lib/cart.ts +27 -0
- package/templates/storefront-fashion/next.config.ts +40 -0
- package/templates/storefront-fashion/package.json +28 -0
- package/templates/storefront-fashion/postcss.config.mjs +7 -0
- package/templates/storefront-fashion/tsconfig.json +23 -0
- package/templates/storefront-grocery/.env.example +16 -0
- package/templates/storefront-grocery/AGENTS.md +39 -0
- package/templates/storefront-grocery/README.md +73 -0
- package/templates/storefront-grocery/app/about/page.tsx +38 -0
- package/templates/storefront-grocery/app/cart/page.tsx +9 -0
- package/templates/storefront-grocery/app/categories/[slug]/listing-client.tsx +19 -0
- package/templates/storefront-grocery/app/categories/[slug]/page.tsx +118 -0
- package/templates/storefront-grocery/app/checkout/page.tsx +17 -0
- package/templates/storefront-grocery/app/collections/[slug]/listing-client.tsx +20 -0
- package/templates/storefront-grocery/app/collections/[slug]/page.tsx +118 -0
- package/templates/storefront-grocery/app/error.tsx +60 -0
- package/templates/storefront-grocery/app/faq/page.tsx +46 -0
- package/templates/storefront-grocery/app/globals.css +45 -0
- package/templates/storefront-grocery/app/layout.tsx +75 -0
- package/templates/storefront-grocery/app/llms.txt/route.ts +94 -0
- package/templates/storefront-grocery/app/not-found.tsx +39 -0
- package/templates/storefront-grocery/app/orders/[id]/page.tsx +21 -0
- package/templates/storefront-grocery/app/page.tsx +97 -0
- package/templates/storefront-grocery/app/privacy/page.tsx +44 -0
- package/templates/storefront-grocery/app/robots.ts +18 -0
- package/templates/storefront-grocery/app/shop/page.tsx +31 -0
- package/templates/storefront-grocery/app/shop/shop-client.tsx +27 -0
- package/templates/storefront-grocery/app/sitemap.ts +62 -0
- package/templates/storefront-grocery/app/terms/page.tsx +44 -0
- package/templates/storefront-grocery/components/cart-pill.tsx +32 -0
- package/templates/storefront-grocery/components/category-grid.tsx +28 -0
- package/templates/storefront-grocery/components/collection-strip.tsx +45 -0
- package/templates/storefront-grocery/components/footer.tsx +148 -0
- package/templates/storefront-grocery/components/header.tsx +43 -0
- package/templates/storefront-grocery/components/hero.tsx +25 -0
- package/templates/storefront-grocery/components/nav-link.tsx +20 -0
- package/templates/storefront-grocery/components/providers.tsx +31 -0
- package/templates/storefront-grocery/components/store-product-card.tsx +83 -0
- package/templates/storefront-grocery/lib/brand.ts +284 -0
- package/templates/storefront-grocery/lib/cart.ts +27 -0
- package/templates/storefront-grocery/next.config.ts +40 -0
- package/templates/storefront-grocery/package.json +28 -0
- package/templates/storefront-grocery/postcss.config.mjs +7 -0
- package/templates/storefront-grocery/tsconfig.json +23 -0
- package/templates/storefront-restaurant/.env.example +16 -0
- package/templates/storefront-restaurant/AGENTS.md +39 -0
- package/templates/storefront-restaurant/README.md +73 -0
- package/templates/storefront-restaurant/app/about/page.tsx +38 -0
- package/templates/storefront-restaurant/app/cart/page.tsx +9 -0
- package/templates/storefront-restaurant/app/categories/[slug]/listing-client.tsx +19 -0
- package/templates/storefront-restaurant/app/categories/[slug]/page.tsx +118 -0
- package/templates/storefront-restaurant/app/checkout/page.tsx +17 -0
- package/templates/storefront-restaurant/app/collections/[slug]/listing-client.tsx +20 -0
- package/templates/storefront-restaurant/app/collections/[slug]/page.tsx +118 -0
- package/templates/storefront-restaurant/app/error.tsx +60 -0
- package/templates/storefront-restaurant/app/faq/page.tsx +46 -0
- package/templates/storefront-restaurant/app/globals.css +44 -0
- package/templates/storefront-restaurant/app/layout.tsx +80 -0
- package/templates/storefront-restaurant/app/llms.txt/route.ts +94 -0
- package/templates/storefront-restaurant/app/not-found.tsx +39 -0
- package/templates/storefront-restaurant/app/orders/[id]/page.tsx +21 -0
- package/templates/storefront-restaurant/app/page.tsx +97 -0
- package/templates/storefront-restaurant/app/privacy/page.tsx +44 -0
- package/templates/storefront-restaurant/app/robots.ts +18 -0
- package/templates/storefront-restaurant/app/shop/page.tsx +31 -0
- package/templates/storefront-restaurant/app/shop/shop-client.tsx +27 -0
- package/templates/storefront-restaurant/app/sitemap.ts +62 -0
- package/templates/storefront-restaurant/app/terms/page.tsx +44 -0
- package/templates/storefront-restaurant/components/cart-pill.tsx +32 -0
- package/templates/storefront-restaurant/components/category-grid.tsx +28 -0
- package/templates/storefront-restaurant/components/collection-strip.tsx +45 -0
- package/templates/storefront-restaurant/components/footer.tsx +148 -0
- package/templates/storefront-restaurant/components/header.tsx +43 -0
- package/templates/storefront-restaurant/components/hero.tsx +25 -0
- package/templates/storefront-restaurant/components/nav-link.tsx +20 -0
- package/templates/storefront-restaurant/components/product-modal.tsx +105 -0
- package/templates/storefront-restaurant/components/providers.tsx +31 -0
- package/templates/storefront-restaurant/components/store-product-card.tsx +83 -0
- package/templates/storefront-restaurant/lib/brand.ts +287 -0
- package/templates/storefront-restaurant/lib/cart.ts +27 -0
- package/templates/storefront-restaurant/next.config.ts +40 -0
- package/templates/storefront-restaurant/package.json +28 -0
- package/templates/storefront-restaurant/postcss.config.mjs +7 -0
- package/templates/storefront-restaurant/tsconfig.json +23 -0
- package/templates/storefront-retail/.env.example +5 -0
- package/templates/storefront-retail/AGENTS.md +64 -0
- package/templates/storefront-retail/app/about/page.tsx +24 -54
- package/templates/storefront-retail/app/categories/[slug]/page.tsx +2 -1
- package/templates/storefront-retail/app/collections/[slug]/page.tsx +2 -1
- package/templates/storefront-retail/app/error.tsx +61 -0
- package/templates/storefront-retail/app/faq/page.tsx +11 -92
- package/templates/storefront-retail/app/layout.tsx +37 -8
- package/templates/storefront-retail/app/llms.txt/route.ts +94 -0
- package/templates/storefront-retail/app/not-found.tsx +39 -0
- package/templates/storefront-retail/app/page.tsx +16 -73
- package/templates/storefront-retail/app/privacy/page.tsx +24 -81
- package/templates/storefront-retail/app/products/[slug]/page.tsx +165 -0
- package/templates/storefront-retail/app/products/[slug]/product-detail.tsx +71 -0
- package/templates/storefront-retail/app/robots.ts +18 -0
- package/templates/storefront-retail/app/shop/page.tsx +3 -3
- package/templates/storefront-retail/app/sitemap.ts +59 -0
- package/templates/storefront-retail/app/terms/page.tsx +24 -97
- package/templates/storefront-retail/components/brand-marquee.tsx +6 -15
- package/templates/storefront-retail/components/feature-hero.tsx +2 -1
- package/templates/storefront-retail/components/footer.tsx +85 -120
- package/templates/storefront-retail/components/header.tsx +10 -12
- package/templates/storefront-retail/components/newsletter.tsx +7 -9
- package/templates/storefront-retail/components/promo-banner.tsx +12 -15
- package/templates/storefront-retail/components/store-product-card.tsx +7 -7
- package/templates/storefront-retail/components/trade-in-cta.tsx +11 -17
- package/templates/storefront-retail/components/trust-bar.tsx +45 -5
- package/templates/storefront-retail/lib/brand.ts +567 -0
- package/templates/storefront-services/.env.example +16 -0
- package/templates/storefront-services/AGENTS.md +41 -0
- package/templates/storefront-services/README.md +73 -0
- package/templates/storefront-services/app/about/page.tsx +38 -0
- package/templates/storefront-services/app/cart/page.tsx +9 -0
- package/templates/storefront-services/app/categories/[slug]/listing-client.tsx +19 -0
- package/templates/storefront-services/app/categories/[slug]/page.tsx +118 -0
- package/templates/storefront-services/app/checkout/page.tsx +17 -0
- package/templates/storefront-services/app/collections/[slug]/listing-client.tsx +20 -0
- package/templates/storefront-services/app/collections/[slug]/page.tsx +118 -0
- package/templates/storefront-services/app/error.tsx +60 -0
- package/templates/storefront-services/app/faq/page.tsx +46 -0
- package/templates/storefront-services/app/globals.css +45 -0
- package/templates/storefront-services/app/layout.tsx +80 -0
- package/templates/storefront-services/app/llms.txt/route.ts +94 -0
- package/templates/storefront-services/app/not-found.tsx +39 -0
- package/templates/storefront-services/app/orders/[id]/page.tsx +21 -0
- package/templates/storefront-services/app/page.tsx +97 -0
- package/templates/storefront-services/app/privacy/page.tsx +44 -0
- package/templates/storefront-services/app/robots.ts +18 -0
- package/templates/storefront-services/app/shop/page.tsx +31 -0
- package/templates/storefront-services/app/shop/shop-client.tsx +27 -0
- package/templates/storefront-services/app/sitemap.ts +62 -0
- package/templates/storefront-services/app/terms/page.tsx +44 -0
- package/templates/storefront-services/components/cart-pill.tsx +32 -0
- package/templates/storefront-services/components/category-grid.tsx +28 -0
- package/templates/storefront-services/components/collection-strip.tsx +45 -0
- package/templates/storefront-services/components/footer.tsx +148 -0
- package/templates/storefront-services/components/header.tsx +43 -0
- package/templates/storefront-services/components/hero.tsx +25 -0
- package/templates/storefront-services/components/nav-link.tsx +20 -0
- package/templates/storefront-services/components/product-modal.tsx +105 -0
- package/templates/storefront-services/components/providers.tsx +31 -0
- package/templates/storefront-services/components/store-product-card.tsx +83 -0
- package/templates/storefront-services/lib/brand.ts +303 -0
- package/templates/storefront-services/lib/cart.ts +27 -0
- package/templates/storefront-services/next.config.ts +40 -0
- package/templates/storefront-services/package.json +28 -0
- package/templates/storefront-services/postcss.config.mjs +7 -0
- package/templates/storefront-services/tsconfig.json +23 -0
- /package/templates/{storefront-retail → storefront-grocery}/components/product-modal.tsx +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { cacheTag, cacheLife } from "next/cache";
|
|
2
|
+
import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
|
|
3
|
+
import { brand } from "@/lib/brand";
|
|
4
|
+
|
|
5
|
+
const SITE_URL =
|
|
6
|
+
process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
|
|
7
|
+
|
|
8
|
+
async function buildLlmsTxt(): Promise<string> {
|
|
9
|
+
"use cache";
|
|
10
|
+
cacheTag(tags.products(), tags.categories(), tags.collections());
|
|
11
|
+
cacheLife("hours");
|
|
12
|
+
|
|
13
|
+
const client = getServerClient();
|
|
14
|
+
const [productsRes, categoriesRes, collectionsRes] = await Promise.all([
|
|
15
|
+
client.catalogue.getProducts({ limit: 500 }),
|
|
16
|
+
client.catalogue.getCategories(),
|
|
17
|
+
client.catalogue.getCollections(),
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const products: Product[] = productsRes.ok ? productsRes.value.items : [];
|
|
21
|
+
const categories = categoriesRes.ok ? categoriesRes.value : [];
|
|
22
|
+
const collections = collectionsRes.ok ? collectionsRes.value : [];
|
|
23
|
+
|
|
24
|
+
const lines: string[] = [];
|
|
25
|
+
lines.push(`# ${brand.name}`);
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push(`> ${brand.llms.summary}`);
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push("## Browse");
|
|
30
|
+
lines.push(`- [Home](${SITE_URL}/)`);
|
|
31
|
+
lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);
|
|
32
|
+
|
|
33
|
+
if (categories.length > 0) {
|
|
34
|
+
lines.push("");
|
|
35
|
+
lines.push("## Categories");
|
|
36
|
+
for (const c of categories) {
|
|
37
|
+
lines.push(
|
|
38
|
+
`- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (collections.length > 0) {
|
|
44
|
+
lines.push("");
|
|
45
|
+
lines.push("## Collections");
|
|
46
|
+
for (const c of collections) {
|
|
47
|
+
lines.push(
|
|
48
|
+
`- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (products.length > 0) {
|
|
54
|
+
lines.push("");
|
|
55
|
+
lines.push("## Products");
|
|
56
|
+
for (const p of products) {
|
|
57
|
+
const slug = p.slug ?? p.id;
|
|
58
|
+
const price = `${p.currency ?? brand.currency} ${p.default_price}`;
|
|
59
|
+
const desc = p.description ? ` — ${p.description.replace(/\s+/g, " ").slice(0, 200)}` : "";
|
|
60
|
+
lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
lines.push("");
|
|
65
|
+
lines.push("## Information");
|
|
66
|
+
lines.push(`- [About](${SITE_URL}/about)`);
|
|
67
|
+
lines.push(`- [FAQ](${SITE_URL}/faq)`);
|
|
68
|
+
lines.push(`- [Terms of Service](${SITE_URL}/terms)`);
|
|
69
|
+
lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);
|
|
70
|
+
lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);
|
|
71
|
+
lines.push("");
|
|
72
|
+
lines.push("## Contact");
|
|
73
|
+
lines.push(`- Email: ${brand.contact.email}`);
|
|
74
|
+
lines.push(`- Phone: ${brand.contact.phone}`);
|
|
75
|
+
lines.push(`- Address: ${brand.contact.address}`);
|
|
76
|
+
|
|
77
|
+
return lines.join("\n") + "\n";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `/llms.txt` — machine-readable site index for LLMs (per llmstxt.org).
|
|
82
|
+
* Lets coding agents and chat assistants find products, categories, and
|
|
83
|
+
* support pages without scraping HTML. Plain Markdown so it streams cheaply
|
|
84
|
+
* into context windows.
|
|
85
|
+
*/
|
|
86
|
+
export async function GET(): Promise<Response> {
|
|
87
|
+
const body = await buildLlmsTxt();
|
|
88
|
+
return new Response(body, {
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
91
|
+
"Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { brand } from "@/lib/brand";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: `Page not found — ${brand.name}`,
|
|
7
|
+
description: "We couldn't find that page.",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function NotFound() {
|
|
11
|
+
return (
|
|
12
|
+
<section className="max-w-2xl mx-auto px-8 py-20 text-center">
|
|
13
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">
|
|
14
|
+
404
|
|
15
|
+
</p>
|
|
16
|
+
<h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">
|
|
17
|
+
Page not found.
|
|
18
|
+
</h1>
|
|
19
|
+
<p className="text-muted-foreground leading-relaxed mb-8">
|
|
20
|
+
The URL you followed might be old, a typo, or pointing at a product
|
|
21
|
+
that's sold out for the day. Try the menu or head back home.
|
|
22
|
+
</p>
|
|
23
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
24
|
+
<Link
|
|
25
|
+
href="/shop"
|
|
26
|
+
className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"
|
|
27
|
+
>
|
|
28
|
+
Browse the menu
|
|
29
|
+
</Link>
|
|
30
|
+
<Link
|
|
31
|
+
href="/"
|
|
32
|
+
className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"
|
|
33
|
+
>
|
|
34
|
+
Back home
|
|
35
|
+
</Link>
|
|
36
|
+
</div>
|
|
37
|
+
</section>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {
|
|
4
|
+
const { id } = await params;
|
|
5
|
+
return (
|
|
6
|
+
<section className="max-w-2xl mx-auto px-8 py-20 text-center">
|
|
7
|
+
<h1 className="font-serif text-3xl mt-0 mb-3">Thanks — your order is confirmed</h1>
|
|
8
|
+
<p className="text-muted-foreground">
|
|
9
|
+
Order <code className="font-mono text-foreground">{id}</code>
|
|
10
|
+
</p>
|
|
11
|
+
<p className="mt-6">
|
|
12
|
+
<Link
|
|
13
|
+
href="/"
|
|
14
|
+
className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"
|
|
15
|
+
>
|
|
16
|
+
Continue shopping
|
|
17
|
+
</Link>
|
|
18
|
+
</p>
|
|
19
|
+
</section>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { cacheTag, cacheLife } from "next/cache";
|
|
4
|
+
import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
|
|
5
|
+
import { Hero } from "@/components/hero";
|
|
6
|
+
import { CollectionStrip } from "@/components/collection-strip";
|
|
7
|
+
import { CategoryGrid } from "@/components/category-grid";
|
|
8
|
+
import { brand } from "@/lib/brand";
|
|
9
|
+
|
|
10
|
+
export const metadata: Metadata = {
|
|
11
|
+
title: `${brand.name} — ${brand.hero.title}`,
|
|
12
|
+
description: brand.description,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function getHomeData() {
|
|
16
|
+
"use cache";
|
|
17
|
+
cacheTag(tags.collections(), tags.categories(), tags.products());
|
|
18
|
+
cacheLife("hours");
|
|
19
|
+
|
|
20
|
+
const client = getServerClient();
|
|
21
|
+
const [colRes, catRes] = await Promise.all([
|
|
22
|
+
client.catalogue.getCollections(),
|
|
23
|
+
client.catalogue.getCategories(),
|
|
24
|
+
]);
|
|
25
|
+
const collections = colRes.ok ? colRes.value : [];
|
|
26
|
+
const categories = catRes.ok ? catRes.value : [];
|
|
27
|
+
|
|
28
|
+
const collectionsWithProducts = await Promise.all(
|
|
29
|
+
collections.map(async (col) => {
|
|
30
|
+
const r = await client.catalogue.getCollectionProducts(col.id);
|
|
31
|
+
const items = r.ok
|
|
32
|
+
? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))
|
|
33
|
+
: [];
|
|
34
|
+
return { collection: col, products: items };
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
collections: collectionsWithProducts.filter((x) => x.products.length > 0),
|
|
40
|
+
categories,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default async function HomePage() {
|
|
45
|
+
const { collections, categories } = await getHomeData();
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<Hero
|
|
50
|
+
badge={brand.hero.badge}
|
|
51
|
+
title={brand.hero.title}
|
|
52
|
+
subtitle={brand.hero.subtitle}
|
|
53
|
+
/>
|
|
54
|
+
{collections.map(({ collection, products }) => (
|
|
55
|
+
<Suspense
|
|
56
|
+
key={collection.id}
|
|
57
|
+
fallback={<StripSkeleton title={collection.name} />}
|
|
58
|
+
>
|
|
59
|
+
<CollectionStrip
|
|
60
|
+
collection={collection}
|
|
61
|
+
products={products}
|
|
62
|
+
collectionHref={`/collections/${collection.slug}`}
|
|
63
|
+
/>
|
|
64
|
+
</Suspense>
|
|
65
|
+
))}
|
|
66
|
+
<Suspense fallback={<CategoryGridSkeleton />}>
|
|
67
|
+
<CategoryGrid categories={categories} />
|
|
68
|
+
</Suspense>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function StripSkeleton({ title }: { title: string }) {
|
|
74
|
+
return (
|
|
75
|
+
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
76
|
+
<h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>
|
|
77
|
+
<div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">
|
|
78
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
79
|
+
<div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function CategoryGridSkeleton() {
|
|
87
|
+
return (
|
|
88
|
+
<section className="max-w-7xl mx-auto px-8 pt-14">
|
|
89
|
+
<div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />
|
|
90
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
91
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
92
|
+
<div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { brand } from "@/lib/brand";
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: `Privacy Policy — ${brand.name}`,
|
|
6
|
+
description: `How ${brand.name} collects, uses, and protects your personal data.`,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function PrivacyPage() {
|
|
10
|
+
const p = brand.privacy;
|
|
11
|
+
return (
|
|
12
|
+
<article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">
|
|
13
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">
|
|
14
|
+
{p.eyebrow}
|
|
15
|
+
</p>
|
|
16
|
+
<h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">
|
|
17
|
+
{p.title}
|
|
18
|
+
</h1>
|
|
19
|
+
<p className="text-sm text-muted-foreground not-prose mb-10">
|
|
20
|
+
Last updated: {p.lastUpdated}
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<section className="space-y-5 leading-relaxed text-foreground/90">
|
|
24
|
+
{p.sections.map((s) => (
|
|
25
|
+
<div key={s.heading}>
|
|
26
|
+
<h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>
|
|
27
|
+
{typeof s.body === "string" ? (
|
|
28
|
+
<p>{s.body}</p>
|
|
29
|
+
) : (
|
|
30
|
+
<>
|
|
31
|
+
<p>{s.body.intro}</p>
|
|
32
|
+
<ul className="list-disc pl-6 space-y-2">
|
|
33
|
+
{s.body.bullets.map((b) => (
|
|
34
|
+
<li key={b}>{b}</li>
|
|
35
|
+
))}
|
|
36
|
+
</ul>
|
|
37
|
+
</>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
41
|
+
</section>
|
|
42
|
+
</article>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
|
|
3
|
+
const SITE_URL =
|
|
4
|
+
process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
|
|
5
|
+
|
|
6
|
+
export default function robots(): MetadataRoute.Robots {
|
|
7
|
+
return {
|
|
8
|
+
rules: [
|
|
9
|
+
{
|
|
10
|
+
userAgent: "*",
|
|
11
|
+
allow: "/",
|
|
12
|
+
disallow: ["/cart", "/checkout", "/orders/", "/api/"],
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
sitemap: `${SITE_URL}/sitemap.xml`,
|
|
16
|
+
host: SITE_URL,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { cacheTag, cacheLife } from "next/cache";
|
|
3
|
+
import { getServerClient, tags } from "@cimplify/sdk/server";
|
|
4
|
+
import { ShopClient } from "./shop-client";
|
|
5
|
+
import { brand } from "@/lib/brand";
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: `Shop — ${brand.name}`,
|
|
9
|
+
description: brand.description,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function getShopData() {
|
|
13
|
+
"use cache";
|
|
14
|
+
cacheTag(tags.products(), tags.categories());
|
|
15
|
+
cacheLife("hours");
|
|
16
|
+
|
|
17
|
+
const client = getServerClient();
|
|
18
|
+
const [p, c] = await Promise.all([
|
|
19
|
+
client.catalogue.getProducts({ limit: 50 }),
|
|
20
|
+
client.catalogue.getCategories(),
|
|
21
|
+
]);
|
|
22
|
+
return {
|
|
23
|
+
products: p.ok ? p.value.items : [],
|
|
24
|
+
categories: c.ok ? c.value : [],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default async function ShopPage() {
|
|
29
|
+
const { products, categories } = await getShopData();
|
|
30
|
+
return <ShopClient products={products} categories={categories} />;
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CataloguePage } from "@cimplify/sdk/react";
|
|
4
|
+
import type { Category, Product } from "@cimplify/sdk";
|
|
5
|
+
import { StoreProductCard } from "@/components/store-product-card";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Client island for the shop page. Server-side fetches all products and
|
|
9
|
+
* categories (cached via `'use cache'` in `app/shop/page.tsx`), then hands
|
|
10
|
+
* them to `<CataloguePage>` which owns the interactive filter / sort state.
|
|
11
|
+
*/
|
|
12
|
+
export function ShopClient({
|
|
13
|
+
products,
|
|
14
|
+
categories,
|
|
15
|
+
}: {
|
|
16
|
+
products: Product[];
|
|
17
|
+
categories: Category[];
|
|
18
|
+
}) {
|
|
19
|
+
return (
|
|
20
|
+
<CataloguePage
|
|
21
|
+
title="The Menu"
|
|
22
|
+
products={products}
|
|
23
|
+
categories={categories}
|
|
24
|
+
renderCard={(p) => <StoreProductCard product={p} />}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { getServerClient, type Product } from "@cimplify/sdk/server";
|
|
3
|
+
|
|
4
|
+
const SITE_URL =
|
|
5
|
+
process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
|
|
6
|
+
|
|
7
|
+
const STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [
|
|
8
|
+
{ path: "/", priority: 1.0, changeFrequency: "daily" },
|
|
9
|
+
{ path: "/shop", priority: 0.9, changeFrequency: "daily" },
|
|
10
|
+
{ path: "/about", priority: 0.5, changeFrequency: "monthly" },
|
|
11
|
+
{ path: "/faq", priority: 0.4, changeFrequency: "monthly" },
|
|
12
|
+
{ path: "/terms", priority: 0.2, changeFrequency: "monthly" },
|
|
13
|
+
{ path: "/privacy", priority: 0.2, changeFrequency: "monthly" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
17
|
+
const now = new Date();
|
|
18
|
+
const client = getServerClient();
|
|
19
|
+
|
|
20
|
+
const [productsRes, categoriesRes, collectionsRes] = await Promise.all([
|
|
21
|
+
client.catalogue.getProducts({ limit: 500 }),
|
|
22
|
+
client.catalogue.getCategories(),
|
|
23
|
+
client.catalogue.getCollections(),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const products = productsRes.ok ? productsRes.value.items : [];
|
|
27
|
+
const categories = categoriesRes.ok ? categoriesRes.value : [];
|
|
28
|
+
const collections = collectionsRes.ok ? collectionsRes.value : [];
|
|
29
|
+
|
|
30
|
+
const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({
|
|
31
|
+
url: `${SITE_URL}${r.path}`,
|
|
32
|
+
lastModified: now,
|
|
33
|
+
changeFrequency: r.changeFrequency,
|
|
34
|
+
priority: r.priority,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// Bakery uses a URL-driven product modal (`?product=<slug>` on the home or
|
|
38
|
+
// shop pages). Emit those as deep-linkable URLs so search engines and LLMs
|
|
39
|
+
// can index each product canonically.
|
|
40
|
+
const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({
|
|
41
|
+
url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,
|
|
42
|
+
lastModified: p.updated_at ? new Date(p.updated_at) : now,
|
|
43
|
+
changeFrequency: "weekly",
|
|
44
|
+
priority: 0.7,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({
|
|
48
|
+
url: `${SITE_URL}/categories/${c.slug}`,
|
|
49
|
+
lastModified: now,
|
|
50
|
+
changeFrequency: "weekly",
|
|
51
|
+
priority: 0.6,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({
|
|
55
|
+
url: `${SITE_URL}/collections/${c.slug}`,
|
|
56
|
+
lastModified: now,
|
|
57
|
+
changeFrequency: "weekly",
|
|
58
|
+
priority: 0.6,
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];
|
|
62
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { brand } from "@/lib/brand";
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: `Terms of Service — ${brand.name}`,
|
|
6
|
+
description: `The rules of the road for ordering from ${brand.name}.`,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function TermsPage() {
|
|
10
|
+
const t = brand.terms;
|
|
11
|
+
return (
|
|
12
|
+
<article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">
|
|
13
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">
|
|
14
|
+
{t.eyebrow}
|
|
15
|
+
</p>
|
|
16
|
+
<h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">
|
|
17
|
+
{t.title}
|
|
18
|
+
</h1>
|
|
19
|
+
<p className="text-sm text-muted-foreground not-prose mb-10">
|
|
20
|
+
Last updated: {t.lastUpdated}
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<section className="space-y-5 leading-relaxed text-foreground/90">
|
|
24
|
+
{t.sections.map((s) => (
|
|
25
|
+
<div key={s.heading}>
|
|
26
|
+
<h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>
|
|
27
|
+
{typeof s.body === "string" ? (
|
|
28
|
+
<p>{s.body}</p>
|
|
29
|
+
) : (
|
|
30
|
+
<>
|
|
31
|
+
<p>{s.body.intro}</p>
|
|
32
|
+
<ul className="list-disc pl-6 space-y-2">
|
|
33
|
+
{s.body.bullets.map((b) => (
|
|
34
|
+
<li key={b}>{b}</li>
|
|
35
|
+
))}
|
|
36
|
+
</ul>
|
|
37
|
+
</>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
41
|
+
</section>
|
|
42
|
+
</article>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useCartCount } from "@/lib/cart";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cart pill — dynamic island. Reads the live cart count via the SDK and
|
|
8
|
+
* renders a link to `/cart`. Wrap in `<Suspense fallback={<CartPillSkeleton/>}>`
|
|
9
|
+
* so the cached header chrome streams without blocking on the cart fetch.
|
|
10
|
+
*/
|
|
11
|
+
export function CartPill() {
|
|
12
|
+
const { count } = useCartCount();
|
|
13
|
+
return (
|
|
14
|
+
<Link
|
|
15
|
+
href="/cart"
|
|
16
|
+
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105"
|
|
17
|
+
>
|
|
18
|
+
Cart · {count}
|
|
19
|
+
</Link>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CartPillSkeleton() {
|
|
24
|
+
return (
|
|
25
|
+
<span
|
|
26
|
+
aria-hidden
|
|
27
|
+
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"
|
|
28
|
+
>
|
|
29
|
+
Cart · …
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";
|
|
5
|
+
import type { Category } from "@cimplify/sdk";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Homepage category tiles — defers to the SDK's <CategoryGrid/> for layout
|
|
9
|
+
* and accessibility, and routes selections to /categories/:slug.
|
|
10
|
+
*/
|
|
11
|
+
export function CategoryGrid({ categories }: { categories?: Category[] }) {
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
return (
|
|
14
|
+
<section className="max-w-7xl mx-auto px-8 pt-14">
|
|
15
|
+
<h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>
|
|
16
|
+
<SdkCategoryGrid
|
|
17
|
+
categories={categories}
|
|
18
|
+
onSelect={(c) => router.push(`/categories/${c.slug}`)}
|
|
19
|
+
classNames={{
|
|
20
|
+
item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",
|
|
21
|
+
name: "font-serif text-lg font-semibold text-foreground",
|
|
22
|
+
description: "text-xs text-muted-foreground",
|
|
23
|
+
count: "text-xs text-muted-foreground mt-1",
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
</section>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import type { Collection, Product } from "@cimplify/sdk";
|
|
3
|
+
import { StoreProductCard } from "./store-product-card";
|
|
4
|
+
|
|
5
|
+
interface CollectionStripProps {
|
|
6
|
+
collection: Collection;
|
|
7
|
+
products: Product[];
|
|
8
|
+
collectionHref?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Horizontal strip of products under a collection title. Cards come from the
|
|
13
|
+
* SDK so every variant (Food, Bundle, Composite, Service, …) renders
|
|
14
|
+
* correctly; clicks open the shared URL-driven product modal.
|
|
15
|
+
*/
|
|
16
|
+
export function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {
|
|
17
|
+
if (products.length === 0) return null;
|
|
18
|
+
return (
|
|
19
|
+
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
20
|
+
<header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">
|
|
21
|
+
<h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>
|
|
22
|
+
{collectionHref && (
|
|
23
|
+
<Link
|
|
24
|
+
href={collectionHref}
|
|
25
|
+
className="text-[13px] font-semibold text-primary hover:underline"
|
|
26
|
+
>
|
|
27
|
+
See all →
|
|
28
|
+
</Link>
|
|
29
|
+
)}
|
|
30
|
+
{collection.description && (
|
|
31
|
+
<p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">
|
|
32
|
+
{collection.description}
|
|
33
|
+
</p>
|
|
34
|
+
)}
|
|
35
|
+
</header>
|
|
36
|
+
<div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">
|
|
37
|
+
{products.slice(0, 8).map((p) => (
|
|
38
|
+
<div key={p.id} className="snap-start">
|
|
39
|
+
<StoreProductCard product={p} />
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</section>
|
|
44
|
+
);
|
|
45
|
+
}
|