@forgecart/cli 2.202610052310.0 → 2.202610071755.0
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/package.json +1 -1
- package/templates/storefront-shadcn/.forgecartignore +2 -0
- package/templates/storefront-shadcn/Procfile +1 -0
- package/templates/storefront-shadcn/README.md +229 -0
- package/templates/storefront-shadcn/SEO-MIGRATION.md +708 -0
- package/templates/storefront-shadcn/components.json +21 -0
- package/templates/storefront-shadcn/next.config.js +105 -0
- package/templates/storefront-shadcn/package.json +39 -0
- package/templates/storefront-shadcn/postcss.config.js +5 -0
- package/templates/storefront-shadcn/src/app/%5F%5Ffc/identify/route.ts +205 -0
- package/templates/storefront-shadcn/src/app/%5F%5Ffc/track/route.ts +189 -0
- package/templates/storefront-shadcn/src/app/%5F%5Fforge_beacon/route.ts +87 -0
- package/templates/storefront-shadcn/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
- package/templates/storefront-shadcn/src/app/cart/page.tsx +53 -0
- package/templates/storefront-shadcn/src/app/checkout/page.tsx +53 -0
- package/templates/storefront-shadcn/src/app/error.tsx +23 -0
- package/templates/storefront-shadcn/src/app/global-error.tsx +23 -0
- package/templates/storefront-shadcn/src/app/globals.css +156 -0
- package/templates/storefront-shadcn/src/app/layout.tsx +185 -0
- package/templates/storefront-shadcn/src/app/page.tsx +217 -0
- package/templates/storefront-shadcn/src/app/pages/[slug]/not-found.tsx +24 -0
- package/templates/storefront-shadcn/src/app/pages/[slug]/page.tsx +115 -0
- package/templates/storefront-shadcn/src/app/ping/route.ts +21 -0
- package/templates/storefront-shadcn/src/app/products/[slug]/not-found.tsx +18 -0
- package/templates/storefront-shadcn/src/app/products/[slug]/page.tsx +317 -0
- package/templates/storefront-shadcn/src/app/products/page.tsx +107 -0
- package/templates/storefront-shadcn/src/app/register/page.tsx +54 -0
- package/templates/storefront-shadcn/src/app/reset-password/page.tsx +60 -0
- package/templates/storefront-shadcn/src/app/robots.ts +69 -0
- package/templates/storefront-shadcn/src/app/sitemap.ts +106 -0
- package/templates/storefront-shadcn/src/app/verify/page.tsx +157 -0
- package/templates/storefront-shadcn/src/components/CartView.tsx +333 -0
- package/templates/storefront-shadcn/src/components/ForgeErrorBeacon.tsx +102 -0
- package/templates/storefront-shadcn/src/components/ForgeTracker.tsx +479 -0
- package/templates/storefront-shadcn/src/components/ForgecartDesigner.tsx +43 -0
- package/templates/storefront-shadcn/src/components/Header.tsx +73 -0
- package/templates/storefront-shadcn/src/components/LanguageSwitcher.tsx +96 -0
- package/templates/storefront-shadcn/src/components/LocaleLink.tsx +49 -0
- package/templates/storefront-shadcn/src/components/ProductCard.tsx +59 -0
- package/templates/storefront-shadcn/src/components/ProductPurchase.tsx +235 -0
- package/templates/storefront-shadcn/src/components/account/AccountMessage.tsx +63 -0
- package/templates/storefront-shadcn/src/components/account/RegisterForm.tsx +257 -0
- package/templates/storefront-shadcn/src/components/account/RequestPasswordResetForm.tsx +93 -0
- package/templates/storefront-shadcn/src/components/account/ResetPasswordForm.tsx +163 -0
- package/templates/storefront-shadcn/src/components/checkout/AddressStep.tsx +271 -0
- package/templates/storefront-shadcn/src/components/checkout/CheckoutFlow.tsx +551 -0
- package/templates/storefront-shadcn/src/components/checkout/CheckoutGate.tsx +55 -0
- package/templates/storefront-shadcn/src/components/checkout/PaymentElementForm.tsx +140 -0
- package/templates/storefront-shadcn/src/components/checkout/PaymentFormEmbed.tsx +89 -0
- package/templates/storefront-shadcn/src/components/checkout/RatesStep.tsx +115 -0
- package/templates/storefront-shadcn/src/components/ui/alert.tsx +75 -0
- package/templates/storefront-shadcn/src/components/ui/badge.tsx +40 -0
- package/templates/storefront-shadcn/src/components/ui/button.tsx +64 -0
- package/templates/storefront-shadcn/src/components/ui/card.tsx +28 -0
- package/templates/storefront-shadcn/src/components/ui/input.tsx +26 -0
- package/templates/storefront-shadcn/src/components/ui/label.tsx +22 -0
- package/templates/storefront-shadcn/src/components/ui/native-select.tsx +27 -0
- package/templates/storefront-shadcn/src/components/ui/skeleton.tsx +21 -0
- package/templates/storefront-shadcn/src/components/ui/utils.ts +16 -0
- package/templates/storefront-shadcn/src/instrumentation.ts +109 -0
- package/templates/storefront-shadcn/src/lib/account/account-link.ts +76 -0
- package/templates/storefront-shadcn/src/lib/account/register-state.ts +133 -0
- package/templates/storefront-shadcn/src/lib/account/reset-password-state.ts +111 -0
- package/templates/storefront-shadcn/src/lib/account/verify-state.ts +56 -0
- package/templates/storefront-shadcn/src/lib/account-actions.ts +76 -0
- package/templates/storefront-shadcn/src/lib/account-session.ts +47 -0
- package/templates/storefront-shadcn/src/lib/action-result.ts +30 -0
- package/templates/storefront-shadcn/src/lib/asset-alt.ts +34 -0
- package/templates/storefront-shadcn/src/lib/backend-actions.ts +20 -0
- package/templates/storefront-shadcn/src/lib/backend-client.ts +47 -0
- package/templates/storefront-shadcn/src/lib/cart-context.tsx +236 -0
- package/templates/storefront-shadcn/src/lib/checkout-session.ts +185 -0
- package/templates/storefront-shadcn/src/lib/content/page-metadata.ts +113 -0
- package/templates/storefront-shadcn/src/lib/content/render-fields.tsx +256 -0
- package/templates/storefront-shadcn/src/lib/content/resolve-page.ts +143 -0
- package/templates/storefront-shadcn/src/lib/error-messages.ts +24 -0
- package/templates/storefront-shadcn/src/lib/experiments.ts +333 -0
- package/templates/storefront-shadcn/src/lib/forgecart.ts +464 -0
- package/templates/storefront-shadcn/src/lib/format.ts +89 -0
- package/templates/storefront-shadcn/src/lib/identify-forward.ts +152 -0
- package/templates/storefront-shadcn/src/lib/locale/channel-locales-loader.ts +169 -0
- package/templates/storefront-shadcn/src/lib/locale/channel-locales-map.ts +46 -0
- package/templates/storefront-shadcn/src/lib/locale/channel-locales.ts +191 -0
- package/templates/storefront-shadcn/src/lib/locale/grammar.ts +194 -0
- package/templates/storefront-shadcn/src/lib/locale/localized-path.ts +55 -0
- package/templates/storefront-shadcn/src/lib/locale/middleware-plan.ts +107 -0
- package/templates/storefront-shadcn/src/lib/locale/request-binding.ts +80 -0
- package/templates/storefront-shadcn/src/lib/locale/request-locale.ts +66 -0
- package/templates/storefront-shadcn/src/lib/marketing-params.ts +213 -0
- package/templates/storefront-shadcn/src/lib/money.ts +50 -0
- package/templates/storefront-shadcn/src/lib/seo/alternates.ts +123 -0
- package/templates/storefront-shadcn/src/lib/seo/json-ld.ts +266 -0
- package/templates/storefront-shadcn/src/lib/seo/metadata.ts +419 -0
- package/templates/storefront-shadcn/src/lib/seo/noindex.ts +218 -0
- package/templates/storefront-shadcn/src/lib/seo/public-origin.ts +166 -0
- package/templates/storefront-shadcn/src/lib/seo/redirect-plan.ts +86 -0
- package/templates/storefront-shadcn/src/lib/seo/resolve-path.ts +107 -0
- package/templates/storefront-shadcn/src/lib/seo/scaffolded-routes.ts +83 -0
- package/templates/storefront-shadcn/src/lib/seo/sidecar.ts +75 -0
- package/templates/storefront-shadcn/src/lib/seo/site-verification.ts +98 -0
- package/templates/storefront-shadcn/src/lib/seo/sitemap-cache.ts +114 -0
- package/templates/storefront-shadcn/src/lib/seo/sitemap-entries.ts +321 -0
- package/templates/storefront-shadcn/src/lib/session-actions.ts +61 -0
- package/templates/storefront-shadcn/src/lib/session-cookies.ts +98 -0
- package/templates/storefront-shadcn/src/lib/shop-config.ts +51 -0
- package/templates/storefront-shadcn/src/lib/shop-session.ts +151 -0
- package/templates/storefront-shadcn/src/lib/track-forward.ts +200 -0
- package/templates/storefront-shadcn/src/lib/uuid.ts +19 -0
- package/templates/storefront-shadcn/src/middleware.ts +379 -0
- package/templates/storefront-shadcn/src/seo/redirects.ts +44 -0
- package/templates/storefront-shadcn/src/server/app.module.ts +18 -0
- package/templates/storefront-shadcn/src/server/backend-api.ts +26 -0
- package/templates/storefront-shadcn/src/server/backend-method.decorator.ts +23 -0
- package/templates/storefront-shadcn/src/server/bootstrap.ts +122 -0
- package/templates/storefront-shadcn/src/server/customer-extras/customer-extras.module.ts +13 -0
- package/templates/storefront-shadcn/src/server/customer-extras/service/customer-extras.service.ts +58 -0
- package/templates/storefront-shadcn/src/server/customer-extras/type/customer-extras.types.ts +11 -0
- package/templates/storefront-shadcn/src/server/forge/live-revision.ts +158 -0
- package/templates/storefront-shadcn/src/server/forgecart/forgecart-client.factory.ts +69 -0
- package/templates/storefront-shadcn/src/server/forgecart/forgecart.module.ts +9 -0
- package/templates/storefront-shadcn/src/server/runner.ts +90 -0
- package/templates/storefront-shadcn/src/server/types.ts +36 -0
- package/templates/storefront-shadcn/tsconfig.json +25 -0
- package/templates/storefront-shadcn-sdk-floor.json +1174 -0
- package/templates/template-set.json +10 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
import { getBackend } from '../../../../server/bootstrap';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DEV-ONLY registry listing — the agent's verification surface for backend
|
|
7
|
+
* features. After authoring a method, the agent probes this route and
|
|
8
|
+
* confirms the new name is listed; a boot failure (the api-map drift check)
|
|
9
|
+
* surfaces here as a 500 carrying the actionable message.
|
|
10
|
+
*
|
|
11
|
+
* Gated on `NODE_ENV === 'development'`: the literal check is inlined by
|
|
12
|
+
* the bundler, so a production build answers 404 and dead-code-eliminates
|
|
13
|
+
* the body. Method NAMES are the only thing exposed — never inputs,
|
|
14
|
+
* outputs, or configuration.
|
|
15
|
+
*/
|
|
16
|
+
export async function GET(): Promise<NextResponse> {
|
|
17
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
18
|
+
return NextResponse.json({ error: 'not found' }, { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const { registry } = await getBackend();
|
|
22
|
+
return NextResponse.json({ methods: [...registry.keys()].sort() });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
25
|
+
return NextResponse.json({ error: reason }, { status: 500 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { connection } from 'next/server';
|
|
3
|
+
import { Suspense } from 'react';
|
|
4
|
+
|
|
5
|
+
import { CartView } from '../../components/CartView';
|
|
6
|
+
import { Skeleton } from '../../components/ui/skeleton';
|
|
7
|
+
import { getChannelSellingPlanGroups } from '../../lib/forgecart';
|
|
8
|
+
import { getRequestLocale } from '../../lib/locale/request-binding';
|
|
9
|
+
import { routeRobots } from '../../lib/seo/noindex';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Never indexable: a cart is per-shopper state, identical in copy for everyone
|
|
13
|
+
* and meaningless to a visitor arriving from a search result.
|
|
14
|
+
*/
|
|
15
|
+
export function generateMetadata(): Metadata {
|
|
16
|
+
return { robots: routeRobots('/cart') };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Cart route. The shopper's cart itself lives on their OWN websocket
|
|
21
|
+
* (`CartView` hydrates it client-side); the server's only read here is the
|
|
22
|
+
* PUBLIC channel subscription plans off the anonymous channel singleton —
|
|
23
|
+
* streamed as a hole while the shell renders.
|
|
24
|
+
*/
|
|
25
|
+
export default function CartPage() {
|
|
26
|
+
return (
|
|
27
|
+
<Suspense fallback={<CartRouteSkeleton />}>
|
|
28
|
+
<CartWithPlans />
|
|
29
|
+
</Suspense>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function CartWithPlans() {
|
|
34
|
+
await connection();
|
|
35
|
+
const { binding } = await getRequestLocale();
|
|
36
|
+
// Channel-wide subscription plans are public channel data — the one
|
|
37
|
+
// server-fetched input; the "Subscribe to your whole order" box renders
|
|
38
|
+
// only when there are any.
|
|
39
|
+
const channelGroups = await getChannelSellingPlanGroups();
|
|
40
|
+
return <CartView channelGroups={channelGroups} locale={binding} />;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function CartRouteSkeleton() {
|
|
44
|
+
return (
|
|
45
|
+
<div className="space-y-8" aria-busy="true" aria-label="Loading your cart">
|
|
46
|
+
<h1 className="text-3xl font-bold tracking-tight">Your cart</h1>
|
|
47
|
+
<div className="grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
|
|
48
|
+
<Skeleton className="h-64 w-full" />
|
|
49
|
+
<Skeleton className="h-80 w-full" />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { connection } from 'next/server';
|
|
3
|
+
import { Suspense } from 'react';
|
|
4
|
+
|
|
5
|
+
import { CheckoutGate } from '../../components/checkout/CheckoutGate';
|
|
6
|
+
import { Skeleton } from '../../components/ui/skeleton';
|
|
7
|
+
import { getAvailableCountries } from '../../lib/forgecart';
|
|
8
|
+
import { getRequestLocale } from '../../lib/locale/request-binding';
|
|
9
|
+
import { routeRobots } from '../../lib/seo/noindex';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Never indexable: checkout is a funnel step, not a destination, and every
|
|
13
|
+
* nested step inherits the decision through `NOINDEX_PATHS`.
|
|
14
|
+
*/
|
|
15
|
+
export function generateMetadata(): Metadata {
|
|
16
|
+
return { robots: routeRobots('/checkout') };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checkout entry. The server's only job here is the PUBLIC prefetch — the
|
|
21
|
+
* channel's shipping countries off the anonymous channel singleton, streamed
|
|
22
|
+
* as a hole. The session work (the order itself, every
|
|
23
|
+
* checkout mutation) lives on the shopper's websocket: `CheckoutGate`
|
|
24
|
+
* hydrates the live cart client-side and mounts the flow, or shows the
|
|
25
|
+
* honest empty state.
|
|
26
|
+
*/
|
|
27
|
+
export default function CheckoutPage() {
|
|
28
|
+
return (
|
|
29
|
+
<div className="mx-auto max-w-2xl space-y-6">
|
|
30
|
+
<h1 className="text-2xl font-bold tracking-tight">Checkout</h1>
|
|
31
|
+
<Suspense fallback={<CheckoutRouteSkeleton />}>
|
|
32
|
+
<CheckoutWithCountries />
|
|
33
|
+
</Suspense>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function CheckoutWithCountries() {
|
|
39
|
+
await connection();
|
|
40
|
+
const { binding } = await getRequestLocale();
|
|
41
|
+
const countries = await getAvailableCountries();
|
|
42
|
+
return <CheckoutGate countries={countries} locale={binding} />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function CheckoutRouteSkeleton() {
|
|
46
|
+
return (
|
|
47
|
+
<div className="space-y-4" aria-busy="true" aria-label="Loading checkout">
|
|
48
|
+
<Skeleton className="h-10 w-full" />
|
|
49
|
+
<Skeleton className="h-40 w-full" />
|
|
50
|
+
<Skeleton className="h-10 w-40" />
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '../components/ui/button';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Route-level backstop boundary. The action layer never throws (typed
|
|
7
|
+
* `ActionResult` envelopes end at inline UI), so landing here means a RENDER
|
|
8
|
+
* failed — e.g. a Server Component read against an unreachable shop API.
|
|
9
|
+
* Recovery is a re-render via `reset()`.
|
|
10
|
+
*/
|
|
11
|
+
export default function RouteError({ reset }: { error: Error; reset: () => void }) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="mx-auto max-w-xl space-y-4 py-16 text-center">
|
|
14
|
+
<h1 className="text-2xl font-bold tracking-tight text-foreground">Something went wrong</h1>
|
|
15
|
+
<p className="text-muted-foreground">
|
|
16
|
+
The store could not load this page. Please try again in a moment.
|
|
17
|
+
</p>
|
|
18
|
+
<Button type="button" onClick={() => reset()}>
|
|
19
|
+
Try again
|
|
20
|
+
</Button>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Root backstop: replaces the whole document when the root layout itself
|
|
5
|
+
* fails to render. Must own its own <html>/<body> — the layout that normally
|
|
6
|
+
* provides them is the thing that crashed. Styled inline (no globals.css —
|
|
7
|
+
* that import lives in the crashed layout).
|
|
8
|
+
*/
|
|
9
|
+
export default function GlobalError({ reset }: { error: Error; reset: () => void }) {
|
|
10
|
+
return (
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<body
|
|
13
|
+
style={{ fontFamily: 'system-ui, sans-serif', padding: '4rem 1rem', textAlign: 'center' }}
|
|
14
|
+
>
|
|
15
|
+
<h1>Something went wrong</h1>
|
|
16
|
+
<p>The store is temporarily unavailable. Please try again in a moment.</p>
|
|
17
|
+
<button type="button" onClick={() => reset()} style={{ padding: '0.5rem 1.25rem' }}>
|
|
18
|
+
Try again
|
|
19
|
+
</button>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* STOREFRONT THEME — the single brand mutation point (design-system contract)
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
7
|
+
*
|
|
8
|
+
* Every component in this template speaks SEMANTIC shadcn/ui tokens (bg-card,
|
|
9
|
+
* text-muted-foreground, border-input, the Button / Badge / Alert variants,
|
|
10
|
+
* …), so the `:root` block below is the ONE place a brand lives. Restyling
|
|
11
|
+
* the storefront = editing those values — nothing else. Never scatter raw
|
|
12
|
+
* palette utilities (bg-gray-900, text-white, #hex) through components; if a
|
|
13
|
+
* component needs a color, it needs a TOKEN: declared once in `:root` and
|
|
14
|
+
* exposed to Tailwind through `@theme inline`, as the status tokens are.
|
|
15
|
+
*
|
|
16
|
+
* The daisyUI storefront and this one are rows of one template set. The table
|
|
17
|
+
* is how every daisyUI theme token reads here — the same brand decision under
|
|
18
|
+
* its shadcn/ui name:
|
|
19
|
+
*
|
|
20
|
+
* daisyUI theme token shadcn/ui token (this file)
|
|
21
|
+
* ─────────────────────────── ──────────────────────────────────────────
|
|
22
|
+
* --color-base-100 --card, --popover
|
|
23
|
+
* --color-base-200 --background
|
|
24
|
+
* --color-base-300 --border, --input
|
|
25
|
+
* --color-base-content --foreground (+ --card-foreground,
|
|
26
|
+
* --popover-foreground)
|
|
27
|
+
* --color-primary(-content) --primary(-foreground)
|
|
28
|
+
* --color-secondary(-content) --secondary(-foreground)
|
|
29
|
+
* --color-accent(-content) --accent(-foreground)
|
|
30
|
+
* --color-neutral(-content) --muted(-foreground)
|
|
31
|
+
* --color-error --destructive
|
|
32
|
+
* --color-success(-content) --success(-foreground) declared by this variant
|
|
33
|
+
* --color-warning(-content) --warning(-foreground) declared by this variant
|
|
34
|
+
* --color-info(-content) --info(-foreground) declared by this variant
|
|
35
|
+
* --radius-selector --radius-sm
|
|
36
|
+
* --radius-field --radius-md
|
|
37
|
+
* --radius-box --radius-lg (= --radius)
|
|
38
|
+
* --font-sans (@theme) --font-sans (@theme inline)
|
|
39
|
+
*
|
|
40
|
+
* shadcn/ui names no success, warning or info color, so this variant declares
|
|
41
|
+
* them beside --destructive, each with the -foreground that stays legible on
|
|
42
|
+
* it (the Alert's status variants fill with them). --ring has no daisyUI
|
|
43
|
+
* counterpart — daisyUI draws a focus outline in the control's own color — so
|
|
44
|
+
* it takes the primary ink.
|
|
45
|
+
*
|
|
46
|
+
* The baseline is the daisyUI storefront's own, deliberately UNBRANDED
|
|
47
|
+
* monochrome (near-black on soft gray), carried value for value through the
|
|
48
|
+
* table — with one deliberate exception, the muted pair. daisyUI's neutral is
|
|
49
|
+
* a high-contrast FILL; shadcn/ui's muted pair is the SUBDUED one every
|
|
50
|
+
* vendored component reads for quiet surfaces and secondary text (bg-muted,
|
|
51
|
+
* text-muted-foreground — an input's placeholder among them). The baseline
|
|
52
|
+
* keeps that role: --muted is a soft gray surface, and --muted-foreground the
|
|
53
|
+
* mid-gray ink the daisyUI template writes as text-base-content/60. Accent
|
|
54
|
+
* keeps daisyUI's dark highlight, which is also what a ghost or outline
|
|
55
|
+
* button turns on hover.
|
|
56
|
+
*
|
|
57
|
+
* Radius: daisyUI's selector and field radii are both 0.375rem and its box
|
|
58
|
+
* radius 0.5rem, so --radius is the box radius and the sm/md steps sit one
|
|
59
|
+
* notch below it. A brand font goes in `@theme inline`: swap the --font-sans
|
|
60
|
+
* stack (and add the font's `@font-face`/import above it).
|
|
61
|
+
*/
|
|
62
|
+
:root {
|
|
63
|
+
/* Base — white surfaces over a soft-gray substrate, near-black text. */
|
|
64
|
+
--background: #f9fafb;
|
|
65
|
+
--foreground: #111827;
|
|
66
|
+
--card: #ffffff;
|
|
67
|
+
--card-foreground: #111827;
|
|
68
|
+
--popover: #ffffff;
|
|
69
|
+
--popover-foreground: #111827;
|
|
70
|
+
--border: #e5e7eb;
|
|
71
|
+
--input: #e5e7eb;
|
|
72
|
+
|
|
73
|
+
/* Actions — unbranded monochrome. */
|
|
74
|
+
--primary: #111827;
|
|
75
|
+
--primary-foreground: #ffffff;
|
|
76
|
+
--secondary: #374151;
|
|
77
|
+
--secondary-foreground: #ffffff;
|
|
78
|
+
--accent: #1f2937;
|
|
79
|
+
--accent-foreground: #ffffff;
|
|
80
|
+
--muted: #f3f4f6;
|
|
81
|
+
--muted-foreground: #6b7280;
|
|
82
|
+
--ring: #111827;
|
|
83
|
+
|
|
84
|
+
/* Status — conventional semantics, not brand. */
|
|
85
|
+
--destructive: #dc2626;
|
|
86
|
+
--success: #16a34a;
|
|
87
|
+
--success-foreground: #ffffff;
|
|
88
|
+
--warning: #d97706;
|
|
89
|
+
--warning-foreground: #ffffff;
|
|
90
|
+
--info: #2563eb;
|
|
91
|
+
--info-foreground: #ffffff;
|
|
92
|
+
|
|
93
|
+
/* Shape — the daisyUI storefront's box radius. */
|
|
94
|
+
--radius: 0.5rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* The tokens as Tailwind theme values: bg-card, text-muted-foreground, rounded-md, font-sans, … */
|
|
98
|
+
@theme inline {
|
|
99
|
+
--color-background: var(--background);
|
|
100
|
+
--color-foreground: var(--foreground);
|
|
101
|
+
--color-card: var(--card);
|
|
102
|
+
--color-card-foreground: var(--card-foreground);
|
|
103
|
+
--color-popover: var(--popover);
|
|
104
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
105
|
+
--color-primary: var(--primary);
|
|
106
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
107
|
+
--color-secondary: var(--secondary);
|
|
108
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
109
|
+
--color-muted: var(--muted);
|
|
110
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
111
|
+
--color-accent: var(--accent);
|
|
112
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
113
|
+
--color-destructive: var(--destructive);
|
|
114
|
+
--color-border: var(--border);
|
|
115
|
+
--color-input: var(--input);
|
|
116
|
+
--color-ring: var(--ring);
|
|
117
|
+
--color-success: var(--success);
|
|
118
|
+
--color-success-foreground: var(--success-foreground);
|
|
119
|
+
--color-warning: var(--warning);
|
|
120
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
121
|
+
--color-info: var(--info);
|
|
122
|
+
--color-info-foreground: var(--info-foreground);
|
|
123
|
+
|
|
124
|
+
--radius-sm: calc(var(--radius) - 2px);
|
|
125
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
126
|
+
--radius-lg: var(--radius);
|
|
127
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
128
|
+
|
|
129
|
+
/* Brand typography mutation point — swap the stack to give the store a font. */
|
|
130
|
+
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
|
131
|
+
"Segoe UI Symbol", "Noto Color Emoji";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* The shadcn/ui base layer: token-colored borders and focus outlines, the page on --background. */
|
|
135
|
+
@layer base {
|
|
136
|
+
* {
|
|
137
|
+
@apply border-border outline-ring/50;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
body {
|
|
141
|
+
@apply bg-background text-foreground;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* Base document behavior (structural — NOT part of the theme contract).
|
|
147
|
+
* A horizontal touchpad glide must never trigger the browser's swipe-back
|
|
148
|
+
* navigation: mid-checkout that discards the shopper's in-progress state
|
|
149
|
+
* (the refresh-resume path recovers the payment step, but preventing the
|
|
150
|
+
* accidental navigation is the real fix). The back button and keyboard
|
|
151
|
+
* navigation are untouched — only the overscroll gesture is consumed.
|
|
152
|
+
*/
|
|
153
|
+
html,
|
|
154
|
+
body {
|
|
155
|
+
overscroll-behavior-x: none;
|
|
156
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { notFound, permanentRedirect } from 'next/navigation';
|
|
3
|
+
import { Suspense, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { ForgeErrorBeacon } from '../components/ForgeErrorBeacon';
|
|
6
|
+
import { ForgeTracker } from '../components/ForgeTracker';
|
|
7
|
+
import { ForgecartDesigner } from '../components/ForgecartDesigner';
|
|
8
|
+
import { Header } from '../components/Header';
|
|
9
|
+
import { LocaleLink } from '../components/LocaleLink';
|
|
10
|
+
import { CartProvider } from '../lib/cart-context';
|
|
11
|
+
import { getRequestLocale } from '../lib/locale/request-binding';
|
|
12
|
+
import { shellMetadata } from '../lib/seo/metadata';
|
|
13
|
+
import { getSiteVerifications } from '../lib/seo/site-verification';
|
|
14
|
+
import { getBrowserShopConfig } from '../lib/shop-config';
|
|
15
|
+
import { FORGE_REVISION_META, resolveLiveRevision } from '../server/forge/live-revision';
|
|
16
|
+
|
|
17
|
+
import './globals.css';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The deployment-wide metadata floor.
|
|
21
|
+
*
|
|
22
|
+
* Resolved per REQUEST rather than declared as a static `metadata` object,
|
|
23
|
+
* because everything in it arrives in the environment of the running server
|
|
24
|
+
* rather than of the machine that built the bundle: the public origin from
|
|
25
|
+
* `.env` (see `lib/seo/public-origin.ts`) and the shop name from the channel
|
|
26
|
+
* itself. A build-time answer would bake the builder's empty environment into
|
|
27
|
+
* every store shipped from that image.
|
|
28
|
+
*
|
|
29
|
+
* The title is the STORE's name, so a merchant who scaffolds a page and writes
|
|
30
|
+
* no metadata gets their own store in the browser tab instead of the word
|
|
31
|
+
* `Storefront` — which is what every page of every ForgeCart store displayed
|
|
32
|
+
* before this. Route-specific claims live on the routes; this is only the
|
|
33
|
+
* floor, and it deliberately carries no canonical (it does not know which
|
|
34
|
+
* route it is wrapping).
|
|
35
|
+
*
|
|
36
|
+
* It reads the SAME resolution the layout body reads below — React's request
|
|
37
|
+
* cache makes `getRequestLocale()` one channel read per request no matter how
|
|
38
|
+
* many callers there are, so the floor costs nothing extra.
|
|
39
|
+
*
|
|
40
|
+
* The site-verification placements (#1530) are read on the same request cache
|
|
41
|
+
* and handed to the SAME composer, so `lib/seo/metadata.ts` stays the only
|
|
42
|
+
* thing in this template that turns a placement into a `<meta>`: the
|
|
43
|
+
* alternative — a hand-written tag in the markup below — would be a second
|
|
44
|
+
* emitter with its own idea of when a placement is servable. They ride the
|
|
45
|
+
* FLOOR rather than a route, because the verifier chooses which page it fetches
|
|
46
|
+
* and a tag on only some of them is a verification that works until it does not.
|
|
47
|
+
*
|
|
48
|
+
* It also carries the DEV-ONLY revision stamp (#847). The tag has to be written
|
|
49
|
+
* where the page is RENDERED, because that is the only moment that knows which
|
|
50
|
+
* tree the shopper is actually looking at: the browser beacon reads it back at
|
|
51
|
+
* throw time, so a promote landing between serve and throw can no longer make
|
|
52
|
+
* the report blame the new revision for the old one's crash. It rides
|
|
53
|
+
* `generateMetadata` rather than a `<meta>` in the JSX below because arbitrary
|
|
54
|
+
* head tags are what `other` is for, and the shell's own metadata contract then
|
|
55
|
+
* stays untouched — the stamp is spread ON TOP of `shellMetadata`'s answer and
|
|
56
|
+
* adds a key no SEO surface reads.
|
|
57
|
+
*
|
|
58
|
+
* Dev-gated at this call site, like every other file in the beacon path: the
|
|
59
|
+
* literal `NODE_ENV` check is what lets a production build eliminate the
|
|
60
|
+
* resolution entirely, so a deployed storefront emits no tag and leaks no
|
|
61
|
+
* revision. Unresolvable is a NORMAL answer and means no tag — the report then
|
|
62
|
+
* arrives untagged and the receiver keeps its existing behaviour.
|
|
63
|
+
*/
|
|
64
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
65
|
+
const { shopName, channelResolved } = await getRequestLocale();
|
|
66
|
+
const siteVerifications = await getSiteVerifications();
|
|
67
|
+
const shell = shellMetadata({ shopName, channelResolved, siteVerifications });
|
|
68
|
+
if (process.env.NODE_ENV !== 'development') return shell;
|
|
69
|
+
const revision = resolveLiveRevision();
|
|
70
|
+
if (revision === null) return shell;
|
|
71
|
+
// MERGED into the shell's `other`, never written over it: that key carries the
|
|
72
|
+
// site-verification placements (#1530), and a storefront pod serves through
|
|
73
|
+
// `npm run dev`, so an overwrite here would strip the provider's tag from
|
|
74
|
+
// every page its verifier can fetch.
|
|
75
|
+
return { ...shell, other: { ...shell.other, [FORGE_REVISION_META]: revision } };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The root layout — DYNAMIC on every route, by design (D8).
|
|
80
|
+
*
|
|
81
|
+
* It reads the request locale to write `<html lang>`, and `lang` is an
|
|
82
|
+
* attribute on the document root: structurally outside every Suspense
|
|
83
|
+
* boundary, so it cannot come from a streamed hole. That single requirement
|
|
84
|
+
* decides the rendering model for the whole storefront, and `next.config.js`
|
|
85
|
+
* carries the reasoning — Cache Components is off because the escape hatch it
|
|
86
|
+
* would force (a `<Suspense>` above `<body>`) is exactly what makes the two
|
|
87
|
+
* throws below unable to set a status.
|
|
88
|
+
*
|
|
89
|
+
* Those throws are the locale contract's teeth. Raised HERE, in the shell,
|
|
90
|
+
* they reach the one place Next assigns a status: `permanentRedirect` gives a
|
|
91
|
+
* real 308 with a `Location`, and `notFound` a real 404. Raised from inside
|
|
92
|
+
* any boundary they would resolve as client-rendered content instead, and the
|
|
93
|
+
* response would be a 200 carrying a 404 page — a soft-404, which a crawler
|
|
94
|
+
* indexes rather than drops.
|
|
95
|
+
*
|
|
96
|
+
* One consequence worth knowing: on `notFound()` Next discards the app tree
|
|
97
|
+
* and seeds a bare error document, so `app/not-found.tsx` reaches the shopper
|
|
98
|
+
* on client hydration rather than in the SSR body. The STATUS is correct
|
|
99
|
+
* either way, which is what the crawler consumes.
|
|
100
|
+
*
|
|
101
|
+
* The cart still hydrates CLIENT-side over the shopper's own websocket
|
|
102
|
+
* (`CartProvider` + `shop-session.ts`), armed with the channel coordinates
|
|
103
|
+
* serialized here (public storefront token — the admin secret never takes this
|
|
104
|
+
* path).
|
|
105
|
+
*/
|
|
106
|
+
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
107
|
+
const { decision, binding, languageCodes } = await getRequestLocale();
|
|
108
|
+
if (decision.kind === 'redirect') permanentRedirect(decision.to);
|
|
109
|
+
if (decision.kind === 'notFound') notFound();
|
|
110
|
+
|
|
111
|
+
// The shopper's own socket is opened CLIENT-side, so it cannot read the
|
|
112
|
+
// request headers the server resolved the locale from. It gets the resolved
|
|
113
|
+
// language here, which keeps one source of truth — the URL — for both the
|
|
114
|
+
// server's per-locale clients and the browser's session client. Without it
|
|
115
|
+
// the cart would answer in the channel default on a German page.
|
|
116
|
+
const shopConfig = getBrowserShopConfig(binding.locale);
|
|
117
|
+
return (
|
|
118
|
+
<html lang={binding.locale}>
|
|
119
|
+
<body className="min-h-screen bg-background font-sans text-foreground antialiased">
|
|
120
|
+
<CartProvider shopConfig={shopConfig}>
|
|
121
|
+
<Header locale={binding} languageCodes={languageCodes} />
|
|
122
|
+
<main className="mx-auto max-w-6xl px-4 py-10">{children}</main>
|
|
123
|
+
<footer className="mt-8 border-t bg-card px-6 py-8 text-sm text-muted-foreground sm:px-10">
|
|
124
|
+
<div className="flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
|
|
125
|
+
<aside>
|
|
126
|
+
<p className="font-medium text-foreground">Storefront</p>
|
|
127
|
+
<p>Powered by ForgeCart</p>
|
|
128
|
+
</aside>
|
|
129
|
+
<nav aria-label="Footer">
|
|
130
|
+
<div className="grid grid-flow-col gap-4">
|
|
131
|
+
<LocaleLink
|
|
132
|
+
href="/"
|
|
133
|
+
locale={binding}
|
|
134
|
+
className="underline-offset-4 hover:text-foreground hover:underline"
|
|
135
|
+
>
|
|
136
|
+
Home
|
|
137
|
+
</LocaleLink>
|
|
138
|
+
<LocaleLink
|
|
139
|
+
href="/products"
|
|
140
|
+
locale={binding}
|
|
141
|
+
className="underline-offset-4 hover:text-foreground hover:underline"
|
|
142
|
+
>
|
|
143
|
+
Products
|
|
144
|
+
</LocaleLink>
|
|
145
|
+
<LocaleLink
|
|
146
|
+
href="/cart"
|
|
147
|
+
locale={binding}
|
|
148
|
+
className="underline-offset-4 hover:text-foreground hover:underline"
|
|
149
|
+
>
|
|
150
|
+
Cart
|
|
151
|
+
</LocaleLink>
|
|
152
|
+
{/* The only affordance for `/register` (#1471). It sits in the
|
|
153
|
+
footer rather than the header nav on purpose: the template
|
|
154
|
+
ships no sign-in surface yet, and an account entry in the
|
|
155
|
+
primary nav would advertise an account AREA that does not
|
|
156
|
+
exist. A merchant who adds sign-in moves it up. */}
|
|
157
|
+
<LocaleLink
|
|
158
|
+
href="/register"
|
|
159
|
+
locale={binding}
|
|
160
|
+
className="underline-offset-4 hover:text-foreground hover:underline"
|
|
161
|
+
>
|
|
162
|
+
Create account
|
|
163
|
+
</LocaleLink>
|
|
164
|
+
</div>
|
|
165
|
+
</nav>
|
|
166
|
+
</div>
|
|
167
|
+
</footer>
|
|
168
|
+
</CartProvider>
|
|
169
|
+
{/* Invisible client plumbing. The tracker reads usePathname(), which
|
|
170
|
+
needs a boundary of its own — null fallback, these render nothing.
|
|
171
|
+
This must stay INSIDE <body>: a boundary above it would make the
|
|
172
|
+
shell resumable and cost the statuses above. */}
|
|
173
|
+
<Suspense fallback={null}>
|
|
174
|
+
<ForgecartDesigner />
|
|
175
|
+
<ForgeErrorBeacon />
|
|
176
|
+
<ForgeTracker
|
|
177
|
+
enabled={Boolean(
|
|
178
|
+
process.env.FORGECART_SHOP_API_URL && process.env.FORGECART_CHANNEL_TOKEN,
|
|
179
|
+
)}
|
|
180
|
+
/>
|
|
181
|
+
</Suspense>
|
|
182
|
+
</body>
|
|
183
|
+
</html>
|
|
184
|
+
);
|
|
185
|
+
}
|