create-cartbase 0.1.20 → 0.1.22
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/LICENSE +21 -21
- package/README.md +25 -25
- package/dist/index.js +20 -20
- package/package.json +24 -24
- package/template/app/docs/auth.md +105 -105
- package/template/app/docs/carts.md +23 -4
- package/template/app/docs/categories.md +194 -194
- package/template/app/docs/checkout.md +714 -714
- package/template/app/docs/components.md +270 -24
- package/template/app/docs/consent.md +91 -91
- package/template/app/docs/deploy.md +197 -197
- package/template/app/docs/gift-cards.md +153 -153
- package/template/app/docs/metaobjects.md +126 -126
- package/template/app/docs/orders.md +221 -221
- package/template/app/docs/regions.md +269 -269
- package/template/app/docs/reviews.md +258 -238
- package/template/app/docs/search.md +227 -227
- package/template/app/docs/store.md +23 -1
- package/template/app/docs/subscriptions.md +148 -148
- package/template/app/docs/variables.md +331 -315
- package/template/app/package.json +1 -1
- package/template/app/postcss.config.cjs +11 -11
- package/template/app/src/app/checkout/checkout-empty.tsx +36 -0
- package/template/app/src/app/checkout/checkout-page-client.tsx +80 -73
- package/template/app/src/app/checkout/error.tsx +23 -0
- package/template/app/src/app/checkout/page.tsx +16 -7
- package/template/app/src/app/globals.css +26 -26
- package/template/app/src/app/layout.tsx +126 -126
- package/template/app/src/app/page.tsx +37 -37
- package/template/app/src/app/products/[handle]/page.tsx +89 -89
- package/template/app/src/app/search/page.tsx +33 -33
- package/template/app/src/lib/browser-client.ts +35 -35
- package/template/app/src/lib/catalog.ts +120 -120
- package/template/app/src/lib/server-client.ts +25 -25
- package/template/app/src/lib/store-client.ts +16 -16
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Tailwind 4 ships its own PostCSS plugin and handles vendor prefixing
|
|
3
|
-
* internally, so the v3 pairing of `tailwindcss` + `autoprefixer` is gone.
|
|
4
|
-
* There is no tailwind.config file either: the theme is defined in CSS
|
|
5
|
-
* (see src/app/globals.css and the theme shipped by @cartbase/storefront).
|
|
6
|
-
*/
|
|
7
|
-
module.exports = {
|
|
8
|
-
plugins: {
|
|
9
|
-
"@tailwindcss/postcss": {},
|
|
10
|
-
},
|
|
11
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* Tailwind 4 ships its own PostCSS plugin and handles vendor prefixing
|
|
3
|
+
* internally, so the v3 pairing of `tailwindcss` + `autoprefixer` is gone.
|
|
4
|
+
* There is no tailwind.config file either: the theme is defined in CSS
|
|
5
|
+
* (see src/app/globals.css and the theme shipped by @cartbase/storefront).
|
|
6
|
+
*/
|
|
7
|
+
module.exports = {
|
|
8
|
+
plugins: {
|
|
9
|
+
"@tailwindcss/postcss": {},
|
|
10
|
+
},
|
|
11
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Link from "next/link"
|
|
2
|
+
import { defaultCheckoutLabels } from "@cartbase/storefront/checkout/labels"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The checkout with nothing to check out.
|
|
6
|
+
*
|
|
7
|
+
* A shopper who lands here with an empty cart used to be redirected to the
|
|
8
|
+
* home page without a word, which reads as the store losing their order.
|
|
9
|
+
* The page says it instead and offers the way back, which matters more here
|
|
10
|
+
* than anywhere: a checkout shell carries no header and no cart drawer, so
|
|
11
|
+
* the notice is the only door.
|
|
12
|
+
*
|
|
13
|
+
* The copy is the label pack's, so a store that mounts a locale gets it in
|
|
14
|
+
* its own language; this route renders on the server, above the provider,
|
|
15
|
+
* so it reads the defaults directly.
|
|
16
|
+
*/
|
|
17
|
+
export function CheckoutEmpty() {
|
|
18
|
+
const labels = defaultCheckoutLabels
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="mx-auto flex max-w-md flex-col items-center px-5 py-20 text-center">
|
|
22
|
+
<h1 className="text-xl font-semibold text-foreground">
|
|
23
|
+
{labels.cartEmptyTitle}
|
|
24
|
+
</h1>
|
|
25
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
26
|
+
{labels.cartEmptyText}
|
|
27
|
+
</p>
|
|
28
|
+
<Link
|
|
29
|
+
href="/"
|
|
30
|
+
className="mt-6 inline-flex h-12 items-center justify-center rounded-xl bg-foreground px-8 text-sm font-semibold text-card transition-colors hover:bg-foreground/90"
|
|
31
|
+
>
|
|
32
|
+
{labels.cartEmptyBack}
|
|
33
|
+
</Link>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -1,73 +1,80 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useRouter } from "next/navigation"
|
|
4
|
-
import type { Cart } from "@cartbase/storefront/api/carts"
|
|
5
|
-
import type { StoreShippingOption } from "@cartbase/storefront/api/checkout"
|
|
6
|
-
import type { StorePaymentEntry } from "@cartbase/storefront/api/checkout"
|
|
7
|
-
import { CheckoutProvider } from "@cartbase/storefront/checkout/context"
|
|
8
|
-
import { CheckoutClient } from "@cartbase/storefront/checkout/checkout-client"
|
|
9
|
-
import { browserClient } from "@/lib/browser-client"
|
|
10
|
-
|
|
11
|
-
/** sessionStorage key the confirmation page reads (guests have no order
|
|
12
|
-
* read endpoint — the completeCart() response is the only order handle,
|
|
13
|
-
* orders.md). */
|
|
14
|
-
export const LAST_ORDER_STORAGE_KEY = "barter:last-order"
|
|
15
|
-
|
|
16
|
-
export function CheckoutPageClient({
|
|
17
|
-
cart,
|
|
18
|
-
shippingOptions,
|
|
19
|
-
paymentProviders,
|
|
20
|
-
}: {
|
|
21
|
-
cart: Cart
|
|
22
|
-
shippingOptions: StoreShippingOption[]
|
|
23
|
-
paymentProviders: StorePaymentEntry[]
|
|
24
|
-
}) {
|
|
25
|
-
const router = useRouter()
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<CheckoutProvider orderConfirmedPath="/order/{id}/confirmed">
|
|
29
|
-
<CheckoutClient
|
|
30
|
-
client={browserClient}
|
|
31
|
-
cart={cart}
|
|
32
|
-
customer={null}
|
|
33
|
-
availableShippingMethods={shippingOptions}
|
|
34
|
-
availablePaymentMethods={paymentProviders}
|
|
35
|
-
// NO COUNTRY LIST HERE, on purpose (2026-09-13). This file is the
|
|
36
|
-
// seed for every scaffolded store, and it used to pass
|
|
37
|
-
// `countries={[{iso_2:"bg", display_name:"Bulgaria"}]}` with
|
|
38
|
-
// `countryCode="bg"`, so every store on earth shipped a checkout
|
|
39
|
-
// offering one country, Bulgaria, in a box the shopper could not
|
|
40
|
-
// change. The list belongs to the store, not to this file: omit the
|
|
41
|
-
// prop and the hook reads GET /api/store/countries, which answers
|
|
42
|
-
// with the countries the store's Markets declare, or with the whole
|
|
43
|
-
// world when it has declared none.
|
|
44
|
-
//
|
|
45
|
-
// A single-market store adds `countryCode="xx"` here to preselect
|
|
46
|
-
// its country, and a store that sells to exactly one country gets
|
|
47
|
-
// the read-only field automatically, because its Markets say so.
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation"
|
|
4
|
+
import type { Cart } from "@cartbase/storefront/api/carts"
|
|
5
|
+
import type { StoreShippingOption } from "@cartbase/storefront/api/checkout"
|
|
6
|
+
import type { StorePaymentEntry } from "@cartbase/storefront/api/checkout"
|
|
7
|
+
import { CheckoutProvider } from "@cartbase/storefront/checkout/context"
|
|
8
|
+
import { CheckoutClient } from "@cartbase/storefront/checkout/checkout-client"
|
|
9
|
+
import { browserClient } from "@/lib/browser-client"
|
|
10
|
+
|
|
11
|
+
/** sessionStorage key the confirmation page reads (guests have no order
|
|
12
|
+
* read endpoint — the completeCart() response is the only order handle,
|
|
13
|
+
* orders.md). */
|
|
14
|
+
export const LAST_ORDER_STORAGE_KEY = "barter:last-order"
|
|
15
|
+
|
|
16
|
+
export function CheckoutPageClient({
|
|
17
|
+
cart,
|
|
18
|
+
shippingOptions,
|
|
19
|
+
paymentProviders,
|
|
20
|
+
}: {
|
|
21
|
+
cart: Cart
|
|
22
|
+
shippingOptions: StoreShippingOption[]
|
|
23
|
+
paymentProviders: StorePaymentEntry[]
|
|
24
|
+
}) {
|
|
25
|
+
const router = useRouter()
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<CheckoutProvider orderConfirmedPath="/order/{id}/confirmed">
|
|
29
|
+
<CheckoutClient
|
|
30
|
+
client={browserClient}
|
|
31
|
+
cart={cart}
|
|
32
|
+
customer={null}
|
|
33
|
+
availableShippingMethods={shippingOptions}
|
|
34
|
+
availablePaymentMethods={paymentProviders}
|
|
35
|
+
// NO COUNTRY LIST HERE, on purpose (2026-09-13). This file is the
|
|
36
|
+
// seed for every scaffolded store, and it used to pass
|
|
37
|
+
// `countries={[{iso_2:"bg", display_name:"Bulgaria"}]}` with
|
|
38
|
+
// `countryCode="bg"`, so every store on earth shipped a checkout
|
|
39
|
+
// offering one country, Bulgaria, in a box the shopper could not
|
|
40
|
+
// change. The list belongs to the store, not to this file: omit the
|
|
41
|
+
// prop and the hook reads GET /api/store/countries, which answers
|
|
42
|
+
// with the countries the store's Markets declare, or with the whole
|
|
43
|
+
// world when it has declared none.
|
|
44
|
+
//
|
|
45
|
+
// A single-market store adds `countryCode="xx"` here to preselect
|
|
46
|
+
// its country, and a store that sells to exactly one country gets
|
|
47
|
+
// the read-only field automatically, because its Markets say so.
|
|
48
|
+
//
|
|
49
|
+
// NO PAYMENT FILTER HERE, on purpose (2026-09-18), for the same
|
|
50
|
+
// reason as the country list. This file used to pass a
|
|
51
|
+
// `paymentMethodFilter` that kept merchant methods and dropped every
|
|
52
|
+
// connected processor, so every scaffolded store was born ignoring
|
|
53
|
+
// its own card: a merchant could connect Stripe in the admin, see it
|
|
54
|
+
// reported as connected, and still be offered nothing but cash on
|
|
55
|
+
// delivery at checkout, with no error anywhere to explain it. What a
|
|
56
|
+
// store can be paid with is the store's answer, and the listing
|
|
57
|
+
// already gives it: processors it has connected and linked to the
|
|
58
|
+
// market, plus its enabled methods, rule-filtered server-side. The
|
|
59
|
+
// `paymentMethodFilter` seam stays for a store with a real reason to
|
|
60
|
+
// narrow it further, and a store with no processor connected needs
|
|
61
|
+
// no filter to hide one.
|
|
62
|
+
onOrderPlaced={(order) => {
|
|
63
|
+
// Guest order handle = the completeCart() response (orders.md).
|
|
64
|
+
// Stash it (+ the decorated cart lines for the items list) for
|
|
65
|
+
// the confirmation page, then navigate.
|
|
66
|
+
try {
|
|
67
|
+
sessionStorage.setItem(
|
|
68
|
+
LAST_ORDER_STORAGE_KEY,
|
|
69
|
+
JSON.stringify({ order, cartItems: cart.items ?? [] })
|
|
70
|
+
)
|
|
71
|
+
} catch {
|
|
72
|
+
// best-effort — the confirmation page has a fallback state
|
|
73
|
+
}
|
|
74
|
+
router.push(`/order/${order.id}/confirmed`)
|
|
75
|
+
}}
|
|
76
|
+
onCartChange={() => router.refresh()}
|
|
77
|
+
/>
|
|
78
|
+
</CheckoutProvider>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { CheckoutErrorScreen } from "@cartbase/storefront/checkout/checkout-error-screen"
|
|
4
|
+
import { browserClient } from "@/lib/browser-client"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The checkout's error boundary. Next renders this in place of the page
|
|
8
|
+
* when anything in it throws, a server component included, so a shopper at
|
|
9
|
+
* the moment of paying sees the store's own words instead of the
|
|
10
|
+
* framework's error page, and the failure reaches the merchant's checkout
|
|
11
|
+
* error log rather than nobody.
|
|
12
|
+
*/
|
|
13
|
+
export default function CheckoutError({
|
|
14
|
+
error,
|
|
15
|
+
reset,
|
|
16
|
+
}: {
|
|
17
|
+
error: Error & { digest?: string }
|
|
18
|
+
reset: () => void
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<CheckoutErrorScreen error={error} reset={reset} client={browserClient} />
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "@cartbase/storefront/api/checkout"
|
|
9
9
|
import { readCartCookie } from "@/lib/config"
|
|
10
10
|
import { getServerClient } from "@/lib/server-client"
|
|
11
|
+
import { CheckoutEmpty } from "./checkout-empty"
|
|
11
12
|
import { CheckoutPageClient } from "./checkout-page-client"
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -32,14 +33,22 @@ async function Checkout() {
|
|
|
32
33
|
// Back-compat: prefer `_cartbase_cart`, fall back to the legacy
|
|
33
34
|
// `_barter_cart_id` name (platform-fingerprints card).
|
|
34
35
|
const cartId = readCartCookie((name) => jar.get(name)?.value)
|
|
35
|
-
if (!cartId) redirect("/")
|
|
36
36
|
|
|
37
|
-
const client = await getServerClient()
|
|
38
|
-
const cart =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
const client = cartId ? await getServerClient() : null
|
|
38
|
+
const cart =
|
|
39
|
+
client && cartId
|
|
40
|
+
? await retrieveCart(client, cartId)
|
|
41
|
+
.then((res) => res.cart)
|
|
42
|
+
.catch(() => null)
|
|
43
|
+
: null
|
|
44
|
+
|
|
45
|
+
// A placed order still redirects: it has its own confirmation page.
|
|
46
|
+
if (cart?.completed_at) redirect("/")
|
|
47
|
+
|
|
48
|
+
// Nothing to check out, so the page SAYS so rather than bouncing the
|
|
49
|
+
// shopper home without a word (2026-09-18). A checkout shell carries no
|
|
50
|
+
// header and no drawer, so this notice is also the only way back.
|
|
51
|
+
if (!client || !cart || (cart.items ?? []).length === 0) return <CheckoutEmpty />
|
|
43
52
|
|
|
44
53
|
const [shippingOptions, paymentProviders] = await Promise.all([
|
|
45
54
|
listShippingOptions(client, { cart_id: cart.id }),
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The whole styling setup for a Cartbase storefront: two imports.
|
|
3
|
-
*
|
|
4
|
-
* Tailwind 4 is CSS-first, so there is no config file and no preset to
|
|
5
|
-
* register. The library ships its theme filled, so this app renders as a
|
|
6
|
-
* designed store without choosing a single value.
|
|
7
|
-
*
|
|
8
|
-
* To make the store yours, override the token values below the imports.
|
|
9
|
-
* Never redefine the names — components reference them, and the library
|
|
10
|
-
* repaints from the values alone.
|
|
11
|
-
*/
|
|
12
|
-
@import "tailwindcss";
|
|
13
|
-
@import "@cartbase/storefront/theme";
|
|
14
|
-
|
|
15
|
-
/*
|
|
16
|
-
* The library ships as TypeScript source rather than a build, so Tailwind has
|
|
17
|
-
* to scan it to know which classes exist. Without this the components render
|
|
18
|
-
* with no styles at all — the classes are in the markup but never generated.
|
|
19
|
-
*/
|
|
20
|
-
@source "../../node_modules/@cartbase/storefront/src";
|
|
21
|
-
|
|
22
|
-
/*
|
|
23
|
-
* Dark mode is class-based: put `.dark` on <html> and every token swaps.
|
|
24
|
-
* Tailwind 4 needs this declared explicitly.
|
|
25
|
-
*/
|
|
26
|
-
@custom-variant dark (&:where(.dark, .dark *));
|
|
1
|
+
/*
|
|
2
|
+
* The whole styling setup for a Cartbase storefront: two imports.
|
|
3
|
+
*
|
|
4
|
+
* Tailwind 4 is CSS-first, so there is no config file and no preset to
|
|
5
|
+
* register. The library ships its theme filled, so this app renders as a
|
|
6
|
+
* designed store without choosing a single value.
|
|
7
|
+
*
|
|
8
|
+
* To make the store yours, override the token values below the imports.
|
|
9
|
+
* Never redefine the names — components reference them, and the library
|
|
10
|
+
* repaints from the values alone.
|
|
11
|
+
*/
|
|
12
|
+
@import "tailwindcss";
|
|
13
|
+
@import "@cartbase/storefront/theme";
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* The library ships as TypeScript source rather than a build, so Tailwind has
|
|
17
|
+
* to scan it to know which classes exist. Without this the components render
|
|
18
|
+
* with no styles at all — the classes are in the markup but never generated.
|
|
19
|
+
*/
|
|
20
|
+
@source "../../node_modules/@cartbase/storefront/src";
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* Dark mode is class-based: put `.dark` on <html> and every token swaps.
|
|
24
|
+
* Tailwind 4 needs this declared explicitly.
|
|
25
|
+
*/
|
|
26
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
import type { Metadata } from "next"
|
|
2
|
-
import Link from "next/link"
|
|
3
|
-
import type { Menu } from "@cartbase/storefront/api/menus"
|
|
4
|
-
import { ConsentInit } from "@cartbase/storefront/tracking/consent-init"
|
|
5
|
-
import { StorefrontTags } from "@cartbase/storefront/tracking/storefront-tags"
|
|
6
|
-
import { TrackInit } from "@cartbase/storefront/tracking/track-init"
|
|
7
|
-
import { CartButtonClient } from "@cartbase/storefront/common/cart-button-client"
|
|
8
|
-
import { createStorefrontMetadata, PlatformInit } from "@cartbase/storefront/platform"
|
|
9
|
-
import { STORE_LOCALE } from "@/lib/config"
|
|
10
|
-
import { getConsentSettings, getMenuSafe, getStoreIdentity, getTracking } from "@/lib/catalog"
|
|
11
|
-
import { Providers } from "./providers"
|
|
12
|
-
import "./globals.css"
|
|
13
|
-
|
|
14
|
-
// createStorefrontMetadata (platform-fingerprints card) stamps
|
|
15
|
-
// `<meta name="generator" content="Cartbase" />` on every page — never
|
|
16
|
-
// hand-write `generator` here.
|
|
17
|
-
export async function generateMetadata(): Promise<Metadata> {
|
|
18
|
-
const store = await getStoreIdentity()
|
|
19
|
-
return createStorefrontMetadata({
|
|
20
|
-
title: store.name,
|
|
21
|
-
description: store.brand.slogan ?? undefined,
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function MenuNav({ menu }: { menu: Menu | null }) {
|
|
26
|
-
if (!menu || menu.items.length === 0) return null
|
|
27
|
-
return (
|
|
28
|
-
<nav className="flex items-center gap-4">
|
|
29
|
-
{menu.items.map((item) => (
|
|
30
|
-
<Link
|
|
31
|
-
key={`${item.title}-${item.url}`}
|
|
32
|
-
href={item.url}
|
|
33
|
-
className="text-sm text-muted-foreground hover:text-foreground"
|
|
34
|
-
>
|
|
35
|
-
{item.title}
|
|
36
|
-
</Link>
|
|
37
|
-
))}
|
|
38
|
-
</nav>
|
|
39
|
-
)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Runbook steps 3–4: bootstrap store config at layout level, then mount
|
|
44
|
-
* order inside <body>: ConsentInit FIRST (static, synchronous), then the
|
|
45
|
-
* consent-gated UI, then navigation. A store with no menus 404s every
|
|
46
|
-
* handle — render no nav, never crash (menus.md).
|
|
47
|
-
*
|
|
48
|
-
* Nothing here reads the request. The store, the consent settings, the tags
|
|
49
|
-
* and the menus are cached reads (lib/catalog.ts), so the shell is
|
|
50
|
-
* prerendered and every page is served from the CDN (Cache Components). The
|
|
51
|
-
* cart is the visitor's: the drawer reads its id from the cookie in the
|
|
52
|
-
* browser and the cart after it (providers.tsx), and the cart button counts
|
|
53
|
-
* the drawer's cart, so no page waits for the cart.
|
|
54
|
-
*/
|
|
55
|
-
export default async function RootLayout({
|
|
56
|
-
children,
|
|
57
|
-
}: {
|
|
58
|
-
children: React.ReactNode
|
|
59
|
-
}) {
|
|
60
|
-
const [store, consent, tracking, mainMenu, footerMenu] = await Promise.all([
|
|
61
|
-
getStoreIdentity(),
|
|
62
|
-
getConsentSettings(),
|
|
63
|
-
getTracking(),
|
|
64
|
-
getMenuSafe("main-menu"),
|
|
65
|
-
getMenuSafe("footer"),
|
|
66
|
-
])
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<html lang={STORE_LOCALE.code}>
|
|
70
|
-
<body>
|
|
71
|
-
{/* The store's own consent switch decides the default: a store
|
|
72
|
-
that collects consent starts every visitor denied until they
|
|
73
|
-
choose, a store with the banner off starts them granted. The
|
|
74
|
-
prop is required, so a layout cannot leave it out. */}
|
|
75
|
-
<ConsentInit required={consent.enabled} />
|
|
76
|
-
{/* Every marketing tag the store configured in the admin, mounted
|
|
77
|
-
from its own config: Meta, TikTok, ChatGPT, Google (GA4 + Ads
|
|
78
|
-
on one tag), GTM. Nothing to wire per vendor: saving the ids
|
|
79
|
-
under Settings, Integrations is the whole merchant-side act.
|
|
80
|
-
Order matters: ConsentInit sets the Consent Mode defaults
|
|
81
|
-
synchronously ABOVE this, so every tag below inherits the
|
|
82
|
-
gate. The config is a cached read handed over, so the tags
|
|
83
|
-
cost the page nothing at request time. */}
|
|
84
|
-
{tracking ? <StorefrontTags config={tracking} /> : null}
|
|
85
|
-
{/* Captures UTMs and the ad-click ids (ttclid / gclid / gbraid /
|
|
86
|
-
wbraid) that exist ONLY on an ad's landing URL — miss them
|
|
87
|
-
here and no platform can attribute the order to the click. */}
|
|
88
|
-
<TrackInit />
|
|
89
|
-
<PlatformInit storeId={store.id} />
|
|
90
|
-
<Providers consent={consent}>
|
|
91
|
-
<header className="border-b border-border">
|
|
92
|
-
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-6">
|
|
93
|
-
<Link href="/" className="font-semibold text-lg">
|
|
94
|
-
{store.name}
|
|
95
|
-
</Link>
|
|
96
|
-
<div className="flex items-center gap-6">
|
|
97
|
-
<MenuNav menu={mainMenu} />
|
|
98
|
-
<Link
|
|
99
|
-
href="/search"
|
|
100
|
-
className="text-sm text-muted-foreground hover:text-foreground"
|
|
101
|
-
>
|
|
102
|
-
Search
|
|
103
|
-
</Link>
|
|
104
|
-
{/*
|
|
105
|
-
The component workbench lives at /gallery in THIS app only
|
|
106
|
-
(the seed excludes it, src/lib/storefront/seed.ts), so the
|
|
107
|
-
shop's own nav never links to it: a merchant's store must
|
|
108
|
-
not carry a dead link.
|
|
109
|
-
*/}
|
|
110
|
-
{/* No `cart` prop: the badge counts the drawer's own cart. */}
|
|
111
|
-
<CartButtonClient />
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
</header>
|
|
115
|
-
<main>{children}</main>
|
|
116
|
-
<footer className="border-t border-border mt-12">
|
|
117
|
-
<div className="max-w-7xl mx-auto px-4 py-8 text-sm text-muted-foreground flex items-center justify-between">
|
|
118
|
-
<span>{store.name}</span>
|
|
119
|
-
<MenuNav menu={footerMenu} />
|
|
120
|
-
</div>
|
|
121
|
-
</footer>
|
|
122
|
-
</Providers>
|
|
123
|
-
</body>
|
|
124
|
-
</html>
|
|
125
|
-
)
|
|
126
|
-
}
|
|
1
|
+
import type { Metadata } from "next"
|
|
2
|
+
import Link from "next/link"
|
|
3
|
+
import type { Menu } from "@cartbase/storefront/api/menus"
|
|
4
|
+
import { ConsentInit } from "@cartbase/storefront/tracking/consent-init"
|
|
5
|
+
import { StorefrontTags } from "@cartbase/storefront/tracking/storefront-tags"
|
|
6
|
+
import { TrackInit } from "@cartbase/storefront/tracking/track-init"
|
|
7
|
+
import { CartButtonClient } from "@cartbase/storefront/common/cart-button-client"
|
|
8
|
+
import { createStorefrontMetadata, PlatformInit } from "@cartbase/storefront/platform"
|
|
9
|
+
import { STORE_LOCALE } from "@/lib/config"
|
|
10
|
+
import { getConsentSettings, getMenuSafe, getStoreIdentity, getTracking } from "@/lib/catalog"
|
|
11
|
+
import { Providers } from "./providers"
|
|
12
|
+
import "./globals.css"
|
|
13
|
+
|
|
14
|
+
// createStorefrontMetadata (platform-fingerprints card) stamps
|
|
15
|
+
// `<meta name="generator" content="Cartbase" />` on every page — never
|
|
16
|
+
// hand-write `generator` here.
|
|
17
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
18
|
+
const store = await getStoreIdentity()
|
|
19
|
+
return createStorefrontMetadata({
|
|
20
|
+
title: store.name,
|
|
21
|
+
description: store.brand.slogan ?? undefined,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function MenuNav({ menu }: { menu: Menu | null }) {
|
|
26
|
+
if (!menu || menu.items.length === 0) return null
|
|
27
|
+
return (
|
|
28
|
+
<nav className="flex items-center gap-4">
|
|
29
|
+
{menu.items.map((item) => (
|
|
30
|
+
<Link
|
|
31
|
+
key={`${item.title}-${item.url}`}
|
|
32
|
+
href={item.url}
|
|
33
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
34
|
+
>
|
|
35
|
+
{item.title}
|
|
36
|
+
</Link>
|
|
37
|
+
))}
|
|
38
|
+
</nav>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Runbook steps 3–4: bootstrap store config at layout level, then mount
|
|
44
|
+
* order inside <body>: ConsentInit FIRST (static, synchronous), then the
|
|
45
|
+
* consent-gated UI, then navigation. A store with no menus 404s every
|
|
46
|
+
* handle — render no nav, never crash (menus.md).
|
|
47
|
+
*
|
|
48
|
+
* Nothing here reads the request. The store, the consent settings, the tags
|
|
49
|
+
* and the menus are cached reads (lib/catalog.ts), so the shell is
|
|
50
|
+
* prerendered and every page is served from the CDN (Cache Components). The
|
|
51
|
+
* cart is the visitor's: the drawer reads its id from the cookie in the
|
|
52
|
+
* browser and the cart after it (providers.tsx), and the cart button counts
|
|
53
|
+
* the drawer's cart, so no page waits for the cart.
|
|
54
|
+
*/
|
|
55
|
+
export default async function RootLayout({
|
|
56
|
+
children,
|
|
57
|
+
}: {
|
|
58
|
+
children: React.ReactNode
|
|
59
|
+
}) {
|
|
60
|
+
const [store, consent, tracking, mainMenu, footerMenu] = await Promise.all([
|
|
61
|
+
getStoreIdentity(),
|
|
62
|
+
getConsentSettings(),
|
|
63
|
+
getTracking(),
|
|
64
|
+
getMenuSafe("main-menu"),
|
|
65
|
+
getMenuSafe("footer"),
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<html lang={STORE_LOCALE.code}>
|
|
70
|
+
<body>
|
|
71
|
+
{/* The store's own consent switch decides the default: a store
|
|
72
|
+
that collects consent starts every visitor denied until they
|
|
73
|
+
choose, a store with the banner off starts them granted. The
|
|
74
|
+
prop is required, so a layout cannot leave it out. */}
|
|
75
|
+
<ConsentInit required={consent.enabled} />
|
|
76
|
+
{/* Every marketing tag the store configured in the admin, mounted
|
|
77
|
+
from its own config: Meta, TikTok, ChatGPT, Google (GA4 + Ads
|
|
78
|
+
on one tag), GTM. Nothing to wire per vendor: saving the ids
|
|
79
|
+
under Settings, Integrations is the whole merchant-side act.
|
|
80
|
+
Order matters: ConsentInit sets the Consent Mode defaults
|
|
81
|
+
synchronously ABOVE this, so every tag below inherits the
|
|
82
|
+
gate. The config is a cached read handed over, so the tags
|
|
83
|
+
cost the page nothing at request time. */}
|
|
84
|
+
{tracking ? <StorefrontTags config={tracking} /> : null}
|
|
85
|
+
{/* Captures UTMs and the ad-click ids (ttclid / gclid / gbraid /
|
|
86
|
+
wbraid) that exist ONLY on an ad's landing URL — miss them
|
|
87
|
+
here and no platform can attribute the order to the click. */}
|
|
88
|
+
<TrackInit />
|
|
89
|
+
<PlatformInit storeId={store.id} />
|
|
90
|
+
<Providers consent={consent}>
|
|
91
|
+
<header className="border-b border-border">
|
|
92
|
+
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-6">
|
|
93
|
+
<Link href="/" className="font-semibold text-lg">
|
|
94
|
+
{store.name}
|
|
95
|
+
</Link>
|
|
96
|
+
<div className="flex items-center gap-6">
|
|
97
|
+
<MenuNav menu={mainMenu} />
|
|
98
|
+
<Link
|
|
99
|
+
href="/search"
|
|
100
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
101
|
+
>
|
|
102
|
+
Search
|
|
103
|
+
</Link>
|
|
104
|
+
{/*
|
|
105
|
+
The component workbench lives at /gallery in THIS app only
|
|
106
|
+
(the seed excludes it, src/lib/storefront/seed.ts), so the
|
|
107
|
+
shop's own nav never links to it: a merchant's store must
|
|
108
|
+
not carry a dead link.
|
|
109
|
+
*/}
|
|
110
|
+
{/* No `cart` prop: the badge counts the drawer's own cart. */}
|
|
111
|
+
<CartButtonClient />
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</header>
|
|
115
|
+
<main>{children}</main>
|
|
116
|
+
<footer className="border-t border-border mt-12">
|
|
117
|
+
<div className="max-w-7xl mx-auto px-4 py-8 text-sm text-muted-foreground flex items-center justify-between">
|
|
118
|
+
<span>{store.name}</span>
|
|
119
|
+
<MenuNav menu={footerMenu} />
|
|
120
|
+
</div>
|
|
121
|
+
</footer>
|
|
122
|
+
</Providers>
|
|
123
|
+
</body>
|
|
124
|
+
</html>
|
|
125
|
+
)
|
|
126
|
+
}
|