@cimplify/cli 0.5.2 → 0.5.4

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 (103) hide show
  1. package/dist/{add-BEJH4T3P.mjs → add-FU46PRCJ.mjs} +1 -1
  2. package/dist/{chunk-DZPR5L6H.mjs → chunk-72IHRRLU.mjs} +1 -1
  3. package/dist/{chunk-MUG454RK.mjs → chunk-A6BBQWTB.mjs} +1 -1
  4. package/dist/{chunk-TR65XHUE.mjs → chunk-OPWMPCPM.mjs} +24 -24
  5. package/dist/dispatcher.mjs +9 -9
  6. package/dist/{doctor-QO2SRNH6.mjs → doctor-FZCJWAPT.mjs} +2 -2
  7. package/dist/{explain-CGWT7HMV.mjs → explain-4I6JLKDW.mjs} +1 -1
  8. package/dist/{introspect-WDGC7BKQ.mjs → introspect-5ODUL6UC.mjs} +2 -2
  9. package/dist/{list-NO5SPHSU.mjs → list-EKOWQ53I.mjs} +1 -1
  10. package/dist/{update-EEQ7JJTP.mjs → update-NHGK3AAH.mjs} +1 -1
  11. package/package.json +1 -1
  12. package/templates/manifest.json +2 -2
  13. package/templates/storefront-auto/.claude/skills/cimplify-storefront/SKILL.md +2 -2
  14. package/templates/storefront-auto/.env.example +6 -24
  15. package/templates/storefront-auto/README.md +0 -4
  16. package/templates/storefront-auto/app/.well-known/ucp/route.ts +41 -15
  17. package/templates/storefront-auto/app/layout.tsx +31 -26
  18. package/templates/storefront-auto/app/llms.txt/route.ts +3 -5
  19. package/templates/storefront-auto/app/opensearch.xml/route.ts +5 -6
  20. package/templates/storefront-auto/app/products/[slug]/page.tsx +5 -5
  21. package/templates/storefront-auto/app/robots.ts +5 -6
  22. package/templates/storefront-auto/app/sitemap.ts +6 -7
  23. package/templates/storefront-auto/lib/site-url.ts +29 -0
  24. package/templates/storefront-bakery/.claude/skills/cimplify-storefront/SKILL.md +2 -2
  25. package/templates/storefront-bakery/.env.example +6 -15
  26. package/templates/storefront-bakery/AGENTS.md +2 -2
  27. package/templates/storefront-bakery/README.md +0 -4
  28. package/templates/storefront-bakery/app/.well-known/ucp/route.ts +41 -15
  29. package/templates/storefront-bakery/app/layout.tsx +40 -35
  30. package/templates/storefront-bakery/app/llms.txt/route.ts +3 -5
  31. package/templates/storefront-bakery/app/opensearch.xml/route.ts +5 -6
  32. package/templates/storefront-bakery/app/robots.ts +5 -6
  33. package/templates/storefront-bakery/app/sitemap.ts +6 -7
  34. package/templates/storefront-bakery/lib/site-url.ts +29 -0
  35. package/templates/storefront-fashion/.claude/skills/cimplify-storefront/SKILL.md +2 -2
  36. package/templates/storefront-fashion/.env.example +6 -24
  37. package/templates/storefront-fashion/AGENTS.md +1 -1
  38. package/templates/storefront-fashion/README.md +0 -4
  39. package/templates/storefront-fashion/app/.well-known/ucp/route.ts +41 -15
  40. package/templates/storefront-fashion/app/layout.tsx +31 -26
  41. package/templates/storefront-fashion/app/llms.txt/route.ts +3 -5
  42. package/templates/storefront-fashion/app/opensearch.xml/route.ts +5 -6
  43. package/templates/storefront-fashion/app/products/[slug]/page.tsx +5 -5
  44. package/templates/storefront-fashion/app/robots.ts +5 -6
  45. package/templates/storefront-fashion/app/sitemap.ts +6 -7
  46. package/templates/storefront-fashion/lib/site-url.ts +29 -0
  47. package/templates/storefront-grocery/.claude/skills/cimplify-storefront/SKILL.md +2 -2
  48. package/templates/storefront-grocery/.env.example +6 -24
  49. package/templates/storefront-grocery/AGENTS.md +1 -2
  50. package/templates/storefront-grocery/README.md +0 -4
  51. package/templates/storefront-grocery/app/.well-known/ucp/route.ts +41 -15
  52. package/templates/storefront-grocery/app/layout.tsx +31 -26
  53. package/templates/storefront-grocery/app/llms.txt/route.ts +3 -5
  54. package/templates/storefront-grocery/app/opensearch.xml/route.ts +5 -6
  55. package/templates/storefront-grocery/app/robots.ts +5 -6
  56. package/templates/storefront-grocery/app/sitemap.ts +6 -7
  57. package/templates/storefront-grocery/lib/site-url.ts +29 -0
  58. package/templates/storefront-pharmacy/.claude/skills/cimplify-storefront/SKILL.md +2 -2
  59. package/templates/storefront-pharmacy/.env.example +6 -24
  60. package/templates/storefront-pharmacy/AGENTS.md +1 -1
  61. package/templates/storefront-pharmacy/README.md +0 -4
  62. package/templates/storefront-pharmacy/app/.well-known/ucp/route.ts +41 -15
  63. package/templates/storefront-pharmacy/app/layout.tsx +31 -26
  64. package/templates/storefront-pharmacy/app/llms.txt/route.ts +3 -5
  65. package/templates/storefront-pharmacy/app/opensearch.xml/route.ts +5 -6
  66. package/templates/storefront-pharmacy/app/products/[slug]/page.tsx +5 -5
  67. package/templates/storefront-pharmacy/app/robots.ts +5 -6
  68. package/templates/storefront-pharmacy/app/sitemap.ts +6 -7
  69. package/templates/storefront-pharmacy/lib/site-url.ts +29 -0
  70. package/templates/storefront-restaurant/.claude/skills/cimplify-storefront/SKILL.md +2 -2
  71. package/templates/storefront-restaurant/.env.example +6 -24
  72. package/templates/storefront-restaurant/AGENTS.md +1 -2
  73. package/templates/storefront-restaurant/README.md +0 -4
  74. package/templates/storefront-restaurant/app/.well-known/ucp/route.ts +41 -15
  75. package/templates/storefront-restaurant/app/layout.tsx +31 -26
  76. package/templates/storefront-restaurant/app/llms.txt/route.ts +3 -5
  77. package/templates/storefront-restaurant/app/opensearch.xml/route.ts +5 -6
  78. package/templates/storefront-restaurant/app/robots.ts +5 -6
  79. package/templates/storefront-restaurant/app/sitemap.ts +6 -7
  80. package/templates/storefront-restaurant/lib/site-url.ts +29 -0
  81. package/templates/storefront-retail/.claude/skills/cimplify-storefront/SKILL.md +2 -2
  82. package/templates/storefront-retail/.env.example +6 -24
  83. package/templates/storefront-retail/AGENTS.md +1 -1
  84. package/templates/storefront-retail/README.md +0 -4
  85. package/templates/storefront-retail/app/.well-known/ucp/route.ts +41 -15
  86. package/templates/storefront-retail/app/layout.tsx +31 -26
  87. package/templates/storefront-retail/app/llms.txt/route.ts +3 -5
  88. package/templates/storefront-retail/app/opensearch.xml/route.ts +5 -6
  89. package/templates/storefront-retail/app/products/[slug]/page.tsx +5 -5
  90. package/templates/storefront-retail/app/robots.ts +5 -6
  91. package/templates/storefront-retail/app/sitemap.ts +6 -7
  92. package/templates/storefront-retail/lib/site-url.ts +29 -0
  93. package/templates/storefront-services/.claude/skills/cimplify-storefront/SKILL.md +2 -2
  94. package/templates/storefront-services/.env.example +6 -24
  95. package/templates/storefront-services/AGENTS.md +1 -2
  96. package/templates/storefront-services/README.md +0 -4
  97. package/templates/storefront-services/app/.well-known/ucp/route.ts +41 -15
  98. package/templates/storefront-services/app/layout.tsx +31 -26
  99. package/templates/storefront-services/app/llms.txt/route.ts +3 -5
  100. package/templates/storefront-services/app/opensearch.xml/route.ts +5 -6
  101. package/templates/storefront-services/app/robots.ts +5 -6
  102. package/templates/storefront-services/app/sitemap.ts +6 -7
  103. package/templates/storefront-services/lib/site-url.ts +29 -0
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { TEMPLATES } from './chunk-TR65XHUE.mjs';
3
- import { package_default } from './chunk-DZPR5L6H.mjs';
2
+ import { TEMPLATES } from './chunk-OPWMPCPM.mjs';
3
+ import { package_default } from './chunk-72IHRRLU.mjs';
4
4
 
5
5
  // src/dispatcher.ts
6
6
  var VERSION = package_default.version ?? "unknown";
@@ -134,15 +134,15 @@ var COMMANDS = {
134
134
  logs: () => import('./logs-YNN2PQ24.mjs'),
135
135
  status: () => import('./status-JSYXM5RT.mjs'),
136
136
  dev: () => import('./dev-ONW2S77K.mjs'),
137
- introspect: () => import('./introspect-WDGC7BKQ.mjs'),
138
- doctor: () => import('./doctor-QO2SRNH6.mjs'),
139
- explain: () => import('./explain-CGWT7HMV.mjs'),
137
+ introspect: () => import('./introspect-5ODUL6UC.mjs'),
138
+ doctor: () => import('./doctor-FZCJWAPT.mjs'),
139
+ explain: () => import('./explain-4I6JLKDW.mjs'),
140
140
  assets: () => import('./assets-EBEMMENZ.mjs'),
141
141
  repo: () => import('./repo-WOBWKEAO.mjs'),
142
- list: () => import('./list-NO5SPHSU.mjs'),
143
- add: () => import('./add-BEJH4T3P.mjs'),
144
- update: () => import('./update-EEQ7JJTP.mjs'),
145
- upgrade: () => import('./update-EEQ7JJTP.mjs'),
142
+ list: () => import('./list-EKOWQ53I.mjs'),
143
+ add: () => import('./add-FU46PRCJ.mjs'),
144
+ update: () => import('./update-NHGK3AAH.mjs'),
145
+ upgrade: () => import('./update-NHGK3AAH.mjs'),
146
146
  "auth-step-up": () => import('./auth-step-up-BIUYQJP6.mjs')
147
147
  };
148
148
  var COMMAND_PREFIXES = {
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { gatherIntrospection } from './chunk-MUG454RK.mjs';
2
+ import { gatherIntrospection } from './chunk-A6BBQWTB.mjs';
3
3
  import './chunk-K5464A3L.mjs';
4
4
  import './chunk-DBZ3UOQ2.mjs';
5
- import './chunk-DZPR5L6H.mjs';
5
+ import './chunk-72IHRRLU.mjs';
6
6
  import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
7
7
  import { ApiClient } from './chunk-MAOO6ZZ5.mjs';
8
8
  import { readAuthOrNull } from './chunk-UBAI443T.mjs';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { package_default } from './chunk-DZPR5L6H.mjs';
2
+ import { package_default } from './chunk-72IHRRLU.mjs';
3
3
  import { parseArgs } from './chunk-C4M3DXKC.mjs';
4
4
  import { bold, dim, info, result, CliError, CLI_ERROR_CODE } from './chunk-E2T2SBP5.mjs';
5
5
 
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-MUG454RK.mjs';
2
+ export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-A6BBQWTB.mjs';
3
3
  import './chunk-K5464A3L.mjs';
4
4
  import './chunk-DBZ3UOQ2.mjs';
5
- import './chunk-DZPR5L6H.mjs';
5
+ import './chunk-72IHRRLU.mjs';
6
6
  import './chunk-C4M3DXKC.mjs';
7
7
  import './chunk-UBAI443T.mjs';
8
8
  import './chunk-E2T2SBP5.mjs';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { REGISTRY_INDEX } from './chunk-TR65XHUE.mjs';
2
+ import { REGISTRY_INDEX } from './chunk-OPWMPCPM.mjs';
3
3
  import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
4
4
  import { CliError, CLI_ERROR_CODE, info, bold, dim, green, result } from './chunk-E2T2SBP5.mjs';
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { package_default } from './chunk-DZPR5L6H.mjs';
2
+ import { package_default } from './chunk-72IHRRLU.mjs';
3
3
  import { promptYesNo } from './chunk-ITAFAORS.mjs';
4
4
  import { parseArgs, flagBool, flagString } from './chunk-C4M3DXKC.mjs';
5
5
  import { success, bold, info, dim, result, failure, CliError, CLI_ERROR_CODE, step, isJsonMode } from './chunk-E2T2SBP5.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Cimplify CLI — deploy, manage env vars, link projects, and scaffold storefronts",
5
5
  "keywords": [
6
6
  "cimplify",
@@ -1,6 +1,6 @@
1
1
  {
2
- "cliVersion": "0.5.2",
3
- "generatedAt": "2026-05-16T20:10:00.000Z",
2
+ "cliVersion": "0.5.4",
3
+ "generatedAt": "2026-05-16T20:50:00.000Z",
4
4
  "templates": [
5
5
  {
6
6
  "id": "bakery",
@@ -49,7 +49,7 @@ mock: { seed, businessId }
49
49
  1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.
50
50
  2. Edit `app/globals.css` `@theme { … }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.
51
51
  3. (Optional) Swap fonts in `app/layout.tsx` — `next/font/google` import + the variable wired into the `<html>` className.
52
- 4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.
52
+ 4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).
53
53
 
54
54
  Don't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.
55
55
 
@@ -131,7 +131,7 @@ cimplify domains add my-store.com
131
131
  | Which page reads which `brand.X` field | `AGENTS.md` at project root |
132
132
  | Architectural rules | `AGENTS.md` at project root + this skill |
133
133
  | Running locally | `bun dev` (boots mock + Next together) |
134
- | Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |
134
+ | Switching mock seed | edit `dev:mock` in `package.json` |
135
135
  | Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |
136
136
 
137
137
  ## What to do when the user asks something out-of-scope
@@ -1,26 +1,8 @@
1
- # Tenant public key. Get yours from the desk Developers tab. The mock
2
- # accepts any value, so leave the placeholder during local dev.
1
+ # Your tenant public key. Get it from the desk's Developers tab in
2
+ # production; the mock accepts any value during local dev.
3
3
  NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev
4
4
 
5
- # Business id used by the mock seed (default: Driveline Auto).
6
- NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_driveline_auto
7
-
8
- # Canonical public site URL — used by sitemap.xml, robots.txt, llms.txt,
9
- # and OpenGraph metadata. Set this on production deploys; `cimplify env push`
10
- # wires it into your linked project.
11
- NEXT_PUBLIC_SITE_URL=https://example.com
12
-
13
- # Business handle (human-readable slug, e.g. "akua-bakery"). Used by the
14
- # UCP manifest endpoint at /.well-known/ucp so AI agents (Claude / ChatGPT /
15
- # Gemini) can discover this storefront's commerce capabilities. Set this
16
- # on production deploys; leave empty in dev unless you're testing UCP.
17
- NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE=
18
-
19
- # ── Escape hatches (rarely needed) ────────────────────────────────────
20
- # Override the storefront API host (where /api/v1/catalogue, /api/v1/orders
21
- # live). Defaults to https://storefronts.cimplify.io in production,
22
- # 127.0.0.1:8787 in dev. Set only for self-hosted Cimplify or staging.
23
- # NEXT_PUBLIC_CIMPLIFY_STOREFRONT_URL=
24
-
25
- # Override the management/UCP host. Defaults to https://api.cimplify.io.
26
- # NEXT_PUBLIC_CIMPLIFY_API_URL=
5
+ # Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,
6
+ # OpenGraph, and llms.txt. Leave unset and the storefront derives the
7
+ # canonical from the request `Host` header automatically.
8
+ # NEXT_PUBLIC_SITE_URL=
@@ -31,16 +31,12 @@ cimplify-mock --seed restaurant # Mama's Kitchen
31
31
  cimplify-mock --seed retail # Currents Electronics
32
32
  ```
33
33
 
34
- Update `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` to match the seeded business.
35
-
36
34
  ## Go live
37
35
 
38
36
  ```diff
39
37
  # .env.local
40
38
  - NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev
41
39
  + NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>
42
- - NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_driveline_auto
43
- + NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>
44
40
  ```
45
41
 
46
42
  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,44 +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 to Cimplify, which
9
- * returns the canonical manifest. Edge-cached for an hour because
10
- * 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.
11
12
  */
12
13
  const UCP_API_BASE = "https://api.cimplify.io";
13
-
14
+ const STOREFRONT_API_BASE =
15
+ process.env.NODE_ENV === "production"
16
+ ? "https://storefronts.cimplify.io"
17
+ : "http://127.0.0.1:8787";
14
18
 
15
19
  export async function GET() {
16
- const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;
17
-
18
- if (!businessHandle) {
20
+ const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;
21
+ if (!publicKey) {
19
22
  return NextResponse.json(
20
23
  {
21
- error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",
24
+ error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",
22
25
  remediation:
23
- "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).",
24
27
  },
25
28
  { status: 500 },
26
29
  );
27
30
  }
28
31
 
29
32
  try {
30
- const response = await fetch(
31
- `${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`,
32
58
  {
33
59
  headers: { "Content-Type": "application/json" },
34
60
  next: { revalidate: 3600 },
35
61
  },
36
62
  );
37
63
 
38
- if (!response.ok) {
64
+ if (!manifestResp.ok) {
39
65
  return NextResponse.json(
40
- { error: `Upstream UCP manifest fetch failed: ${response.status}` },
41
- { status: response.status },
66
+ { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },
67
+ { status: manifestResp.status },
42
68
  );
43
69
  }
44
70
 
45
- const manifest = await response.json();
71
+ const manifest = await manifestResp.json();
46
72
  return NextResponse.json(manifest, {
47
73
  headers: {
48
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"],
@@ -19,42 +20,46 @@ const mono = JetBrains_Mono({
19
20
  display: "swap",
20
21
  });
21
22
 
22
- const SITE_URL =
23
- process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
24
-
25
- export const metadata: Metadata = {
26
- metadataBase: new URL(SITE_URL),
27
- title: {
23
+ export async function generateMetadata(): Promise<Metadata> {
24
+ const siteUrl = await getSiteUrl();
25
+ return {
26
+ metadataBase: new URL(siteUrl),
27
+ title: {
28
28
  default: brand.name,
29
29
  template: `%s — ${brand.name}`,
30
- },
31
- description: brand.description,
32
- openGraph: {
30
+ },
31
+ description: brand.description,
32
+ openGraph: {
33
33
  type: "website",
34
34
  siteName: brand.name,
35
35
  locale: brand.locale,
36
- },
37
- twitter: { card: "summary_large_image" },
38
- };
36
+ },
37
+ twitter: { card: "summary_large_image" },
38
+ };
39
+ }
39
40
 
40
- const ORGANIZATION_LD = {
41
- "@context": "https://schema.org",
42
- "@type": brand.schemaType,
43
- name: brand.name,
44
- url: SITE_URL,
45
- description: brand.description,
46
- email: brand.contact.email,
47
- telephone: brand.contact.phoneTel,
48
- address: {
41
+ async function organizationLd() {
42
+ const siteUrl = await getSiteUrl();
43
+ return {
44
+ "@context": "https://schema.org",
45
+ "@type": brand.schemaType,
46
+ name: brand.name,
47
+ url: siteUrl,
48
+ description: brand.description,
49
+ email: brand.contact.email,
50
+ telephone: brand.contact.phoneTel,
51
+ address: {
49
52
  "@type": "PostalAddress",
50
53
  streetAddress: brand.contact.streetAddress,
51
54
  addressLocality: brand.contact.city,
52
55
  addressCountry: brand.contact.countryCode,
53
- },
54
- sameAs: brand.socials.map((s) => s.href),
55
- };
56
+ },
57
+ sameAs: brand.socials.map((s) => s.href),
58
+ };
59
+ }
56
60
 
57
- export default function RootLayout({ children }: { children: React.ReactNode }) {
61
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
62
+ const ld = await organizationLd();
58
63
  return (
59
64
  <html lang="en" suppressHydrationWarning className={`${inter.variable} ${mono.variable}`}>
60
65
  <body
@@ -63,7 +68,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
63
68
  >
64
69
  <script
65
70
  type="application/ld+json"
66
- dangerouslySetInnerHTML={{ __html: JSON.stringify(ORGANIZATION_LD) }}
71
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}
67
72
  />
68
73
  <Providers>
69
74
  <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, {
@@ -11,11 +11,9 @@ import {
11
11
  } from "@cimplify/sdk/server";
12
12
  import { ProductDetail } from "./product-detail";
13
13
  import { brand } from "@/lib/brand";
14
+ import { getSiteUrl } from "@/lib/site-url";
14
15
 
15
- const SITE_URL =
16
- process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
17
-
18
- function productLd(product: ProductWithDetails) {
16
+ function productLd(product: ProductWithDetails, SITE_URL: string) {
19
17
  const image = product.image_url ?? product.images?.[0];
20
18
  const inStock = product.inventory_status?.in_stock !== false;
21
19
  return {
@@ -74,6 +72,7 @@ export async function generateMetadata({
74
72
  params: Promise<{ slug: string }>;
75
73
  }): Promise<Metadata> {
76
74
  const { slug } = await params;
75
+ const siteUrl = await getSiteUrl();
77
76
  const data = await getProduct(slug);
78
77
  if (!data) return {};
79
78
  const { product } = data;
@@ -108,6 +107,7 @@ async function ProductContent({
108
107
  params: Promise<{ slug: string }>;
109
108
  }) {
110
109
  const { slug } = await params;
110
+ const siteUrl = await getSiteUrl();
111
111
  const data = await getProduct(slug);
112
112
  if (!data) notFound();
113
113
 
@@ -115,7 +115,7 @@ async function ProductContent({
115
115
  <>
116
116
  <script
117
117
  type="application/ld+json"
118
- dangerouslySetInnerHTML={{ __html: JSON.stringify(productLd(data.product)) }}
118
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(productLd(data.product, siteUrl)) }}
119
119
  />
120
120
  <nav
121
121
  aria-label="Breadcrumb"
@@ -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,28 +27,28 @@ 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,
35
34
  }));
36
35
 
37
36
  const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({
38
- url: `${SITE_URL}/products/${p.slug ?? p.id}`,
37
+ url: `${siteUrl}/products/${p.slug ?? p.id}`,
39
38
  lastModified: p.updated_at ? new Date(p.updated_at) : now,
40
39
  changeFrequency: "weekly",
41
40
  priority: 0.7,
42
41
  }));
43
42
 
44
43
  const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({
45
- url: `${SITE_URL}/categories/${c.slug}`,
44
+ url: `${siteUrl}/categories/${c.slug}`,
46
45
  lastModified: now,
47
46
  changeFrequency: "weekly",
48
47
  priority: 0.6,
49
48
  }));
50
49
 
51
50
  const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({
52
- url: `${SITE_URL}/collections/${c.slug}`,
51
+ url: `${siteUrl}/collections/${c.slug}`,
53
52
  lastModified: now,
54
53
  changeFrequency: "weekly",
55
54
  priority: 0.6,
@@ -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
+ }
@@ -49,7 +49,7 @@ mock: { seed, businessId }
49
49
  1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.
50
50
  2. Edit `app/globals.css` `@theme { … }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.
51
51
  3. (Optional) Swap fonts in `app/layout.tsx` — `next/font/google` import + the variable wired into the `<html>` className.
52
- 4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.
52
+ 4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).
53
53
 
54
54
  Don't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.
55
55
 
@@ -131,7 +131,7 @@ cimplify domains add my-store.com
131
131
  | Which page reads which `brand.X` field | `AGENTS.md` at project root |
132
132
  | Architectural rules | `AGENTS.md` at project root + this skill |
133
133
  | Running locally | `bun dev` (boots mock + Next together) |
134
- | Switching mock seed | edit `dev:mock` in `package.json` and `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` |
134
+ | Switching mock seed | edit `dev:mock` in `package.json` |
135
135
  | Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |
136
136
 
137
137
  ## What to do when the user asks something out-of-scope
@@ -1,17 +1,8 @@
1
- # Tenant public key. Get yours from the desk Developers tab. The mock
2
- # accepts any value, so leave the placeholder during local dev.
1
+ # Your tenant public key. Get it from the desk's Developers tab in
2
+ # production; the mock accepts any value during local dev.
3
3
  NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev
4
4
 
5
- # Business id used by the mock seed (default: Akua's Bakery).
6
- NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_default_akua_bakery
7
-
8
- # Canonical public site URL — used by sitemap.xml, robots.txt, llms.txt,
9
- # and OpenGraph metadata. Set this on production deploys; `cimplify env push`
10
- # wires it into your linked project.
11
- NEXT_PUBLIC_SITE_URL=https://example.com
12
-
13
- # Business handle (human-readable slug, e.g. "akua-bakery"). Used by the
14
- # UCP manifest endpoint at /.well-known/ucp so AI agents (Claude / ChatGPT /
15
- # Gemini) can discover this storefront's commerce capabilities. Set this
16
- # on production deploys; leave empty in dev unless you're testing UCP.
17
- NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE=
5
+ # Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,
6
+ # OpenGraph, and llms.txt. Leave unset and the storefront derives the
7
+ # canonical from the request `Host` header automatically.
8
+ # NEXT_PUBLIC_SITE_URL=
@@ -8,7 +8,7 @@ To rebrand this storefront end-to-end as a different merchant:
8
8
 
9
9
  1. **Edit `lib/brand.ts`.** Every visible string reads from this file.
10
10
  2. **Edit `app/globals.css`** — the `@theme { … }` block holds the design tokens (palette, radius, font references).
11
- 3. **Edit `.env.local`** — set `NEXT_PUBLIC_CIMPLIFY_API_URL`, `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`, `NEXT_PUBLIC_SITE_URL`.
11
+ 3. **Edit `.env.local`** — set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).
12
12
 
13
13
  That is the entire rebrand. Do not modify any `.tsx` file in `app/` or `components/` for content changes — they are design-only and read from `brand`.
14
14
 
@@ -90,7 +90,7 @@ app/
90
90
 
91
91
  - Product detail uses a **URL-driven modal** (`?product=<slug>`), not a static `/products/[slug]` route. Fits impulse-purchase food UX.
92
92
  - Schema.org `@type` is `Bakery` — set in `brand.schemaType`.
93
- - Mock seed: `--seed default` (Akua's Bakery). To preview a different industry, edit `dev:mock` in `package.json` and update `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID`.
93
+ - Mock seed: `--seed default`. To preview a different industry, edit `dev:mock` in `package.json`.
94
94
 
95
95
  ## Known TODOs (not blocking, but worth knowing)
96
96
 
@@ -56,16 +56,12 @@ cimplify-mock --seed services # Serene Spa
56
56
  cimplify-mock --seed grocery # FreshMart
57
57
  ```
58
58
 
59
- Update `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` to match the seeded business.
60
-
61
59
  ## Go live
62
60
 
63
61
  ```diff
64
62
  # .env.local
65
63
  - NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev
66
64
  + NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>
67
- - NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_default_akua_bakery
68
- + NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>
69
65
  ```
70
66
 
71
67
  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`.