@cimplify/sdk 0.40.0 → 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/mock/cli.mjs +30 -6
- package/dist/mock/library.js +30 -6
- package/dist/mock/library.mjs +30 -6
- package/dist/mock/msw.js +30 -6
- package/dist/mock/msw.mjs +30 -6
- 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 +50 -37
- package/templates/storefront-retail/app/collections/[slug]/page.tsx +50 -37
- 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 +111 -26
- 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 +36 -3
- package/templates/storefront-retail/app/shop/shop-client.tsx +6 -1
- 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 +27 -0
- package/templates/storefront-retail/components/category-tiles.tsx +104 -0
- package/templates/storefront-retail/components/feature-hero.tsx +84 -0
- 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 +50 -0
- package/templates/storefront-retail/components/promo-banner.tsx +41 -0
- package/templates/storefront-retail/components/section-heading.tsx +37 -0
- package/templates/storefront-retail/components/store-product-card.tsx +7 -7
- package/templates/storefront-retail/components/trade-in-cta.tsx +54 -0
- package/templates/storefront-retail/components/trust-bar.tsx +66 -0
- 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,24 @@
|
|
|
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="text-3xl mt-0 mb-3 font-bold -tracking-[0.025em]">
|
|
8
|
+
Order confirmed.
|
|
9
|
+
</h1>
|
|
10
|
+
<p className="text-muted-foreground">
|
|
11
|
+
Order <code className="font-mono text-foreground">{id}</code> — you'll
|
|
12
|
+
get an SMS with tracking within 30 minutes.
|
|
13
|
+
</p>
|
|
14
|
+
<p className="mt-6">
|
|
15
|
+
<Link
|
|
16
|
+
href="/"
|
|
17
|
+
className="inline-block px-6 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"
|
|
18
|
+
>
|
|
19
|
+
Continue shopping
|
|
20
|
+
</Link>
|
|
21
|
+
</p>
|
|
22
|
+
</section>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
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 { FeatureHero } from "@/components/feature-hero";
|
|
6
|
+
import { CategoryTiles } from "@/components/category-tiles";
|
|
7
|
+
import { CollectionStrip } from "@/components/collection-strip";
|
|
8
|
+
import { PromoBanner } from "@/components/promo-banner";
|
|
9
|
+
import { TrustBar } from "@/components/trust-bar";
|
|
10
|
+
import { BrandMarquee } from "@/components/brand-marquee";
|
|
11
|
+
import { TradeInCta } from "@/components/trade-in-cta";
|
|
12
|
+
import { Newsletter } from "@/components/newsletter";
|
|
13
|
+
import { SectionHeading } from "@/components/section-heading";
|
|
14
|
+
import { StoreProductCard } from "@/components/store-product-card";
|
|
15
|
+
import { brand } from "@/lib/brand";
|
|
16
|
+
|
|
17
|
+
export const metadata: Metadata = {
|
|
18
|
+
title: brand.hero.title,
|
|
19
|
+
description: brand.description,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
interface CollectionWithProducts {
|
|
23
|
+
collection: { id: string; slug: string; name: string; description?: string };
|
|
24
|
+
products: Product[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function getHomeData() {
|
|
28
|
+
"use cache";
|
|
29
|
+
cacheTag(tags.collections(), tags.categories(), tags.products());
|
|
30
|
+
cacheLife("hours");
|
|
31
|
+
|
|
32
|
+
const client = getServerClient();
|
|
33
|
+
const [colRes, catRes, productsRes] = await Promise.all([
|
|
34
|
+
client.catalogue.getCollections(),
|
|
35
|
+
client.catalogue.getCategories(),
|
|
36
|
+
client.catalogue.getProducts({ limit: 12 }),
|
|
37
|
+
]);
|
|
38
|
+
const collections = colRes.ok ? colRes.value : [];
|
|
39
|
+
const categories = catRes.ok ? catRes.value : [];
|
|
40
|
+
const allProducts = productsRes.ok ? productsRes.value.items : [];
|
|
41
|
+
|
|
42
|
+
const collectionsWithProducts: CollectionWithProducts[] = await Promise.all(
|
|
43
|
+
collections.map(async (col) => {
|
|
44
|
+
const r = await client.catalogue.getCollectionProducts(col.id);
|
|
45
|
+
const items = r.ok
|
|
46
|
+
? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))
|
|
47
|
+
: [];
|
|
48
|
+
return { collection: col, products: items };
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
collections: collectionsWithProducts.filter((x) => x.products.length > 0),
|
|
54
|
+
categories,
|
|
55
|
+
featured: allProducts.slice(0, 4),
|
|
56
|
+
newArrivals: allProducts.slice(4, 12),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Verified-reachable Unsplash fashion editorial — replace with your own asset.
|
|
61
|
+
const HERO_FALLBACK_IMAGE =
|
|
62
|
+
"https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=2000&h=1400&fit=crop&auto=format&q=85";
|
|
63
|
+
|
|
64
|
+
export default async function HomePage() {
|
|
65
|
+
const { collections, categories, featured, newArrivals } = await getHomeData();
|
|
66
|
+
const heroProduct = featured[0];
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<FeatureHero
|
|
71
|
+
eyebrow="Featured · in stock"
|
|
72
|
+
badge="Just landed"
|
|
73
|
+
title={brand.hero.title}
|
|
74
|
+
description={heroProduct?.description ?? brand.hero.subtitle}
|
|
75
|
+
primaryCta={{
|
|
76
|
+
label: heroProduct ? `Shop ${heroProduct.name}` : brand.hero.primaryCtaLabel,
|
|
77
|
+
href: heroProduct ? `/products/${heroProduct.slug ?? heroProduct.id}` : "/shop",
|
|
78
|
+
}}
|
|
79
|
+
secondaryCta={
|
|
80
|
+
brand.hero.secondaryCtaLabel && brand.hero.secondaryCtaHref
|
|
81
|
+
? { label: brand.hero.secondaryCtaLabel, href: brand.hero.secondaryCtaHref }
|
|
82
|
+
: undefined
|
|
83
|
+
}
|
|
84
|
+
imageUrl={heroProduct?.image_url ?? heroProduct?.images?.[0] ?? HERO_FALLBACK_IMAGE}
|
|
85
|
+
imageAlt={heroProduct?.name ?? "Featured product"}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<TrustBar />
|
|
89
|
+
|
|
90
|
+
<Suspense fallback={<CategoryTilesSkeleton />}>
|
|
91
|
+
<CategoryTiles categories={categories} />
|
|
92
|
+
</Suspense>
|
|
93
|
+
|
|
94
|
+
<PromoBanner />
|
|
95
|
+
|
|
96
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">
|
|
97
|
+
<SectionHeading
|
|
98
|
+
eyebrow="Just dropped"
|
|
99
|
+
title="New this week."
|
|
100
|
+
description="Hand-picked launches and freshly restocked staples."
|
|
101
|
+
link={{ label: "Browse all", href: "/shop" }}
|
|
102
|
+
/>
|
|
103
|
+
<Suspense fallback={<GridSkeleton count={4} />}>
|
|
104
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
105
|
+
{newArrivals.slice(0, 4).map((p) => (
|
|
106
|
+
<StoreProductCard key={p.id} product={p} />
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
</Suspense>
|
|
110
|
+
</section>
|
|
111
|
+
|
|
112
|
+
<BrandMarquee />
|
|
113
|
+
|
|
114
|
+
{collections.map(({ collection, products }) => (
|
|
115
|
+
<Suspense
|
|
116
|
+
key={collection.id}
|
|
117
|
+
fallback={<StripSkeleton title={collection.name} />}
|
|
118
|
+
>
|
|
119
|
+
<CollectionStrip
|
|
120
|
+
collection={collection}
|
|
121
|
+
products={products}
|
|
122
|
+
collectionHref={`/collections/${collection.slug}`}
|
|
123
|
+
/>
|
|
124
|
+
</Suspense>
|
|
125
|
+
))}
|
|
126
|
+
|
|
127
|
+
<TradeInCta />
|
|
128
|
+
|
|
129
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">
|
|
130
|
+
<SectionHeading
|
|
131
|
+
eyebrow="Best sellers"
|
|
132
|
+
title="What everyone's buying."
|
|
133
|
+
link={{ label: "See more", href: "/shop" }}
|
|
134
|
+
/>
|
|
135
|
+
<Suspense fallback={<GridSkeleton count={4} />}>
|
|
136
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
137
|
+
{newArrivals.slice(4, 8).map((p) => (
|
|
138
|
+
<StoreProductCard key={p.id} product={p} />
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
</Suspense>
|
|
142
|
+
</section>
|
|
143
|
+
|
|
144
|
+
<Newsletter />
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function CategoryTilesSkeleton() {
|
|
150
|
+
return (
|
|
151
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">
|
|
152
|
+
<div className="h-8 w-64 bg-muted rounded mb-8 animate-pulse" />
|
|
153
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
|
|
154
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
155
|
+
<div key={i} className="h-40 bg-muted rounded-2xl animate-pulse" />
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
</section>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function GridSkeleton({ count }: { count: number }) {
|
|
163
|
+
return (
|
|
164
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
165
|
+
{Array.from({ length: count }).map((_, i) => (
|
|
166
|
+
<div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function StripSkeleton({ title }: { title: string }) {
|
|
173
|
+
return (
|
|
174
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 py-12">
|
|
175
|
+
<h2 className="text-[26px] font-semibold m-0 mb-5 -tracking-[0.02em]">{title}</h2>
|
|
176
|
+
<div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">
|
|
177
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
178
|
+
<div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
</section>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -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-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">
|
|
14
|
+
{p.eyebrow}
|
|
15
|
+
</p>
|
|
16
|
+
<h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">
|
|
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="text-2xl font-semibold mt-0 -tracking-[0.02em]">{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,165 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { notFound } from "next/navigation";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { cacheTag, cacheLife } from "next/cache";
|
|
6
|
+
import {
|
|
7
|
+
getServerClient,
|
|
8
|
+
tags,
|
|
9
|
+
type Product,
|
|
10
|
+
type ProductWithDetails,
|
|
11
|
+
} from "@cimplify/sdk/server";
|
|
12
|
+
import { ProductDetail } from "./product-detail";
|
|
13
|
+
import { brand } from "@/lib/brand";
|
|
14
|
+
|
|
15
|
+
const SITE_URL =
|
|
16
|
+
process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
|
|
17
|
+
|
|
18
|
+
function productLd(product: ProductWithDetails) {
|
|
19
|
+
const image = product.image_url ?? product.images?.[0];
|
|
20
|
+
const inStock = product.inventory_status?.in_stock !== false;
|
|
21
|
+
return {
|
|
22
|
+
"@context": "https://schema.org",
|
|
23
|
+
"@type": "Product",
|
|
24
|
+
name: product.name,
|
|
25
|
+
description: product.description ?? undefined,
|
|
26
|
+
image: image ? [image] : undefined,
|
|
27
|
+
sku: product.id,
|
|
28
|
+
brand: { "@type": "Brand", name: brand.name },
|
|
29
|
+
offers: {
|
|
30
|
+
"@type": "Offer",
|
|
31
|
+
price: product.default_price,
|
|
32
|
+
priceCurrency: product.currency ?? brand.currency,
|
|
33
|
+
availability: inStock
|
|
34
|
+
? "https://schema.org/InStock"
|
|
35
|
+
: "https://schema.org/OutOfStock",
|
|
36
|
+
url: `${SITE_URL}/products/${product.slug ?? product.id}`,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ProductData {
|
|
42
|
+
product: ProductWithDetails;
|
|
43
|
+
related: Product[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getProduct(slug: string): Promise<ProductData | null> {
|
|
47
|
+
"use cache";
|
|
48
|
+
cacheTag(tags.product(slug), tags.products());
|
|
49
|
+
cacheLife("hours");
|
|
50
|
+
|
|
51
|
+
const client = getServerClient();
|
|
52
|
+
const r = await client.catalogue.getProductBySlug(slug);
|
|
53
|
+
if (!r.ok) return null;
|
|
54
|
+
|
|
55
|
+
const related = r.value.category_id
|
|
56
|
+
? await client.catalogue
|
|
57
|
+
.getCategoryProducts(r.value.category_id)
|
|
58
|
+
.then((res) =>
|
|
59
|
+
res.ok
|
|
60
|
+
? (
|
|
61
|
+
((res.value as { items?: Product[] }).items ??
|
|
62
|
+
(res.value as Product[])) as Product[]
|
|
63
|
+
).filter((p) => p.id !== r.value.id).slice(0, 4)
|
|
64
|
+
: [],
|
|
65
|
+
)
|
|
66
|
+
: [];
|
|
67
|
+
|
|
68
|
+
return { product: r.value, related };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function generateMetadata({
|
|
72
|
+
params,
|
|
73
|
+
}: {
|
|
74
|
+
params: Promise<{ slug: string }>;
|
|
75
|
+
}): Promise<Metadata> {
|
|
76
|
+
const { slug } = await params;
|
|
77
|
+
const data = await getProduct(slug);
|
|
78
|
+
if (!data) return {};
|
|
79
|
+
const { product } = data;
|
|
80
|
+
const image = product.image_url ?? product.images?.[0];
|
|
81
|
+
return {
|
|
82
|
+
title: `${product.name} — ${brand.name}`,
|
|
83
|
+
description: product.description ?? undefined,
|
|
84
|
+
openGraph: {
|
|
85
|
+
title: product.name,
|
|
86
|
+
description: product.description ?? undefined,
|
|
87
|
+
images: image ? [{ url: image }] : undefined,
|
|
88
|
+
type: "website",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default async function ProductPage({
|
|
94
|
+
params,
|
|
95
|
+
}: {
|
|
96
|
+
params: Promise<{ slug: string }>;
|
|
97
|
+
}) {
|
|
98
|
+
return (
|
|
99
|
+
<Suspense fallback={<ProductSkeleton />}>
|
|
100
|
+
<ProductContent params={params} />
|
|
101
|
+
</Suspense>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function ProductContent({
|
|
106
|
+
params,
|
|
107
|
+
}: {
|
|
108
|
+
params: Promise<{ slug: string }>;
|
|
109
|
+
}) {
|
|
110
|
+
const { slug } = await params;
|
|
111
|
+
const data = await getProduct(slug);
|
|
112
|
+
if (!data) notFound();
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
<script
|
|
117
|
+
type="application/ld+json"
|
|
118
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(productLd(data.product)) }}
|
|
119
|
+
/>
|
|
120
|
+
<nav
|
|
121
|
+
aria-label="Breadcrumb"
|
|
122
|
+
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"
|
|
123
|
+
>
|
|
124
|
+
<Link href="/" className="hover:text-foreground transition-colors">
|
|
125
|
+
Home
|
|
126
|
+
</Link>
|
|
127
|
+
<span>/</span>
|
|
128
|
+
<Link href="/shop" className="hover:text-foreground transition-colors">
|
|
129
|
+
Shop
|
|
130
|
+
</Link>
|
|
131
|
+
{data.product.category?.slug && (
|
|
132
|
+
<>
|
|
133
|
+
<span>/</span>
|
|
134
|
+
<Link
|
|
135
|
+
href={`/categories/${data.product.category.slug}`}
|
|
136
|
+
className="hover:text-foreground transition-colors"
|
|
137
|
+
>
|
|
138
|
+
{data.product.category.name}
|
|
139
|
+
</Link>
|
|
140
|
+
</>
|
|
141
|
+
)}
|
|
142
|
+
<span>/</span>
|
|
143
|
+
<span className="text-foreground/90 truncate">{data.product.name}</span>
|
|
144
|
+
</nav>
|
|
145
|
+
<ProductDetail product={data.product} related={data.related} />
|
|
146
|
+
</>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function ProductSkeleton() {
|
|
151
|
+
return (
|
|
152
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 py-10">
|
|
153
|
+
<div className="grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 items-start">
|
|
154
|
+
<div className="aspect-square bg-muted rounded-3xl animate-pulse" />
|
|
155
|
+
<div className="space-y-4">
|
|
156
|
+
<div className="h-3 w-24 bg-muted rounded animate-pulse" />
|
|
157
|
+
<div className="h-10 w-3/4 bg-muted rounded animate-pulse" />
|
|
158
|
+
<div className="h-7 w-32 bg-muted rounded animate-pulse" />
|
|
159
|
+
<div className="h-20 w-full bg-muted rounded animate-pulse mt-6" />
|
|
160
|
+
<div className="h-12 w-full bg-muted rounded animate-pulse mt-4" />
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</section>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { ProductPage as SdkProductPage } from "@cimplify/sdk/react";
|
|
6
|
+
import type { Product, ProductWithDetails } from "@cimplify/sdk";
|
|
7
|
+
import { StoreProductCard } from "@/components/store-product-card";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Client island for the product detail page.
|
|
11
|
+
*
|
|
12
|
+
* - Receives a server-fetched `ProductWithDetails` (no client refetch).
|
|
13
|
+
* - Renders the SDK `<ProductPage>` which picks the right layout
|
|
14
|
+
* (Default / Wholesale / Service / Bundle / Composite) automatically.
|
|
15
|
+
* - On add-to-cart success, routes to `/cart`.
|
|
16
|
+
* - Custom Next.js Image renderer for optimised, lazy-loaded gallery shots.
|
|
17
|
+
* - Renders a "You may also like" rail of in-category products below.
|
|
18
|
+
*/
|
|
19
|
+
export function ProductDetail({
|
|
20
|
+
product,
|
|
21
|
+
related,
|
|
22
|
+
}: {
|
|
23
|
+
product: ProductWithDetails;
|
|
24
|
+
related: Product[];
|
|
25
|
+
}) {
|
|
26
|
+
const router = useRouter();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<SdkProductPage
|
|
31
|
+
product={product}
|
|
32
|
+
showRelated={false}
|
|
33
|
+
onAddToCart={() => {
|
|
34
|
+
router.push("/cart");
|
|
35
|
+
}}
|
|
36
|
+
renderImage={({ src, alt, className }) => (
|
|
37
|
+
<Image
|
|
38
|
+
src={src}
|
|
39
|
+
alt={alt}
|
|
40
|
+
width={1200}
|
|
41
|
+
height={1200}
|
|
42
|
+
className={className}
|
|
43
|
+
style={{ width: "100%", height: "auto", objectFit: "cover" }}
|
|
44
|
+
priority
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
className="max-w-7xl mx-auto px-6 sm:px-8 py-8 sm:py-10"
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
{related.length > 0 && (
|
|
51
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-16 border-t border-border mt-8">
|
|
52
|
+
<div className="flex items-end justify-between gap-6 mb-8">
|
|
53
|
+
<div>
|
|
54
|
+
<p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">
|
|
55
|
+
You may also like
|
|
56
|
+
</p>
|
|
57
|
+
<h2 className="text-[clamp(1.5rem,2.5vw,2rem)] font-bold m-0 -tracking-[0.025em]">
|
|
58
|
+
More from this category.
|
|
59
|
+
</h2>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
63
|
+
{related.map((p) => (
|
|
64
|
+
<StoreProductCard key={p.id} product={p} />
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
</section>
|
|
68
|
+
)}
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -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,63 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { cacheTag, cacheLife } from "next/cache";
|
|
4
|
+
import { getServerClient, tags } from "@cimplify/sdk/server";
|
|
5
|
+
import { ShopClient } from "./shop-client";
|
|
6
|
+
import { brand } from "@/lib/brand";
|
|
7
|
+
|
|
8
|
+
export const metadata: Metadata = {
|
|
9
|
+
title: `Shop — ${brand.name}`,
|
|
10
|
+
description: brand.description,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async function getShopData() {
|
|
14
|
+
"use cache";
|
|
15
|
+
cacheTag(tags.products(), tags.categories());
|
|
16
|
+
cacheLife("hours");
|
|
17
|
+
|
|
18
|
+
const client = getServerClient();
|
|
19
|
+
const [p, c] = await Promise.all([
|
|
20
|
+
client.catalogue.getProducts({ limit: 50 }),
|
|
21
|
+
client.catalogue.getCategories(),
|
|
22
|
+
]);
|
|
23
|
+
return {
|
|
24
|
+
products: p.ok ? p.value.items : [],
|
|
25
|
+
categories: c.ok ? c.value : [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default async function ShopPage() {
|
|
30
|
+
const { products, categories } = await getShopData();
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<section className="bg-foreground text-background relative overflow-hidden">
|
|
34
|
+
<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]" />
|
|
35
|
+
<div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">
|
|
36
|
+
<p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">
|
|
37
|
+
Catalogue · {products.length} products
|
|
38
|
+
</p>
|
|
39
|
+
<h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">
|
|
40
|
+
Every product we stock.
|
|
41
|
+
</h1>
|
|
42
|
+
<p className="mt-3 max-w-xl text-base text-background/75">
|
|
43
|
+
Filter, sort, search. Authorised dealer pricing, two-year warranty,
|
|
44
|
+
same-day Accra delivery on every order.
|
|
45
|
+
</p>
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
<Suspense
|
|
49
|
+
fallback={
|
|
50
|
+
<div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">
|
|
51
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
52
|
+
{Array.from({ length: 8 }).map((_, i) => (
|
|
53
|
+
<div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
<ShopClient products={products} categories={categories} />
|
|
60
|
+
</Suspense>
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
* The page hero (in `app/shop/page.tsx`) already provides the title; we
|
|
13
|
+
* pass an empty `title` and override the SDK heading via className so the
|
|
14
|
+
* page reads as one continuous design.
|
|
15
|
+
*/
|
|
16
|
+
export function ShopClient({
|
|
17
|
+
products,
|
|
18
|
+
categories,
|
|
19
|
+
}: {
|
|
20
|
+
products: Product[];
|
|
21
|
+
categories: Category[];
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<CataloguePage
|
|
25
|
+
title="All products"
|
|
26
|
+
products={products}
|
|
27
|
+
categories={categories}
|
|
28
|
+
renderCard={(p) => <StoreProductCard product={p} />}
|
|
29
|
+
className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({
|
|
38
|
+
url: `${SITE_URL}/products/${p.slug ?? p.id}`,
|
|
39
|
+
lastModified: p.updated_at ? new Date(p.updated_at) : now,
|
|
40
|
+
changeFrequency: "weekly",
|
|
41
|
+
priority: 0.7,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({
|
|
45
|
+
url: `${SITE_URL}/categories/${c.slug}`,
|
|
46
|
+
lastModified: now,
|
|
47
|
+
changeFrequency: "weekly",
|
|
48
|
+
priority: 0.6,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({
|
|
52
|
+
url: `${SITE_URL}/collections/${c.slug}`,
|
|
53
|
+
lastModified: now,
|
|
54
|
+
changeFrequency: "weekly",
|
|
55
|
+
priority: 0.6,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];
|
|
59
|
+
}
|