@cimplify/cli 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/{add-IJELMSCY.mjs → add-3DANJHCI.mjs} +1 -1
  2. package/dist/{chunk-O7SMBTHQ.mjs → chunk-2547SIWD.mjs} +1 -1
  3. package/dist/{chunk-U3DWSQYY.mjs → chunk-BB7IBX6X.mjs} +1 -1
  4. package/dist/{chunk-DENWIVVB.mjs → chunk-ZEGSQVM5.mjs} +23 -23
  5. package/dist/dispatcher.mjs +9 -9
  6. package/dist/{doctor-NUKNW4S3.mjs → doctor-TD4RZCCM.mjs} +2 -2
  7. package/dist/{explain-TGRXJW3V.mjs → explain-7OSAQVJ6.mjs} +1 -1
  8. package/dist/{introspect-5W72YWFT.mjs → introspect-SKQMAYFJ.mjs} +2 -2
  9. package/dist/{list-VO4LKRDN.mjs → list-XLJOS2DK.mjs} +1 -1
  10. package/dist/{update-V7XIKMFU.mjs → update-4GX2J3IB.mjs} +1 -1
  11. package/package.json +1 -1
  12. package/templates/manifest.json +2 -2
  13. package/templates/storefront-auto/.env.example +9 -18
  14. package/templates/storefront-auto/README.md +1 -3
  15. package/templates/storefront-auto/app/.well-known/ucp/route.ts +42 -20
  16. package/templates/storefront-auto/app/layout.tsx +31 -26
  17. package/templates/storefront-auto/app/llms.txt/route.ts +3 -5
  18. package/templates/storefront-auto/app/opensearch.xml/route.ts +5 -6
  19. package/templates/storefront-auto/app/products/[slug]/page.tsx +5 -5
  20. package/templates/storefront-auto/app/robots.ts +5 -6
  21. package/templates/storefront-auto/app/sitemap.ts +6 -7
  22. package/templates/storefront-auto/components/providers.tsx +3 -12
  23. package/templates/storefront-auto/lib/site-url.ts +29 -0
  24. package/templates/storefront-auto/next.config.ts +10 -16
  25. package/templates/storefront-bakery/.env.example +9 -18
  26. package/templates/storefront-bakery/README.md +1 -3
  27. package/templates/storefront-bakery/app/.well-known/ucp/route.ts +42 -20
  28. package/templates/storefront-bakery/app/layout.tsx +40 -35
  29. package/templates/storefront-bakery/app/llms.txt/route.ts +3 -5
  30. package/templates/storefront-bakery/app/opensearch.xml/route.ts +5 -6
  31. package/templates/storefront-bakery/app/robots.ts +5 -6
  32. package/templates/storefront-bakery/app/sitemap.ts +6 -7
  33. package/templates/storefront-bakery/components/providers.tsx +3 -12
  34. package/templates/storefront-bakery/lib/site-url.ts +29 -0
  35. package/templates/storefront-bakery/next.config.ts +10 -16
  36. package/templates/storefront-fashion/.env.example +9 -18
  37. package/templates/storefront-fashion/README.md +2 -4
  38. package/templates/storefront-fashion/app/.well-known/ucp/route.ts +42 -20
  39. package/templates/storefront-fashion/app/layout.tsx +31 -26
  40. package/templates/storefront-fashion/app/llms.txt/route.ts +3 -5
  41. package/templates/storefront-fashion/app/opensearch.xml/route.ts +5 -6
  42. package/templates/storefront-fashion/app/products/[slug]/page.tsx +5 -5
  43. package/templates/storefront-fashion/app/robots.ts +5 -6
  44. package/templates/storefront-fashion/app/sitemap.ts +6 -7
  45. package/templates/storefront-fashion/components/providers.tsx +3 -12
  46. package/templates/storefront-fashion/lib/site-url.ts +29 -0
  47. package/templates/storefront-fashion/next.config.ts +10 -16
  48. package/templates/storefront-grocery/.env.example +9 -18
  49. package/templates/storefront-grocery/README.md +2 -4
  50. package/templates/storefront-grocery/app/.well-known/ucp/route.ts +42 -20
  51. package/templates/storefront-grocery/app/layout.tsx +31 -26
  52. package/templates/storefront-grocery/app/llms.txt/route.ts +3 -5
  53. package/templates/storefront-grocery/app/opensearch.xml/route.ts +5 -6
  54. package/templates/storefront-grocery/app/robots.ts +5 -6
  55. package/templates/storefront-grocery/app/sitemap.ts +6 -7
  56. package/templates/storefront-grocery/components/providers.tsx +3 -12
  57. package/templates/storefront-grocery/lib/site-url.ts +29 -0
  58. package/templates/storefront-grocery/next.config.ts +10 -16
  59. package/templates/storefront-pharmacy/.env.example +9 -18
  60. package/templates/storefront-pharmacy/README.md +1 -3
  61. package/templates/storefront-pharmacy/app/.well-known/ucp/route.ts +42 -20
  62. package/templates/storefront-pharmacy/app/layout.tsx +31 -26
  63. package/templates/storefront-pharmacy/app/llms.txt/route.ts +3 -5
  64. package/templates/storefront-pharmacy/app/opensearch.xml/route.ts +5 -6
  65. package/templates/storefront-pharmacy/app/products/[slug]/page.tsx +5 -5
  66. package/templates/storefront-pharmacy/app/robots.ts +5 -6
  67. package/templates/storefront-pharmacy/app/sitemap.ts +6 -7
  68. package/templates/storefront-pharmacy/components/providers.tsx +3 -12
  69. package/templates/storefront-pharmacy/lib/site-url.ts +29 -0
  70. package/templates/storefront-pharmacy/next.config.ts +10 -16
  71. package/templates/storefront-restaurant/.env.example +9 -18
  72. package/templates/storefront-restaurant/README.md +2 -4
  73. package/templates/storefront-restaurant/app/.well-known/ucp/route.ts +42 -20
  74. package/templates/storefront-restaurant/app/layout.tsx +31 -26
  75. package/templates/storefront-restaurant/app/llms.txt/route.ts +3 -5
  76. package/templates/storefront-restaurant/app/opensearch.xml/route.ts +5 -6
  77. package/templates/storefront-restaurant/app/robots.ts +5 -6
  78. package/templates/storefront-restaurant/app/sitemap.ts +6 -7
  79. package/templates/storefront-restaurant/components/providers.tsx +3 -12
  80. package/templates/storefront-restaurant/lib/site-url.ts +29 -0
  81. package/templates/storefront-restaurant/next.config.ts +10 -16
  82. package/templates/storefront-retail/.env.example +9 -18
  83. package/templates/storefront-retail/README.md +1 -3
  84. package/templates/storefront-retail/app/.well-known/ucp/route.ts +42 -20
  85. package/templates/storefront-retail/app/layout.tsx +31 -26
  86. package/templates/storefront-retail/app/llms.txt/route.ts +3 -5
  87. package/templates/storefront-retail/app/opensearch.xml/route.ts +5 -6
  88. package/templates/storefront-retail/app/products/[slug]/page.tsx +5 -5
  89. package/templates/storefront-retail/app/robots.ts +5 -6
  90. package/templates/storefront-retail/app/sitemap.ts +6 -7
  91. package/templates/storefront-retail/components/providers.tsx +3 -12
  92. package/templates/storefront-retail/lib/site-url.ts +29 -0
  93. package/templates/storefront-retail/next.config.ts +10 -16
  94. package/templates/storefront-services/.env.example +9 -18
  95. package/templates/storefront-services/README.md +2 -4
  96. package/templates/storefront-services/app/.well-known/ucp/route.ts +42 -20
  97. package/templates/storefront-services/app/layout.tsx +31 -26
  98. package/templates/storefront-services/app/llms.txt/route.ts +3 -5
  99. package/templates/storefront-services/app/opensearch.xml/route.ts +5 -6
  100. package/templates/storefront-services/app/robots.ts +5 -6
  101. package/templates/storefront-services/app/sitemap.ts +6 -7
  102. package/templates/storefront-services/components/providers.tsx +3 -12
  103. package/templates/storefront-services/lib/site-url.ts +29 -0
  104. package/templates/storefront-services/next.config.ts +10 -16
@@ -5,48 +5,70 @@ import { NextResponse } from "next/server";
5
5
  *
6
6
  * Agents — Claude, ChatGPT, Gemini, MCP clients — probe
7
7
  * `https://<your-domain>/.well-known/ucp` to learn what commerce
8
- * capabilities your storefront supports. We forward the request to
9
- * Cimplify, which returns the canonical manifest; the response body
10
- * tells agents to make subsequent UCP calls directly to
11
- * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).
12
- *
13
- * Edge-cached for an hour because capabilities change rarely.
8
+ * capabilities your storefront supports. We resolve the business's
9
+ * UCP handle from the public key (via the storefront API), then forward
10
+ * to Cimplify for the canonical manifest. Edge-cached for an hour
11
+ * because capabilities change rarely.
14
12
  */
15
- const UCP_API_BASE =
16
- process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";
17
-
13
+ const UCP_API_BASE = "https://api.cimplify.io";
14
+ const STOREFRONT_API_BASE =
15
+ process.env.NODE_ENV === "production"
16
+ ? "https://storefronts.cimplify.io"
17
+ : "http://127.0.0.1:8787";
18
18
 
19
19
  export async function GET() {
20
- const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;
21
-
22
- if (!businessHandle) {
20
+ const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;
21
+ if (!publicKey) {
23
22
  return NextResponse.json(
24
23
  {
25
- error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",
24
+ error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",
26
25
  remediation:
27
- "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",
26
+ "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",
28
27
  },
29
28
  { status: 500 },
30
29
  );
31
30
  }
32
31
 
33
32
  try {
34
- const response = await fetch(
35
- `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,
33
+ // Step 1 resolve the business handle from the public key.
34
+ const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {
35
+ headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },
36
+ next: { revalidate: 3600 },
37
+ });
38
+
39
+ if (!businessResp.ok) {
40
+ return NextResponse.json(
41
+ { error: `Failed to resolve business: ${businessResp.status}` },
42
+ { status: businessResp.status },
43
+ );
44
+ }
45
+
46
+ const businessJson = await businessResp.json();
47
+ const handle: string | undefined = businessJson?.data?.handle;
48
+ if (!handle) {
49
+ return NextResponse.json(
50
+ { error: "Business has no handle configured" },
51
+ { status: 500 },
52
+ );
53
+ }
54
+
55
+ // Step 2 — fetch the canonical UCP manifest.
56
+ const manifestResp = await fetch(
57
+ `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,
36
58
  {
37
59
  headers: { "Content-Type": "application/json" },
38
60
  next: { revalidate: 3600 },
39
61
  },
40
62
  );
41
63
 
42
- if (!response.ok) {
64
+ if (!manifestResp.ok) {
43
65
  return NextResponse.json(
44
- { error: `Upstream UCP manifest fetch failed: ${response.status}` },
45
- { status: response.status },
66
+ { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },
67
+ { status: manifestResp.status },
46
68
  );
47
69
  }
48
70
 
49
- const manifest = await response.json();
71
+ const manifest = await manifestResp.json();
50
72
  return NextResponse.json(manifest, {
51
73
  headers: {
52
74
  "Content-Type": "application/json",
@@ -8,6 +8,7 @@ import { ProductModal } from "@/components/product-modal";
8
8
  import { CartDrawer } from "@/components/cart-drawer";
9
9
  import { Suspense } from "react";
10
10
  import { brand } from "@/lib/brand";
11
+ import { getSiteUrl } from "@/lib/site-url";
11
12
 
12
13
  const inter = Inter({
13
14
  subsets: ["latin"],
@@ -21,42 +22,46 @@ const playfair = Playfair_Display({
21
22
  display: "swap",
22
23
  });
23
24
 
24
- const SITE_URL =
25
- process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
26
-
27
- export const metadata: Metadata = {
28
- metadataBase: new URL(SITE_URL),
29
- title: {
30
- default: brand.name,
31
- template: `%s — ${brand.name}`,
32
- },
33
- description: brand.description,
34
- openGraph: {
35
- type: "website",
36
- siteName: brand.name,
37
- locale: brand.locale,
38
- },
39
- twitter: { card: "summary_large_image" },
40
- };
25
+ export async function generateMetadata(): Promise<Metadata> {
26
+ const siteUrl = await getSiteUrl();
27
+ return {
28
+ metadataBase: new URL(siteUrl),
29
+ title: {
30
+ default: brand.name,
31
+ template: `%s — ${brand.name}`,
32
+ },
33
+ description: brand.description,
34
+ openGraph: {
35
+ type: "website",
36
+ siteName: brand.name,
37
+ locale: brand.locale,
38
+ },
39
+ twitter: { card: "summary_large_image" },
40
+ };
41
+ }
41
42
 
42
- const ORGANIZATION_LD = {
43
- "@context": "https://schema.org",
44
- "@type": brand.schemaType,
45
- name: brand.name,
46
- url: SITE_URL,
47
- description: brand.description,
48
- email: brand.contact.email,
49
- telephone: brand.contact.phoneTel,
50
- address: {
51
- "@type": "PostalAddress",
52
- streetAddress: brand.contact.streetAddress,
53
- addressLocality: brand.contact.city,
54
- addressCountry: brand.contact.countryCode,
55
- },
56
- sameAs: brand.socials.map((s) => s.href),
57
- };
43
+ async function organizationLd() {
44
+ const siteUrl = await getSiteUrl();
45
+ return {
46
+ "@context": "https://schema.org",
47
+ "@type": brand.schemaType,
48
+ name: brand.name,
49
+ url: siteUrl,
50
+ description: brand.description,
51
+ email: brand.contact.email,
52
+ telephone: brand.contact.phoneTel,
53
+ address: {
54
+ "@type": "PostalAddress",
55
+ streetAddress: brand.contact.streetAddress,
56
+ addressLocality: brand.contact.city,
57
+ addressCountry: brand.contact.countryCode,
58
+ },
59
+ sameAs: brand.socials.map((s) => s.href),
60
+ };
61
+ }
58
62
 
59
- export default function RootLayout({ children }: { children: React.ReactNode }) {
63
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
64
+ const ld = await organizationLd();
60
65
  return (
61
66
  <html lang="en" suppressHydrationWarning className={`${inter.variable} ${playfair.variable}`}>
62
67
  <body
@@ -65,7 +70,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
65
70
  >
66
71
  <script
67
72
  type="application/ld+json"
68
- dangerouslySetInnerHTML={{ __html: JSON.stringify(ORGANIZATION_LD) }}
73
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}
69
74
  />
70
75
  <Providers>
71
76
  <Header />
@@ -1,11 +1,9 @@
1
1
  import { cacheTag, cacheLife } from "next/cache";
2
2
  import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
3
3
  import { brand } from "@/lib/brand";
4
+ import { getSiteUrl } from "@/lib/site-url";
4
5
 
5
- const SITE_URL =
6
- process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
7
-
8
- async function buildLlmsTxt(): Promise<string> {
6
+ async function buildLlmsTxt(SITE_URL: string): Promise<string> {
9
7
  "use cache";
10
8
  cacheTag(tags.products(), tags.categories(), tags.collections());
11
9
  cacheLife("hours");
@@ -84,7 +82,7 @@ async function buildLlmsTxt(): Promise<string> {
84
82
  * into context windows.
85
83
  */
86
84
  export async function GET(): Promise<Response> {
87
- const body = await buildLlmsTxt();
85
+ const body = await buildLlmsTxt(await getSiteUrl());
88
86
  return new Response(body, {
89
87
  headers: {
90
88
  "Content-Type": "text/plain; charset=utf-8",
@@ -1,7 +1,5 @@
1
1
  import { brand } from "@/lib/brand";
2
-
3
- const SITE_URL =
4
- process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
2
+ import { getSiteUrl } from "@/lib/site-url";
5
3
 
6
4
  /**
7
5
  * OpenSearch description document — lets browsers add this site to the
@@ -9,14 +7,15 @@ const SITE_URL =
9
7
  * domain, they get an inline search box that hits /search?q=...
10
8
  */
11
9
  export async function GET(): Promise<Response> {
10
+ const siteUrl = await getSiteUrl();
12
11
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
13
12
  <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
14
13
  <ShortName>${escapeXml(brand.shortName)}</ShortName>
15
14
  <Description>Search ${escapeXml(brand.name)}</Description>
16
15
  <InputEncoding>UTF-8</InputEncoding>
17
- <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />
18
- <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />
19
- <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>
16
+ <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />
17
+ <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />
18
+ <moz:SearchForm>${siteUrl}/search</moz:SearchForm>
20
19
  </OpenSearchDescription>
21
20
  `;
22
21
  return new Response(xml, {
@@ -1,9 +1,8 @@
1
1
  import type { MetadataRoute } from "next";
2
+ import { getSiteUrl } from "@/lib/site-url";
2
3
 
3
- const SITE_URL =
4
- process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
5
-
6
- export default function robots(): MetadataRoute.Robots {
4
+ export default async function robots(): Promise<MetadataRoute.Robots> {
5
+ const siteUrl = await getSiteUrl();
7
6
  return {
8
7
  rules: [
9
8
  {
@@ -12,7 +11,7 @@ export default function robots(): MetadataRoute.Robots {
12
11
  disallow: ["/cart", "/checkout", "/orders/", "/api/"],
13
12
  },
14
13
  ],
15
- sitemap: `${SITE_URL}/sitemap.xml`,
16
- host: SITE_URL,
14
+ sitemap: `${siteUrl}/sitemap.xml`,
15
+ host: siteUrl,
17
16
  };
18
17
  }
@@ -1,8 +1,6 @@
1
1
  import type { MetadataRoute } from "next";
2
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";
3
+ import { getSiteUrl } from "@/lib/site-url";
6
4
 
7
5
  const STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [
8
6
  { path: "/", priority: 1.0, changeFrequency: "daily" },
@@ -15,6 +13,7 @@ const STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily"
15
13
 
16
14
  export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
17
15
  const now = new Date();
16
+ const siteUrl = await getSiteUrl();
18
17
  const client = getServerClient();
19
18
 
20
19
  const [productsRes, categoriesRes, collectionsRes] = await Promise.all([
@@ -28,7 +27,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
28
27
  const collections = collectionsRes.ok ? collectionsRes.value : [];
29
28
 
30
29
  const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({
31
- url: `${SITE_URL}${r.path}`,
30
+ url: `${siteUrl}${r.path}`,
32
31
  lastModified: now,
33
32
  changeFrequency: r.changeFrequency,
34
33
  priority: r.priority,
@@ -38,21 +37,21 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
38
37
  // shop pages). Emit those as deep-linkable URLs so search engines and LLMs
39
38
  // can index each product canonically.
40
39
  const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({
41
- url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,
40
+ url: `${siteUrl}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,
42
41
  lastModified: p.updated_at ? new Date(p.updated_at) : now,
43
42
  changeFrequency: "weekly",
44
43
  priority: 0.7,
45
44
  }));
46
45
 
47
46
  const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({
48
- url: `${SITE_URL}/categories/${c.slug}`,
47
+ url: `${siteUrl}/categories/${c.slug}`,
49
48
  lastModified: now,
50
49
  changeFrequency: "weekly",
51
50
  priority: 0.6,
52
51
  }));
53
52
 
54
53
  const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({
55
- url: `${SITE_URL}/collections/${c.slug}`,
54
+ url: `${siteUrl}/collections/${c.slug}`,
56
55
  lastModified: now,
57
56
  changeFrequency: "weekly",
58
57
  priority: 0.6,
@@ -4,21 +4,12 @@ import { useMemo, type ReactNode } from "react";
4
4
  import { createCimplifyClient } from "@cimplify/sdk";
5
5
  import { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";
6
6
 
7
- /**
8
- * Boots the Cimplify SDK client once on the client-side and exposes it via
9
- * <CimplifyProvider/>.
10
- *
11
- * Base-URL resolution:
12
- * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.
13
- * 2) Otherwise use the current origin so requests flow through the
14
- * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).
15
- * 3) Fall back to 127.0.0.1:8787 only during SSR, when there's no window.
16
- */
7
+ // Same-origin client — every request goes through the Next.js rewrite in
8
+ // next.config.ts, so no CORS preflight ever hits the browser.
17
9
  export function Providers({ children }: { children: ReactNode }) {
18
10
  const client = useMemo(() => {
19
11
  const baseUrl =
20
- process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||
21
- (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");
12
+ typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787";
22
13
  const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";
23
14
  return createCimplifyClient({
24
15
  baseUrl,
@@ -0,0 +1,29 @@
1
+ import { headers } from "next/headers";
2
+
3
+ /**
4
+ * Canonical absolute URL for this storefront, derived at request time.
5
+ *
6
+ * Resolution order:
7
+ * 1. `NEXT_PUBLIC_SITE_URL` — set when you need a fixed canonical
8
+ * (multiple domains, www-vs-apex preference, etc.).
9
+ * 2. The request `Host` header — works automatically on every deploy,
10
+ * including preview URLs and custom domains.
11
+ * 3. `https://example.com` — only ever returned during prerender of
12
+ * a page that has no live request (rare in App Router).
13
+ */
14
+ export async function getSiteUrl(): Promise<string> {
15
+ const explicit = process.env.NEXT_PUBLIC_SITE_URL?.trim();
16
+ if (explicit) return explicit;
17
+
18
+ try {
19
+ const h = await headers();
20
+ const host = h.get("host");
21
+ if (host) {
22
+ const isLocal = host.startsWith("localhost") || host.startsWith("127.");
23
+ return `${isLocal ? "http" : "https"}://${host}`;
24
+ }
25
+ } catch {
26
+ // `headers()` is unavailable in some build contexts — fall through.
27
+ }
28
+ return "https://example.com";
29
+ }
@@ -1,18 +1,12 @@
1
1
  import type { NextConfig } from "next";
2
2
 
3
- const MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";
3
+ // Same-origin proxy target for the storefront API. Local mock in dev,
4
+ // hosted Cimplify in prod.
5
+ const STOREFRONT_URL =
6
+ process.env.NODE_ENV === "production"
7
+ ? "https://storefronts.cimplify.io"
8
+ : "http://127.0.0.1:8787";
4
9
 
5
- /**
6
- * Same-origin proxy to the Cimplify mock so the browser never makes a
7
- * cross-origin request in dev (no CORS preflights, no flaky `Origin`
8
- * mismatches). The SDK's base URL stays empty/relative — requests go to
9
- * `/api/v1/...` on the Next origin, then this rewrite forwards them to
10
- * the mock at `127.0.0.1:8787`.
11
- *
12
- * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify
13
- * host and the rewrite continues to work the same way (or remove it and
14
- * pass the absolute URL through `<CimplifyProvider/>`).
15
- */
16
10
  const nextConfig: NextConfig = {
17
11
  // Enable Next 16's `cacheComponents` mode so we can use `'use cache'` +
18
12
  // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,
@@ -20,10 +14,10 @@ const nextConfig: NextConfig = {
20
14
  cacheComponents: true,
21
15
  async rewrites() {
22
16
  return [
23
- { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },
24
- { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },
25
- { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },
26
- { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },
17
+ { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },
18
+ { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },
19
+ { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },
20
+ { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },
27
21
  ];
28
22
  },
29
23
  images: {
@@ -1,22 +1,13 @@
1
- # Cimplify API base URL.
2
- # Dev: leave empty so the SDK uses the current origin (localhost:3000),
3
- # which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).
4
- # Production: set to your Cimplify host (e.g. https://api.cimplify.io).
5
- NEXT_PUBLIC_CIMPLIFY_API_URL=
6
-
7
- # Tenant public key. The mock accepts any value.
1
+ # Your tenant public key. Get it from the desk's Developers tab in
2
+ # production; the mock accepts any value during local dev.
8
3
  NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev
9
4
 
10
- # Business id used by the mock seed (fashion: Studio FRX).
5
+ # Mock seed for local dev picks which fixture business to serve. The
6
+ # scaffold sets this to the right one for your template, so you only
7
+ # need to change it when previewing other industries against the mock.
11
8
  NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_studio_frx
12
9
 
13
- # Canonical public site URL used by sitemap.xml, robots.txt, llms.txt,
14
- # and OpenGraph metadata. Set this on production deploys; `cimplify env push`
15
- # wires it into your linked project.
16
- NEXT_PUBLIC_SITE_URL=https://example.com
17
-
18
- # Business handle (human-readable slug, e.g. "akua-bakery"). Used by the
19
- # UCP manifest endpoint at /.well-known/ucp so AI agents (Claude / ChatGPT /
20
- # Gemini) can discover this storefront's commerce capabilities. Set this
21
- # on production deploys; leave empty in dev unless you're testing UCP.
22
- NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE=
10
+ # Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,
11
+ # OpenGraph, and llms.txt. Leave unset and the storefront derives the
12
+ # canonical from the request `Host` header automatically.
13
+ # NEXT_PUBLIC_SITE_URL=
@@ -66,12 +66,10 @@ cimplify init my-store --template grocery # coming soon
66
66
 
67
67
  ```diff
68
68
  # .env.local
69
- - NEXT_PUBLIC_CIMPLIFY_API_URL=http://127.0.0.1:8787
70
- + NEXT_PUBLIC_CIMPLIFY_API_URL=https://api.cimplify.io
71
69
  - NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev
72
70
  + NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>
73
- - NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_currents_electronics
71
+ - NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_studio_frx
74
72
  + NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>
75
73
  ```
76
74
 
77
- Deploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://docs.cimplify.io/cli/deploy). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.
75
+ Deploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.
@@ -5,48 +5,70 @@ import { NextResponse } from "next/server";
5
5
  *
6
6
  * Agents — Claude, ChatGPT, Gemini, MCP clients — probe
7
7
  * `https://<your-domain>/.well-known/ucp` to learn what commerce
8
- * capabilities your storefront supports. We forward the request to
9
- * Cimplify, which returns the canonical manifest; the response body
10
- * tells agents to make subsequent UCP calls directly to
11
- * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).
12
- *
13
- * Edge-cached for an hour because capabilities change rarely.
8
+ * capabilities your storefront supports. We resolve the business's
9
+ * UCP handle from the public key (via the storefront API), then forward
10
+ * to Cimplify for the canonical manifest. Edge-cached for an hour
11
+ * because capabilities change rarely.
14
12
  */
15
- const UCP_API_BASE =
16
- process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";
17
-
13
+ const UCP_API_BASE = "https://api.cimplify.io";
14
+ const STOREFRONT_API_BASE =
15
+ process.env.NODE_ENV === "production"
16
+ ? "https://storefronts.cimplify.io"
17
+ : "http://127.0.0.1:8787";
18
18
 
19
19
  export async function GET() {
20
- const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;
21
-
22
- if (!businessHandle) {
20
+ const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;
21
+ if (!publicKey) {
23
22
  return NextResponse.json(
24
23
  {
25
- error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",
24
+ error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",
26
25
  remediation:
27
- "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",
26
+ "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",
28
27
  },
29
28
  { status: 500 },
30
29
  );
31
30
  }
32
31
 
33
32
  try {
34
- const response = await fetch(
35
- `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,
33
+ // Step 1 resolve the business handle from the public key.
34
+ const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {
35
+ headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },
36
+ next: { revalidate: 3600 },
37
+ });
38
+
39
+ if (!businessResp.ok) {
40
+ return NextResponse.json(
41
+ { error: `Failed to resolve business: ${businessResp.status}` },
42
+ { status: businessResp.status },
43
+ );
44
+ }
45
+
46
+ const businessJson = await businessResp.json();
47
+ const handle: string | undefined = businessJson?.data?.handle;
48
+ if (!handle) {
49
+ return NextResponse.json(
50
+ { error: "Business has no handle configured" },
51
+ { status: 500 },
52
+ );
53
+ }
54
+
55
+ // Step 2 — fetch the canonical UCP manifest.
56
+ const manifestResp = await fetch(
57
+ `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,
36
58
  {
37
59
  headers: { "Content-Type": "application/json" },
38
60
  next: { revalidate: 3600 },
39
61
  },
40
62
  );
41
63
 
42
- if (!response.ok) {
64
+ if (!manifestResp.ok) {
43
65
  return NextResponse.json(
44
- { error: `Upstream UCP manifest fetch failed: ${response.status}` },
45
- { status: response.status },
66
+ { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },
67
+ { status: manifestResp.status },
46
68
  );
47
69
  }
48
70
 
49
- const manifest = await response.json();
71
+ const manifest = await manifestResp.json();
50
72
  return NextResponse.json(manifest, {
51
73
  headers: {
52
74
  "Content-Type": "application/json",
@@ -6,6 +6,7 @@ import { Header } from "@/components/header";
6
6
  import { Footer } from "@/components/footer";
7
7
  import { CartDrawer } from "@/components/cart-drawer";
8
8
  import { brand } from "@/lib/brand";
9
+ import { getSiteUrl } from "@/lib/site-url";
9
10
 
10
11
  const inter = Inter({
11
12
  subsets: ["latin"],
@@ -20,42 +21,46 @@ const anton = Anton({
20
21
  display: "swap",
21
22
  });
22
23
 
23
- const SITE_URL =
24
- process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
25
-
26
- export const metadata: Metadata = {
27
- metadataBase: new URL(SITE_URL),
28
- title: {
24
+ export async function generateMetadata(): Promise<Metadata> {
25
+ const siteUrl = await getSiteUrl();
26
+ return {
27
+ metadataBase: new URL(siteUrl),
28
+ title: {
29
29
  default: brand.name,
30
30
  template: `%s — ${brand.name}`,
31
- },
32
- description: brand.description,
33
- openGraph: {
31
+ },
32
+ description: brand.description,
33
+ openGraph: {
34
34
  type: "website",
35
35
  siteName: brand.name,
36
36
  locale: brand.locale,
37
- },
38
- twitter: { card: "summary_large_image" },
39
- };
37
+ },
38
+ twitter: { card: "summary_large_image" },
39
+ };
40
+ }
40
41
 
41
- const ORGANIZATION_LD = {
42
- "@context": "https://schema.org",
43
- "@type": brand.schemaType,
44
- name: brand.name,
45
- url: SITE_URL,
46
- description: brand.description,
47
- email: brand.contact.email,
48
- telephone: brand.contact.phoneTel,
49
- address: {
42
+ async function organizationLd() {
43
+ const siteUrl = await getSiteUrl();
44
+ return {
45
+ "@context": "https://schema.org",
46
+ "@type": brand.schemaType,
47
+ name: brand.name,
48
+ url: siteUrl,
49
+ description: brand.description,
50
+ email: brand.contact.email,
51
+ telephone: brand.contact.phoneTel,
52
+ address: {
50
53
  "@type": "PostalAddress",
51
54
  streetAddress: brand.contact.streetAddress,
52
55
  addressLocality: brand.contact.city,
53
56
  addressCountry: brand.contact.countryCode,
54
- },
55
- sameAs: brand.socials.map((s) => s.href),
56
- };
57
+ },
58
+ sameAs: brand.socials.map((s) => s.href),
59
+ };
60
+ }
57
61
 
58
- export default function RootLayout({ children }: { children: React.ReactNode }) {
62
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
63
+ const ld = await organizationLd();
59
64
  return (
60
65
  <html lang="en" suppressHydrationWarning className={`${inter.variable} ${anton.variable}`}>
61
66
  <body
@@ -64,7 +69,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
64
69
  >
65
70
  <script
66
71
  type="application/ld+json"
67
- dangerouslySetInnerHTML={{ __html: JSON.stringify(ORGANIZATION_LD) }}
72
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}
68
73
  />
69
74
  <Providers>
70
75
  <Header />