@cimplify/cli 0.6.16 → 0.7.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.
- package/dist/{add-BBVSJ5ZJ.mjs → add-H6VQXQOI.mjs} +1 -1
- package/dist/{chunk-F5EGUNQZ.mjs → chunk-AS3W7SUI.mjs} +81 -25
- package/dist/{chunk-GL2J7272.mjs → chunk-BFT3GO4Q.mjs} +1 -1
- package/dist/{chunk-KCKMGRII.mjs → chunk-HXJZIFHC.mjs} +2 -2
- package/dist/dispatcher.mjs +9 -9
- package/dist/{doctor-T6NA3EMW.mjs → doctor-7QL3TCOV.mjs} +2 -2
- package/dist/{explain-22NV6OL6.mjs → explain-IVURNUO3.mjs} +4 -4
- package/dist/{introspect-QOW24PJF.mjs → introspect-CFVKMSVE.mjs} +2 -2
- package/dist/{list-JHUIIMC2.mjs → list-56VJKIMT.mjs} +1 -1
- package/dist/{update-IOMLDQEN.mjs → update-IYLHJDL2.mjs} +1 -1
- package/package.json +2 -2
- package/templates/storefront-auto/.claude/skills/cimplify-storefront/SKILL.md +34 -18
- package/templates/storefront-auto/CLAUDE.md +3 -2
- package/templates/storefront-auto/app/categories/[slug]/page.tsx +39 -17
- package/templates/storefront-auto/app/collections/[slug]/page.tsx +39 -17
- package/templates/storefront-auto/app/llms.txt/route.ts +13 -9
- package/templates/storefront-auto/app/page.tsx +3 -6
- package/templates/storefront-auto/app/products/[slug]/page.tsx +51 -21
- package/templates/storefront-auto/app/shop/page.tsx +10 -8
- package/templates/storefront-auto/app/shop/shop-client.tsx +1 -1
- package/templates/storefront-auto/app/sitemap-page/page.tsx +13 -9
- package/templates/storefront-auto/bun.lock +2 -2
- package/templates/storefront-auto/components/footer.tsx +0 -1
- package/templates/storefront-auto/lib/site.config.ts +1 -1
- package/templates/storefront-auto/next.config.ts +6 -5
- package/templates/storefront-auto/package.json +1 -1
- package/templates/storefront-bakery/.claude/skills/cimplify-storefront/SKILL.md +34 -18
- package/templates/storefront-bakery/AGENTS.md +4 -3
- package/templates/storefront-bakery/CLAUDE.md +3 -2
- package/templates/storefront-bakery/app/categories/[slug]/page.tsx +39 -17
- package/templates/storefront-bakery/app/collections/[slug]/page.tsx +39 -17
- package/templates/storefront-bakery/app/llms.txt/route.ts +13 -9
- package/templates/storefront-bakery/app/page.tsx +19 -9
- package/templates/storefront-bakery/app/shop/page.tsx +10 -8
- package/templates/storefront-bakery/app/shop/shop-client.tsx +1 -1
- package/templates/storefront-bakery/app/sitemap-page/page.tsx +13 -9
- package/templates/storefront-bakery/bun.lock +2 -2
- package/templates/storefront-bakery/components/footer.tsx +0 -1
- package/templates/storefront-bakery/lib/site.config.ts +1 -1
- package/templates/storefront-bakery/next.config.ts +6 -5
- package/templates/storefront-bakery/package.json +1 -1
- package/templates/storefront-fashion/.claude/skills/cimplify-storefront/SKILL.md +34 -18
- package/templates/storefront-fashion/CLAUDE.md +3 -2
- package/templates/storefront-fashion/app/categories/[slug]/page.tsx +39 -17
- package/templates/storefront-fashion/app/collections/[slug]/page.tsx +39 -17
- package/templates/storefront-fashion/app/llms.txt/route.ts +13 -9
- package/templates/storefront-fashion/app/page.tsx +23 -10
- package/templates/storefront-fashion/app/products/[slug]/page.tsx +51 -21
- package/templates/storefront-fashion/app/shop/page.tsx +10 -8
- package/templates/storefront-fashion/app/shop/shop-client.tsx +1 -1
- package/templates/storefront-fashion/app/sitemap-page/page.tsx +13 -9
- package/templates/storefront-fashion/bun.lock +2 -2
- package/templates/storefront-fashion/components/footer.tsx +0 -1
- package/templates/storefront-fashion/lib/site.config.ts +1 -1
- package/templates/storefront-fashion/next.config.ts +6 -5
- package/templates/storefront-fashion/package.json +1 -1
- package/templates/storefront-grocery/.claude/skills/cimplify-storefront/SKILL.md +34 -18
- package/templates/storefront-grocery/CLAUDE.md +3 -2
- package/templates/storefront-grocery/app/categories/[slug]/page.tsx +39 -17
- package/templates/storefront-grocery/app/collections/[slug]/page.tsx +39 -17
- package/templates/storefront-grocery/app/llms.txt/route.ts +13 -9
- package/templates/storefront-grocery/app/page.tsx +19 -9
- package/templates/storefront-grocery/app/shop/page.tsx +10 -8
- package/templates/storefront-grocery/app/shop/shop-client.tsx +1 -1
- package/templates/storefront-grocery/app/sitemap-page/page.tsx +13 -9
- package/templates/storefront-grocery/bun.lock +2 -2
- package/templates/storefront-grocery/components/footer.tsx +0 -1
- package/templates/storefront-grocery/lib/site.config.ts +1 -1
- package/templates/storefront-grocery/next.config.ts +6 -5
- package/templates/storefront-grocery/package.json +1 -1
- package/templates/storefront-pharmacy/.claude/skills/cimplify-storefront/SKILL.md +34 -18
- package/templates/storefront-pharmacy/CLAUDE.md +3 -2
- package/templates/storefront-pharmacy/app/categories/[slug]/page.tsx +39 -17
- package/templates/storefront-pharmacy/app/collections/[slug]/page.tsx +39 -17
- package/templates/storefront-pharmacy/app/llms.txt/route.ts +13 -9
- package/templates/storefront-pharmacy/app/page.tsx +3 -6
- package/templates/storefront-pharmacy/app/products/[slug]/page.tsx +51 -21
- package/templates/storefront-pharmacy/app/shop/page.tsx +10 -8
- package/templates/storefront-pharmacy/app/shop/shop-client.tsx +1 -1
- package/templates/storefront-pharmacy/app/sitemap-page/page.tsx +13 -9
- package/templates/storefront-pharmacy/bun.lock +2 -2
- package/templates/storefront-pharmacy/components/footer.tsx +0 -1
- package/templates/storefront-pharmacy/lib/site.config.ts +1 -1
- package/templates/storefront-pharmacy/next.config.ts +6 -5
- package/templates/storefront-pharmacy/package.json +1 -1
- package/templates/storefront-restaurant/.claude/skills/cimplify-storefront/SKILL.md +34 -18
- package/templates/storefront-restaurant/CLAUDE.md +3 -2
- package/templates/storefront-restaurant/app/categories/[slug]/page.tsx +39 -17
- package/templates/storefront-restaurant/app/collections/[slug]/page.tsx +39 -17
- package/templates/storefront-restaurant/app/llms.txt/route.ts +13 -9
- package/templates/storefront-restaurant/app/page.tsx +19 -9
- package/templates/storefront-restaurant/app/reservations/page.tsx +7 -7
- package/templates/storefront-restaurant/app/shop/page.tsx +10 -8
- package/templates/storefront-restaurant/app/shop/shop-client.tsx +1 -1
- package/templates/storefront-restaurant/app/sitemap-page/page.tsx +13 -9
- package/templates/storefront-restaurant/bun.lock +2 -2
- package/templates/storefront-restaurant/components/footer.tsx +0 -1
- package/templates/storefront-restaurant/lib/site.config.ts +1 -1
- package/templates/storefront-restaurant/next.config.ts +6 -5
- package/templates/storefront-restaurant/package.json +1 -1
- package/templates/storefront-retail/.claude/skills/cimplify-storefront/SKILL.md +34 -18
- package/templates/storefront-retail/CLAUDE.md +3 -2
- package/templates/storefront-retail/app/categories/[slug]/page.tsx +39 -17
- package/templates/storefront-retail/app/collections/[slug]/page.tsx +39 -17
- package/templates/storefront-retail/app/llms.txt/route.ts +13 -9
- package/templates/storefront-retail/app/page.tsx +23 -10
- package/templates/storefront-retail/app/products/[slug]/page.tsx +51 -21
- package/templates/storefront-retail/app/shop/page.tsx +10 -8
- package/templates/storefront-retail/app/shop/shop-client.tsx +1 -1
- package/templates/storefront-retail/app/sitemap-page/page.tsx +13 -9
- package/templates/storefront-retail/bun.lock +2 -2
- package/templates/storefront-retail/components/footer.tsx +0 -1
- package/templates/storefront-retail/lib/site.config.ts +1 -1
- package/templates/storefront-retail/next.config.ts +6 -5
- package/templates/storefront-retail/package.json +1 -1
- package/templates/storefront-services/.claude/skills/cimplify-storefront/SKILL.md +34 -18
- package/templates/storefront-services/CLAUDE.md +3 -2
- package/templates/storefront-services/app/book/page.tsx +7 -7
- package/templates/storefront-services/app/categories/[slug]/page.tsx +39 -17
- package/templates/storefront-services/app/collections/[slug]/page.tsx +39 -17
- package/templates/storefront-services/app/llms.txt/route.ts +13 -9
- package/templates/storefront-services/app/page.tsx +19 -9
- package/templates/storefront-services/app/shop/page.tsx +10 -8
- package/templates/storefront-services/app/shop/shop-client.tsx +1 -1
- package/templates/storefront-services/app/sitemap-page/page.tsx +13 -9
- package/templates/storefront-services/bun.lock +2 -2
- package/templates/storefront-services/components/footer.tsx +0 -1
- package/templates/storefront-services/lib/site.config.ts +1 -1
- package/templates/storefront-services/next.config.ts +6 -5
- package/templates/storefront-services/package.json +1 -1
- package/templates/storefront-auto/app/api/revalidate/route.ts +0 -5
- package/templates/storefront-bakery/app/api/revalidate/route.ts +0 -5
- package/templates/storefront-fashion/app/api/revalidate/route.ts +0 -5
- package/templates/storefront-grocery/app/api/revalidate/route.ts +0 -5
- package/templates/storefront-pharmacy/app/api/revalidate/route.ts +0 -5
- package/templates/storefront-restaurant/app/api/revalidate/route.ts +0 -5
- package/templates/storefront-retail/app/api/revalidate/route.ts +0 -5
- package/templates/storefront-services/app/api/revalidate/route.ts +0 -5
|
@@ -2,9 +2,8 @@ import type { Metadata } from "next";
|
|
|
2
2
|
import { Suspense } from "react";
|
|
3
3
|
import { notFound } from "next/navigation";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { cacheTag, cacheLife } from "next/cache";
|
|
6
5
|
import {
|
|
7
|
-
|
|
6
|
+
getServerClient,
|
|
8
7
|
tags,
|
|
9
8
|
type Product,
|
|
10
9
|
type ProductWithDetails,
|
|
@@ -13,6 +12,21 @@ import { ProductDetail } from "./product-detail";
|
|
|
13
12
|
import { brand } from "@/lib/brand";
|
|
14
13
|
import { getSiteUrl } from "@/lib/site-url";
|
|
15
14
|
|
|
15
|
+
// Pre-enumerate every product slug at build time so Next can emit cacheable
|
|
16
|
+
// responses (s-maxage instead of no-store) for the route. Without this, every
|
|
17
|
+
// PDP is treated as fully runtime-dynamic and CF edge skips it. Placeholder
|
|
18
|
+
// fallback keeps the build alive if the catalogue API isn't reachable at
|
|
19
|
+
// build time; the page handler already calls notFound() on unknown slugs.
|
|
20
|
+
export async function generateStaticParams() {
|
|
21
|
+
const r = await getServerClient().catalogue.getProducts({ limit: 10_000 });
|
|
22
|
+
if (!r.ok || r.value.items.length === 0) {
|
|
23
|
+
return [{ slug: "__placeholder__" }];
|
|
24
|
+
}
|
|
25
|
+
return r.value.items.map((p) => ({ slug: p.slug ?? p.id }));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const revalidate = 3600;
|
|
29
|
+
|
|
16
30
|
function productLd(product: ProductWithDetails, SITE_URL: string) {
|
|
17
31
|
const image = product.image_url ?? product.images?.[0];
|
|
18
32
|
const inStock = product.inventory_status?.in_stock !== false;
|
|
@@ -41,34 +55,45 @@ interface ProductData {
|
|
|
41
55
|
related: Product[];
|
|
42
56
|
}
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
type ProductResult =
|
|
59
|
+
| { ok: true; data: ProductData }
|
|
60
|
+
| { ok: false; code: string };
|
|
47
61
|
|
|
62
|
+
async function getProduct(slug: string): Promise<ProductResult> {
|
|
48
63
|
const client = getServerClient();
|
|
49
|
-
const r = await client.catalogue.getProductBySlug(slug
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
const r = await client.catalogue.getProductBySlug(slug, {
|
|
65
|
+
cacheOptions: {
|
|
66
|
+
revalidate: 3600,
|
|
67
|
+
tags: [tags.product(slug), tags.products()],
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (!r.ok) return { ok: false, code: r.error.code };
|
|
71
|
+
|
|
72
|
+
const product = r.value;
|
|
54
73
|
// Tag with the resolved ID — Cimplify dispatches tags.product(id), not slug.
|
|
55
74
|
// See https://cimplify.dev/docs/sdk/revalidation#dynamic-routes--tag-by-id-never-by-slug
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const related = r.value.category_id
|
|
75
|
+
const related = product.category_id
|
|
59
76
|
? await client.catalogue
|
|
60
|
-
.getCategoryProducts(
|
|
77
|
+
.getCategoryProducts(product.category_id, undefined, {
|
|
78
|
+
cacheOptions: {
|
|
79
|
+
revalidate: 3600,
|
|
80
|
+
tags: [
|
|
81
|
+
tags.product(product.id),
|
|
82
|
+
tags.categoryProducts(product.category_id),
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
})
|
|
61
86
|
.then((res) =>
|
|
62
87
|
res.ok
|
|
63
88
|
? (
|
|
64
89
|
((res.value as { items?: Product[] }).items ??
|
|
65
90
|
(res.value as Product[])) as Product[]
|
|
66
|
-
).filter((p) => p.id !==
|
|
91
|
+
).filter((p) => p.id !== product.id).slice(0, 4)
|
|
67
92
|
: [],
|
|
68
93
|
)
|
|
69
94
|
: [];
|
|
70
95
|
|
|
71
|
-
return {
|
|
96
|
+
return { ok: true, data: { product, related } };
|
|
72
97
|
}
|
|
73
98
|
|
|
74
99
|
export async function generateMetadata({
|
|
@@ -78,9 +103,9 @@ export async function generateMetadata({
|
|
|
78
103
|
}): Promise<Metadata> {
|
|
79
104
|
const { slug } = await params;
|
|
80
105
|
const siteUrl = await getSiteUrl();
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
83
|
-
const { product } = data;
|
|
106
|
+
const result = await getProduct(slug);
|
|
107
|
+
if (!result.ok) return {};
|
|
108
|
+
const { product } = result.data;
|
|
84
109
|
const image = product.image_url ?? product.images?.[0];
|
|
85
110
|
return {
|
|
86
111
|
title: `${product.name} — ${brand.name}`,
|
|
@@ -113,8 +138,13 @@ async function ProductContent({
|
|
|
113
138
|
}) {
|
|
114
139
|
const { slug } = await params;
|
|
115
140
|
const siteUrl = await getSiteUrl();
|
|
116
|
-
const
|
|
117
|
-
if (!
|
|
141
|
+
const result = await getProduct(slug);
|
|
142
|
+
if (!result.ok) {
|
|
143
|
+
if (result.code === "NOT_FOUND") notFound();
|
|
144
|
+
// Transient SDK error — render the soft skeleton; ISR will retry on next request.
|
|
145
|
+
return <ProductSkeleton />;
|
|
146
|
+
}
|
|
147
|
+
const data = result.data;
|
|
118
148
|
|
|
119
149
|
return (
|
|
120
150
|
<>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
2
|
import { Suspense } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import { CACHE_LIFE_DEFAULT, getServerClient, tags } from "@cimplify/sdk/server";
|
|
3
|
+
import { getServerClient, tags } from "@cimplify/sdk/server";
|
|
5
4
|
import { ShopClient } from "./shop-client";
|
|
6
5
|
import { brand } from "@/lib/brand";
|
|
7
6
|
|
|
@@ -10,15 +9,18 @@ export const metadata: Metadata = {
|
|
|
10
9
|
description: brand.description,
|
|
11
10
|
};
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
"use cache";
|
|
15
|
-
cacheTag(tags.products(), tags.categories());
|
|
16
|
-
cacheLife(CACHE_LIFE_DEFAULT);
|
|
12
|
+
export const revalidate = 3600;
|
|
17
13
|
|
|
14
|
+
async function getShopData() {
|
|
18
15
|
const client = getServerClient();
|
|
19
16
|
const [p, c] = await Promise.all([
|
|
20
|
-
client.catalogue.getProducts(
|
|
21
|
-
|
|
17
|
+
client.catalogue.getProducts(
|
|
18
|
+
{ limit: 50 },
|
|
19
|
+
{ cacheOptions: { revalidate: 3600, tags: [tags.products()] } },
|
|
20
|
+
),
|
|
21
|
+
client.catalogue.getCategories({
|
|
22
|
+
cacheOptions: { revalidate: 3600, tags: [tags.categories()] },
|
|
23
|
+
}),
|
|
22
24
|
]);
|
|
23
25
|
return {
|
|
24
26
|
products: p.ok ? p.value.items : [],
|
|
@@ -6,7 +6,7 @@ import { StoreProductCard } from "@/components/store-product-card";
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Client island for the shop page. Server-side fetches all products and
|
|
9
|
-
* categories (cached
|
|
9
|
+
* categories (ISR-cached in `app/shop/page.tsx`), then hands
|
|
10
10
|
* them to `<CataloguePage>` which owns the interactive filter / sort state.
|
|
11
11
|
*
|
|
12
12
|
* The page hero (in `app/shop/page.tsx`) already provides the title; we
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
2
|
import Link from "next/link";
|
|
3
|
-
import {
|
|
4
|
-
import { CACHE_LIFE_DEFAULT, getServerClient, tags, type Product } from "@cimplify/sdk/server";
|
|
3
|
+
import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
|
|
5
4
|
import { brand } from "@/lib/brand";
|
|
6
5
|
|
|
7
6
|
export const metadata: Metadata = {
|
|
@@ -9,6 +8,8 @@ export const metadata: Metadata = {
|
|
|
9
8
|
description: "A human-readable index of every page on this site.",
|
|
10
9
|
};
|
|
11
10
|
|
|
11
|
+
export const revalidate = 3600;
|
|
12
|
+
|
|
12
13
|
interface SitemapData {
|
|
13
14
|
products: { slug: string; name: string }[];
|
|
14
15
|
categories: { slug: string; name: string }[];
|
|
@@ -16,15 +17,18 @@ interface SitemapData {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
async function getSitemap(): Promise<SitemapData> {
|
|
19
|
-
"use cache";
|
|
20
|
-
cacheTag(tags.products(), tags.categories(), tags.collections());
|
|
21
|
-
cacheLife(CACHE_LIFE_DEFAULT);
|
|
22
|
-
|
|
23
20
|
const client = getServerClient();
|
|
24
21
|
const [pRes, cRes, colRes] = await Promise.all([
|
|
25
|
-
client.catalogue.getProducts(
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
client.catalogue.getProducts(
|
|
23
|
+
{ limit: 500 },
|
|
24
|
+
{ cacheOptions: { revalidate: 3600, tags: [tags.products()] } },
|
|
25
|
+
),
|
|
26
|
+
client.catalogue.getCategories({
|
|
27
|
+
cacheOptions: { revalidate: 3600, tags: [tags.categories()] },
|
|
28
|
+
}),
|
|
29
|
+
client.catalogue.getCollections({
|
|
30
|
+
cacheOptions: { revalidate: 3600, tags: [tags.collections()] },
|
|
31
|
+
}),
|
|
28
32
|
]);
|
|
29
33
|
return {
|
|
30
34
|
products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "__STOREFRONT_NAME__",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@cimplify/sdk": "^0.
|
|
8
|
+
"@cimplify/sdk": "^0.54.0",
|
|
9
9
|
"next": "^16.2.6",
|
|
10
10
|
"react": "^19.0.0",
|
|
11
11
|
"react-dom": "^19.0.0",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
"@base-ui/utils": ["@base-ui/utils@0.2.9", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw=="],
|
|
34
34
|
|
|
35
|
-
"@cimplify/sdk": ["@cimplify/sdk@0.
|
|
35
|
+
"@cimplify/sdk": ["@cimplify/sdk@0.54.0", "", { "dependencies": { "@base-ui/react": "^1.4.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "libphonenumber-js": "1.12.41", "react-day-picker": "^9.14.0", "tailwind-merge": "^3.5.0", "zod": "^4.4.3" }, "peerDependencies": { "@paystack/inline-js": "^2.22.8", "msw": ">=2.0.0", "react": ">=17.0.0", "vitest": ">=2.0.0" }, "optionalPeers": ["@paystack/inline-js", "msw", "react", "vitest"], "bin": { "cimplify-mock": "dist/mock/cli.mjs" } }, "sha512-YUN/lOqViHci1v9USWWwAWty29pEXBdLTuKarSl8EpafWh5jkdGZ1Q+Ne9/bfgTZmR8iTUltA8U7glV2FoRSgg=="],
|
|
36
36
|
|
|
37
37
|
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
|
|
38
38
|
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Canonical absolute URL. The platform overwrites this at build time with the
|
|
3
3
|
* storefront's primary domain; the default only applies to local builds. It's
|
|
4
4
|
* per-deploy config, not per-request, so it stays a static constant — keeping
|
|
5
|
-
* the root layout prerenderable
|
|
5
|
+
* the root layout prerenderable.
|
|
6
6
|
*/
|
|
7
7
|
export const SITE_URL = "https://example.com";
|
|
@@ -19,13 +19,14 @@ if (STOREFRONT_URL === "http://127.0.0.1:8787") {
|
|
|
19
19
|
);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Cache Components ('use cache' + cacheTag/cacheLife) require Node-specific
|
|
23
|
+
// setTimeout atomicity and serialize a postponed state that routinely exceeds
|
|
24
|
+
// CF Workers' 128MB zlib limit. We're on Cloudflare Workers via opennext, so
|
|
25
|
+
// we stay on Next 16's "Previous Model" — `fetch.next.{revalidate,tags}` via
|
|
26
|
+
// the SDK's `cacheOptions`, plus `export const revalidate` per page. See
|
|
27
|
+
// https://nextjs.org/docs/app/guides/caching-without-cache-components.
|
|
22
28
|
const nextConfig: NextConfig = {
|
|
23
|
-
// Enable Next 16's `cacheComponents` mode so we can use `'use cache'` +
|
|
24
|
-
// `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,
|
|
25
|
-
// dynamic Suspense boundaries fill in when ready.
|
|
26
|
-
cacheComponents: true,
|
|
27
29
|
async redirects() {
|
|
28
|
-
// Config-level so cacheComponents needn't prerender a redirect()-only page.
|
|
29
30
|
return [
|
|
30
31
|
{ source: "/login", destination: "/account", permanent: false },
|
|
31
32
|
{ source: "/signup", destination: "/account", permanent: false },
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cimplify-storefront
|
|
3
|
-
description: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts
|
|
3
|
+
description: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Cimplify Storefront skill
|
|
7
7
|
|
|
8
|
-
You're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file ↔ brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all
|
|
8
|
+
You're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file ↔ brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all eight.
|
|
9
9
|
|
|
10
10
|
## The contract — never break
|
|
11
11
|
|
|
12
12
|
1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface — don't hardcode it in a page or component.
|
|
13
13
|
2. **`app/globals.css` `@theme { … }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.
|
|
14
|
-
3. **
|
|
15
|
-
4.
|
|
16
|
-
5.
|
|
14
|
+
3. **Pages use ISR**, not Cache Components. Each page sets `export const revalidate = <seconds>` and reads from the SDK with `cacheOptions: { revalidate, tags }`. Don't add `'use cache'` / `cacheTag` / `cacheLife` — they require Node-specific runtime guarantees Cloudflare Workers doesn't provide, and their postponed state blows past CF's 128MB zlib limit.
|
|
15
|
+
4. **`cacheComponents` stays OFF** in `next.config.ts`. The deploy target is Cloudflare Workers via opennext.
|
|
16
|
+
5. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.
|
|
17
17
|
6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.
|
|
18
18
|
7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.
|
|
19
19
|
|
|
@@ -57,7 +57,7 @@ Don't touch any other file. If the rebrand needs content not in the schema, add
|
|
|
57
57
|
|
|
58
58
|
1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).
|
|
59
59
|
2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.
|
|
60
|
-
3. Wrap interactive bits in `<Suspense fallback={…}>` so
|
|
60
|
+
3. Wrap interactive bits in `<Suspense fallback={…}>` so static chrome streams first.
|
|
61
61
|
4. Compose into the page.
|
|
62
62
|
|
|
63
63
|
### Wire a Server Action that mutates data
|
|
@@ -72,24 +72,39 @@ export async function createProduct(input: ProductInput) {
|
|
|
72
72
|
}
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
After every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.
|
|
75
|
+
After every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`. These fire eviction events the central tag-cache worker picks up — it drops R2 entries and purges Cloudflare's edge.
|
|
76
76
|
|
|
77
|
-
### Add a Server Component data fetch
|
|
77
|
+
### Add a Server Component data fetch (ISR)
|
|
78
78
|
|
|
79
79
|
```ts
|
|
80
|
-
import { cacheTag, cacheLife } from "next/cache";
|
|
81
80
|
import { getServerClient, tags } from "@cimplify/sdk/server";
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
export const revalidate = 3600; // page-level baseline
|
|
83
|
+
|
|
84
|
+
async function getProducts() {
|
|
85
|
+
const r = await getServerClient().catalogue.getProducts({
|
|
86
|
+
limit: 24,
|
|
87
|
+
cacheOptions: { revalidate: 3600, tags: [tags.products()] },
|
|
88
|
+
});
|
|
89
|
+
if (!r.ok) {
|
|
90
|
+
// Soft-render on transient errors so the page degrades gracefully
|
|
91
|
+
// instead of hard-failing with React #419. Only call notFound() on
|
|
92
|
+
// genuine 404 from origin.
|
|
93
|
+
if (r.error.code === "NOT_FOUND") notFound();
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
89
96
|
return r.value.items;
|
|
90
97
|
}
|
|
91
98
|
```
|
|
92
99
|
|
|
100
|
+
For `[slug]` routes, add `generateStaticParams` with a placeholder fallback so the page is statically prerenderable even with no upstream data yet:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
export async function generateStaticParams() {
|
|
104
|
+
return [{ slug: "__placeholder__" }];
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
93
108
|
### Eject an SDK component for deeper customization
|
|
94
109
|
|
|
95
110
|
Try `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:
|
|
@@ -116,10 +131,11 @@ cimplify domains add my-store.com
|
|
|
116
131
|
## Pitfalls — explicit ❌ list
|
|
117
132
|
|
|
118
133
|
- ❌ Hardcoding any visible string in a page or component. Always `brand.X`.
|
|
119
|
-
- ❌
|
|
120
|
-
- ❌ Using `unstable_cache` — Next 16
|
|
134
|
+
- ❌ Adding `'use cache'`, `cacheTag`, `cacheLife`, or enabling `cacheComponents: true`. We deploy to Cloudflare Workers; postponed state exceeds the 128MB zlib limit. Use `cacheOptions` on SDK reads + `export const revalidate` per page.
|
|
135
|
+
- ❌ Using `unstable_cache` — removed in Next 16. Use SDK `cacheOptions` instead.
|
|
121
136
|
- ❌ Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component — loses per-request memoization.
|
|
122
137
|
- ❌ Mutating data without calling the matching `revalidate*` helper.
|
|
138
|
+
- ❌ Calling `notFound()` on transient SDK errors — causes React #419 mid-stream. Check the `Result.error.code === "NOT_FOUND"` first; otherwise soft-render a skeleton.
|
|
123
139
|
- ❌ Adding an `app/error.tsx` handler that calls `reset()` without logging — silently swallowed errors hide bugs.
|
|
124
140
|
- ❌ Running `bun test` and reporting failures. Use `bun run test:run` (vitest).
|
|
125
141
|
- ❌ Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) — they're real.
|
|
@@ -132,7 +148,7 @@ cimplify domains add my-store.com
|
|
|
132
148
|
| Architectural rules | `AGENTS.md` at project root + this skill |
|
|
133
149
|
| Running locally | `bun dev` (boots mock + Next together) |
|
|
134
150
|
| Switching mock seed | edit `dev:mock` in `package.json` |
|
|
135
|
-
| Full SDK reference | `
|
|
151
|
+
| Full SDK reference | `cimplify.dev/sdk/optimization`, `cimplify.dev/sdk/server` |
|
|
136
152
|
|
|
137
153
|
## What to do when the user asks something out-of-scope
|
|
138
154
|
|
|
@@ -17,6 +17,7 @@ That covers ~95% of what merchants ask for. For anything else, follow the `cimpl
|
|
|
17
17
|
## Don't do
|
|
18
18
|
|
|
19
19
|
- Hardcode strings in pages or components.
|
|
20
|
-
-
|
|
21
|
-
-
|
|
20
|
+
- Enable `cacheComponents: true` in `next.config.ts` — we're on Cloudflare Workers, where `'use cache'` postponed state blows past CF's 128MB zlib limit. This template stays on Next 16's "Previous Model" (ISR): `export const revalidate` per page + `cacheOptions: { revalidate, tags }` on SDK reads.
|
|
21
|
+
- Add `'use cache'`, `cacheTag`, or `cacheLife` anywhere. Use the SDK's `cacheOptions` instead.
|
|
22
|
+
- Use `unstable_cache` — it's gone in Next 16. Use SDK `cacheOptions` or `fetch.next.{revalidate,tags}`.
|
|
22
23
|
- Run `bun test` (Bun's `vi` shim is incomplete) — use `bun run test:run`.
|
|
@@ -2,9 +2,8 @@ import type { Metadata } from "next";
|
|
|
2
2
|
import { Suspense } from "react";
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { notFound } from "next/navigation";
|
|
5
|
-
import { cacheTag, cacheLife } from "next/cache";
|
|
6
5
|
import {
|
|
7
|
-
|
|
6
|
+
getServerClient,
|
|
8
7
|
tags,
|
|
9
8
|
type Category,
|
|
10
9
|
type Product,
|
|
@@ -12,26 +11,46 @@ import {
|
|
|
12
11
|
import { ListingClient } from "./listing-client";
|
|
13
12
|
import { brand } from "@/lib/brand";
|
|
14
13
|
|
|
14
|
+
// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.
|
|
15
|
+
export async function generateStaticParams() {
|
|
16
|
+
const r = await getServerClient().catalogue.getCategories();
|
|
17
|
+
if (!r.ok || r.value.length === 0) {
|
|
18
|
+
return [{ slug: "__placeholder__" }];
|
|
19
|
+
}
|
|
20
|
+
return r.value.map((c) => ({ slug: c.slug ?? c.id }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const revalidate = 3600;
|
|
24
|
+
|
|
15
25
|
interface CategoryData {
|
|
16
26
|
category: Category;
|
|
17
27
|
products: Product[];
|
|
18
28
|
}
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
cacheLife(CACHE_LIFE_DEFAULT);
|
|
30
|
+
type CategoryResult =
|
|
31
|
+
| { ok: true; data: CategoryData }
|
|
32
|
+
| { ok: false; code: string };
|
|
24
33
|
|
|
34
|
+
async function getCategory(slug: string): Promise<CategoryResult> {
|
|
25
35
|
const client = getServerClient();
|
|
26
|
-
const catRes = await client.catalogue.getCategoryBySlug(slug
|
|
27
|
-
|
|
36
|
+
const catRes = await client.catalogue.getCategoryBySlug(slug, {
|
|
37
|
+
cacheOptions: { revalidate: 3600, tags: [tags.categories()] },
|
|
38
|
+
});
|
|
39
|
+
if (!catRes.ok) return { ok: false, code: catRes.error.code };
|
|
28
40
|
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {
|
|
42
|
+
cacheOptions: {
|
|
43
|
+
revalidate: 3600,
|
|
44
|
+
tags: [
|
|
45
|
+
tags.category(catRes.value.id),
|
|
46
|
+
tags.categoryProducts(catRes.value.id),
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
31
50
|
const products = r.ok
|
|
32
51
|
? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))
|
|
33
52
|
: [];
|
|
34
|
-
return { category: catRes.value, products };
|
|
53
|
+
return { ok: true, data: { category: catRes.value, products } };
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
export async function generateMetadata({
|
|
@@ -40,8 +59,9 @@ export async function generateMetadata({
|
|
|
40
59
|
params: Promise<{ slug: string }>;
|
|
41
60
|
}): Promise<Metadata> {
|
|
42
61
|
const { slug } = await params;
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
62
|
+
const result = await getCategory(slug);
|
|
63
|
+
if (!result.ok) return {};
|
|
64
|
+
const data = result.data;
|
|
45
65
|
return {
|
|
46
66
|
title: `${data.category.name} — ${brand.name}`,
|
|
47
67
|
description: data.category.description ?? undefined,
|
|
@@ -66,10 +86,12 @@ async function CategoryContent({
|
|
|
66
86
|
params: Promise<{ slug: string }>;
|
|
67
87
|
}) {
|
|
68
88
|
const { slug } = await params;
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
const result = await getCategory(slug);
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
if (result.code === "NOT_FOUND") notFound();
|
|
92
|
+
return <CategorySkeleton />;
|
|
93
|
+
}
|
|
94
|
+
const { category, products } = result.data;
|
|
73
95
|
return (
|
|
74
96
|
<section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">
|
|
75
97
|
<header className="mb-8 text-center">
|
|
@@ -2,9 +2,8 @@ import type { Metadata } from "next";
|
|
|
2
2
|
import { Suspense } from "react";
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { notFound } from "next/navigation";
|
|
5
|
-
import { cacheTag, cacheLife } from "next/cache";
|
|
6
5
|
import {
|
|
7
|
-
|
|
6
|
+
getServerClient,
|
|
8
7
|
tags,
|
|
9
8
|
type Collection,
|
|
10
9
|
type Product,
|
|
@@ -12,26 +11,46 @@ import {
|
|
|
12
11
|
import { ListingClient } from "./listing-client";
|
|
13
12
|
import { brand } from "@/lib/brand";
|
|
14
13
|
|
|
14
|
+
// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.
|
|
15
|
+
export async function generateStaticParams() {
|
|
16
|
+
const r = await getServerClient().catalogue.getCollections();
|
|
17
|
+
if (!r.ok || r.value.length === 0) {
|
|
18
|
+
return [{ slug: "__placeholder__" }];
|
|
19
|
+
}
|
|
20
|
+
return r.value.map((c) => ({ slug: c.slug ?? c.id }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const revalidate = 3600;
|
|
24
|
+
|
|
15
25
|
interface CollectionData {
|
|
16
26
|
collection: Collection;
|
|
17
27
|
products: Product[];
|
|
18
28
|
}
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
cacheLife(CACHE_LIFE_DEFAULT);
|
|
30
|
+
type CollectionResult =
|
|
31
|
+
| { ok: true; data: CollectionData }
|
|
32
|
+
| { ok: false; code: string };
|
|
24
33
|
|
|
34
|
+
async function getCollection(slug: string): Promise<CollectionResult> {
|
|
25
35
|
const client = getServerClient();
|
|
26
|
-
const colRes = await client.catalogue.getCollectionBySlug(slug
|
|
27
|
-
|
|
36
|
+
const colRes = await client.catalogue.getCollectionBySlug(slug, {
|
|
37
|
+
cacheOptions: { revalidate: 3600, tags: [tags.collections()] },
|
|
38
|
+
});
|
|
39
|
+
if (!colRes.ok) return { ok: false, code: colRes.error.code };
|
|
28
40
|
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {
|
|
42
|
+
cacheOptions: {
|
|
43
|
+
revalidate: 3600,
|
|
44
|
+
tags: [
|
|
45
|
+
tags.collection(colRes.value.id),
|
|
46
|
+
tags.collectionProducts(colRes.value.id),
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
31
50
|
const products = r.ok
|
|
32
51
|
? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))
|
|
33
52
|
: [];
|
|
34
|
-
return { collection: colRes.value, products };
|
|
53
|
+
return { ok: true, data: { collection: colRes.value, products } };
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
export async function generateMetadata({
|
|
@@ -40,8 +59,9 @@ export async function generateMetadata({
|
|
|
40
59
|
params: Promise<{ slug: string }>;
|
|
41
60
|
}): Promise<Metadata> {
|
|
42
61
|
const { slug } = await params;
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
62
|
+
const result = await getCollection(slug);
|
|
63
|
+
if (!result.ok) return {};
|
|
64
|
+
const data = result.data;
|
|
45
65
|
return {
|
|
46
66
|
title: `${data.collection.name} — ${brand.name}`,
|
|
47
67
|
description: data.collection.description ?? undefined,
|
|
@@ -66,10 +86,12 @@ async function CollectionContent({
|
|
|
66
86
|
params: Promise<{ slug: string }>;
|
|
67
87
|
}) {
|
|
68
88
|
const { slug } = await params;
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
const result = await getCollection(slug);
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
if (result.code === "NOT_FOUND") notFound();
|
|
92
|
+
return <CollectionSkeleton />;
|
|
93
|
+
}
|
|
94
|
+
const { collection, products } = result.data;
|
|
73
95
|
return (
|
|
74
96
|
<section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">
|
|
75
97
|
<header className="mb-8 text-center">
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CACHE_LIFE_DEFAULT, getServerClient, tags, type Product } from "@cimplify/sdk/server";
|
|
1
|
+
import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
|
|
3
2
|
import { brand } from "@/lib/brand";
|
|
4
3
|
import { getSiteUrl } from "@/lib/site-url";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
"use cache";
|
|
8
|
-
cacheTag(tags.products(), tags.categories(), tags.collections());
|
|
9
|
-
cacheLife(CACHE_LIFE_DEFAULT);
|
|
5
|
+
export const revalidate = 3600;
|
|
10
6
|
|
|
7
|
+
async function buildLlmsTxt(SITE_URL: string): Promise<string> {
|
|
11
8
|
const client = getServerClient();
|
|
12
9
|
const [productsRes, categoriesRes, collectionsRes] = await Promise.all([
|
|
13
|
-
client.catalogue.getProducts(
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
client.catalogue.getProducts(
|
|
11
|
+
{ limit: 500 },
|
|
12
|
+
{ cacheOptions: { revalidate: 3600, tags: [tags.products()] } },
|
|
13
|
+
),
|
|
14
|
+
client.catalogue.getCategories({
|
|
15
|
+
cacheOptions: { revalidate: 3600, tags: [tags.categories()] },
|
|
16
|
+
}),
|
|
17
|
+
client.catalogue.getCollections({
|
|
18
|
+
cacheOptions: { revalidate: 3600, tags: [tags.collections()] },
|
|
19
|
+
}),
|
|
16
20
|
]);
|
|
17
21
|
|
|
18
22
|
const products: Product[] = productsRes.ok ? productsRes.value.items : [];
|