@cimplify/cli 0.5.2 → 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.
- package/dist/{add-BEJH4T3P.mjs → add-3DANJHCI.mjs} +1 -1
- package/dist/{chunk-MUG454RK.mjs → chunk-2547SIWD.mjs} +1 -1
- package/dist/{chunk-DZPR5L6H.mjs → chunk-BB7IBX6X.mjs} +1 -1
- package/dist/{chunk-TR65XHUE.mjs → chunk-ZEGSQVM5.mjs} +23 -23
- package/dist/dispatcher.mjs +9 -9
- package/dist/{doctor-QO2SRNH6.mjs → doctor-TD4RZCCM.mjs} +2 -2
- package/dist/{explain-CGWT7HMV.mjs → explain-7OSAQVJ6.mjs} +1 -1
- package/dist/{introspect-WDGC7BKQ.mjs → introspect-SKQMAYFJ.mjs} +2 -2
- package/dist/{list-NO5SPHSU.mjs → list-XLJOS2DK.mjs} +1 -1
- package/dist/{update-EEQ7JJTP.mjs → update-4GX2J3IB.mjs} +1 -1
- package/package.json +1 -1
- package/templates/manifest.json +2 -2
- package/templates/storefront-auto/.env.example +9 -22
- package/templates/storefront-auto/app/.well-known/ucp/route.ts +41 -15
- package/templates/storefront-auto/app/layout.tsx +31 -26
- package/templates/storefront-auto/app/llms.txt/route.ts +3 -5
- package/templates/storefront-auto/app/opensearch.xml/route.ts +5 -6
- package/templates/storefront-auto/app/products/[slug]/page.tsx +5 -5
- package/templates/storefront-auto/app/robots.ts +5 -6
- package/templates/storefront-auto/app/sitemap.ts +6 -7
- package/templates/storefront-auto/lib/site-url.ts +29 -0
- package/templates/storefront-bakery/.env.example +9 -13
- package/templates/storefront-bakery/app/.well-known/ucp/route.ts +41 -15
- package/templates/storefront-bakery/app/layout.tsx +40 -35
- package/templates/storefront-bakery/app/llms.txt/route.ts +3 -5
- package/templates/storefront-bakery/app/opensearch.xml/route.ts +5 -6
- package/templates/storefront-bakery/app/robots.ts +5 -6
- package/templates/storefront-bakery/app/sitemap.ts +6 -7
- package/templates/storefront-bakery/lib/site-url.ts +29 -0
- package/templates/storefront-fashion/.env.example +9 -22
- package/templates/storefront-fashion/app/.well-known/ucp/route.ts +41 -15
- package/templates/storefront-fashion/app/layout.tsx +31 -26
- package/templates/storefront-fashion/app/llms.txt/route.ts +3 -5
- package/templates/storefront-fashion/app/opensearch.xml/route.ts +5 -6
- package/templates/storefront-fashion/app/products/[slug]/page.tsx +5 -5
- package/templates/storefront-fashion/app/robots.ts +5 -6
- package/templates/storefront-fashion/app/sitemap.ts +6 -7
- package/templates/storefront-fashion/lib/site-url.ts +29 -0
- package/templates/storefront-grocery/.env.example +9 -22
- package/templates/storefront-grocery/app/.well-known/ucp/route.ts +41 -15
- package/templates/storefront-grocery/app/layout.tsx +31 -26
- package/templates/storefront-grocery/app/llms.txt/route.ts +3 -5
- package/templates/storefront-grocery/app/opensearch.xml/route.ts +5 -6
- package/templates/storefront-grocery/app/robots.ts +5 -6
- package/templates/storefront-grocery/app/sitemap.ts +6 -7
- package/templates/storefront-grocery/lib/site-url.ts +29 -0
- package/templates/storefront-pharmacy/.env.example +9 -22
- package/templates/storefront-pharmacy/app/.well-known/ucp/route.ts +41 -15
- package/templates/storefront-pharmacy/app/layout.tsx +31 -26
- package/templates/storefront-pharmacy/app/llms.txt/route.ts +3 -5
- package/templates/storefront-pharmacy/app/opensearch.xml/route.ts +5 -6
- package/templates/storefront-pharmacy/app/products/[slug]/page.tsx +5 -5
- package/templates/storefront-pharmacy/app/robots.ts +5 -6
- package/templates/storefront-pharmacy/app/sitemap.ts +6 -7
- package/templates/storefront-pharmacy/lib/site-url.ts +29 -0
- package/templates/storefront-restaurant/.env.example +9 -22
- package/templates/storefront-restaurant/app/.well-known/ucp/route.ts +41 -15
- package/templates/storefront-restaurant/app/layout.tsx +31 -26
- package/templates/storefront-restaurant/app/llms.txt/route.ts +3 -5
- package/templates/storefront-restaurant/app/opensearch.xml/route.ts +5 -6
- package/templates/storefront-restaurant/app/robots.ts +5 -6
- package/templates/storefront-restaurant/app/sitemap.ts +6 -7
- package/templates/storefront-restaurant/lib/site-url.ts +29 -0
- package/templates/storefront-retail/.env.example +9 -22
- package/templates/storefront-retail/app/.well-known/ucp/route.ts +41 -15
- package/templates/storefront-retail/app/layout.tsx +31 -26
- package/templates/storefront-retail/app/llms.txt/route.ts +3 -5
- package/templates/storefront-retail/app/opensearch.xml/route.ts +5 -6
- package/templates/storefront-retail/app/products/[slug]/page.tsx +5 -5
- package/templates/storefront-retail/app/robots.ts +5 -6
- package/templates/storefront-retail/app/sitemap.ts +6 -7
- package/templates/storefront-retail/lib/site-url.ts +29 -0
- package/templates/storefront-services/.env.example +9 -22
- package/templates/storefront-services/app/.well-known/ucp/route.ts +41 -15
- package/templates/storefront-services/app/layout.tsx +31 -26
- package/templates/storefront-services/app/llms.txt/route.ts +3 -5
- package/templates/storefront-services/app/opensearch.xml/route.ts +5 -6
- package/templates/storefront-services/app/robots.ts +5 -6
- package/templates/storefront-services/app/sitemap.ts +6 -7
- package/templates/storefront-services/lib/site-url.ts +29 -0
|
@@ -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="${
|
|
18
|
-
<Url type="application/opensearchdescription+xml" rel="self" template="${
|
|
19
|
-
<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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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: `${
|
|
16
|
-
host:
|
|
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: `${
|
|
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: `${
|
|
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: `${
|
|
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: `${
|
|
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
|
+
}
|
|
@@ -1,26 +1,13 @@
|
|
|
1
|
-
#
|
|
2
|
-
# 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.
|
|
3
3
|
NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev
|
|
4
4
|
|
|
5
|
-
#
|
|
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.
|
|
6
8
|
NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_mamas_kitchen
|
|
7
9
|
|
|
8
|
-
#
|
|
9
|
-
# and
|
|
10
|
-
#
|
|
11
|
-
NEXT_PUBLIC_SITE_URL=
|
|
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=
|
|
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=
|
|
@@ -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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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: "
|
|
24
|
+
error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",
|
|
22
25
|
remediation:
|
|
23
|
-
"Set
|
|
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
|
-
|
|
31
|
-
|
|
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 (!
|
|
64
|
+
if (!manifestResp.ok) {
|
|
39
65
|
return NextResponse.json(
|
|
40
|
-
{ error: `Upstream UCP manifest fetch failed: ${
|
|
41
|
-
{ status:
|
|
66
|
+
{ error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },
|
|
67
|
+
{ status: manifestResp.status },
|
|
42
68
|
);
|
|
43
69
|
}
|
|
44
70
|
|
|
45
|
-
const manifest = await
|
|
71
|
+
const manifest = await manifestResp.json();
|
|
46
72
|
return NextResponse.json(manifest, {
|
|
47
73
|
headers: {
|
|
48
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 lora = Lora({
|
|
|
21
22
|
display: "swap",
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
title: {
|
|
25
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
26
|
+
const siteUrl = await getSiteUrl();
|
|
27
|
+
return {
|
|
28
|
+
metadataBase: new URL(siteUrl),
|
|
29
|
+
title: {
|
|
30
30
|
default: brand.name,
|
|
31
31
|
template: `%s — ${brand.name}`,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
},
|
|
33
|
+
description: brand.description,
|
|
34
|
+
openGraph: {
|
|
35
35
|
type: "website",
|
|
36
36
|
siteName: brand.name,
|
|
37
37
|
locale: brand.locale,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
};
|
|
38
|
+
},
|
|
39
|
+
twitter: { card: "summary_large_image" },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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: {
|
|
51
54
|
"@type": "PostalAddress",
|
|
52
55
|
streetAddress: brand.contact.streetAddress,
|
|
53
56
|
addressLocality: brand.contact.city,
|
|
54
57
|
addressCountry: brand.contact.countryCode,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
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} ${lora.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(
|
|
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
|
-
|
|
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="${
|
|
18
|
-
<Url type="application/opensearchdescription+xml" rel="self" template="${
|
|
19
|
-
<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
|
-
|
|
4
|
-
|
|
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: `${
|
|
16
|
-
host:
|
|
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: `${
|
|
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: `${
|
|
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: `${
|
|
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: `${
|
|
54
|
+
url: `${siteUrl}/collections/${c.slug}`,
|
|
56
55
|
lastModified: now,
|
|
57
56
|
changeFrequency: "weekly",
|
|
58
57
|
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
|
+
}
|
|
@@ -1,26 +1,13 @@
|
|
|
1
|
-
#
|
|
2
|
-
# 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.
|
|
3
3
|
NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev
|
|
4
4
|
|
|
5
|
-
#
|
|
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.
|
|
6
8
|
NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_currents_electronics
|
|
7
9
|
|
|
8
|
-
#
|
|
9
|
-
# and
|
|
10
|
-
#
|
|
11
|
-
NEXT_PUBLIC_SITE_URL=
|
|
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=
|
|
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=
|