@forgecart/cli 2.202610052310.0 → 2.202610060357.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,217 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { connection } from 'next/server';
|
|
3
|
+
import { Suspense } from 'react';
|
|
4
|
+
import { cache } from 'react';
|
|
5
|
+
|
|
6
|
+
import { LocaleLink } from '../components/LocaleLink';
|
|
7
|
+
import { ProductCard } from '../components/ProductCard';
|
|
8
|
+
import { buttonVariants } from '../components/ui/button';
|
|
9
|
+
import { Skeleton } from '../components/ui/skeleton';
|
|
10
|
+
import { cn } from '../components/ui/utils';
|
|
11
|
+
import { getAssetAlt } from '../lib/asset-alt';
|
|
12
|
+
import { getVariant } from '../lib/experiments';
|
|
13
|
+
import { getFeaturedProducts } from '../lib/forgecart';
|
|
14
|
+
import type { LocaleBinding } from '../lib/locale/localized-path';
|
|
15
|
+
import { getRequestLocale } from '../lib/locale/request-binding';
|
|
16
|
+
import { staticPathsByLocale } from '../lib/seo/alternates';
|
|
17
|
+
import { routeMetadata } from '../lib/seo/metadata';
|
|
18
|
+
import type { RouteSearchParams } from '../lib/seo/noindex';
|
|
19
|
+
|
|
20
|
+
// The data sections below are per-request holes streamed inside their Suspense
|
|
21
|
+
// boundaries. The page itself is dynamic like every route (the root layout
|
|
22
|
+
// reads the request locale — see next.config.js).
|
|
23
|
+
|
|
24
|
+
/** One featured read per request, shared by both holes (React request cache). */
|
|
25
|
+
const featured = cache(async () => getFeaturedProducts(4));
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The landing surface is indexable — unless the URL carries query state
|
|
29
|
+
* (an experiment pin, a preview) that makes it a VIEW of the page competing
|
|
30
|
+
* with the page's own address.
|
|
31
|
+
*
|
|
32
|
+
* `title: null` because the store's landing page has no name of its own: it IS
|
|
33
|
+
* the store, so it takes the shop name alone rather than composing something
|
|
34
|
+
* like `Home | Acme`. The description is the shell's, inherited by omission.
|
|
35
|
+
*/
|
|
36
|
+
export async function generateMetadata({
|
|
37
|
+
searchParams,
|
|
38
|
+
}: {
|
|
39
|
+
searchParams: Promise<RouteSearchParams>;
|
|
40
|
+
}): Promise<Metadata> {
|
|
41
|
+
const [resolvedSearchParams, { binding, languageCodes, shopName, channelResolved }] =
|
|
42
|
+
await Promise.all([searchParams, getRequestLocale()]);
|
|
43
|
+
return routeMetadata({
|
|
44
|
+
binding,
|
|
45
|
+
shopName,
|
|
46
|
+
channelResolved,
|
|
47
|
+
// The canonical is built from the BASE path, so `/?fc-exp=hero:b` declares
|
|
48
|
+
// `/` as its address — the query-state copy points at the page it is a view
|
|
49
|
+
// of, which is the whole reason those URLs are noindex rather than ignored.
|
|
50
|
+
pathname: '/',
|
|
51
|
+
pathsByLocale: staticPathsByLocale(languageCodes, '/'),
|
|
52
|
+
searchParams: resolvedSearchParams,
|
|
53
|
+
title: null,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Home — the store's landing surface (Blueprint `pages/product-landing-page`):
|
|
59
|
+
* a product-led hero with ONE dominant conversion action, an editorial
|
|
60
|
+
* featured grid (first product large — asymmetry, not a uniform tile wall),
|
|
61
|
+
* and a closing conversion band. Every color is a semantic token; the brand
|
|
62
|
+
* lives in the `:root` block of globals.css alone.
|
|
63
|
+
*/
|
|
64
|
+
export default async function HomePage() {
|
|
65
|
+
const { binding } = await getRequestLocale();
|
|
66
|
+
return (
|
|
67
|
+
<div className="space-y-16">
|
|
68
|
+
<Suspense fallback={<HeroSkeleton />}>
|
|
69
|
+
<Hero locale={binding} />
|
|
70
|
+
</Suspense>
|
|
71
|
+
|
|
72
|
+
<section aria-labelledby="featured-heading">
|
|
73
|
+
<div className="mb-6 flex items-baseline justify-between gap-4">
|
|
74
|
+
<h2 id="featured-heading" className="text-2xl font-semibold tracking-tight">
|
|
75
|
+
Featured
|
|
76
|
+
</h2>
|
|
77
|
+
<LocaleLink
|
|
78
|
+
href="/products"
|
|
79
|
+
locale={binding}
|
|
80
|
+
className="text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
|
|
81
|
+
>
|
|
82
|
+
View all products
|
|
83
|
+
</LocaleLink>
|
|
84
|
+
</div>
|
|
85
|
+
<Suspense fallback={<GridSkeleton />}>
|
|
86
|
+
<FeaturedGrid locale={binding} />
|
|
87
|
+
</Suspense>
|
|
88
|
+
</section>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The experiment-split hero (design doc F-B): variants are code branches
|
|
95
|
+
* keyed on `variantKey`. `hero-headline` is the template's reference
|
|
96
|
+
* experiment — create one with that key (variants `control` + `b`) in the
|
|
97
|
+
* dashboard and this hero starts splitting; until then `variantKey` is null
|
|
98
|
+
* and the default copy renders. `data-fc-variant` mirrors the assignment for
|
|
99
|
+
* debugging/verification; exposure tracking happens server-side.
|
|
100
|
+
*/
|
|
101
|
+
async function Hero({ locale }: { locale: LocaleBinding }) {
|
|
102
|
+
// Request-time hole: streamed inside its Suspense boundary.
|
|
103
|
+
await connection();
|
|
104
|
+
const [products, hero] = await Promise.all([featured(), getVariant('hero-headline')]);
|
|
105
|
+
const heroProduct = products.find((product) => product.featuredAsset) ?? null;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<section
|
|
109
|
+
data-fc-variant={hero.variantKey ?? 'default'}
|
|
110
|
+
className="overflow-hidden rounded-xl border bg-card text-card-foreground shadow-sm"
|
|
111
|
+
>
|
|
112
|
+
<div className="flex w-full flex-col gap-10 p-8 lg:flex-row lg:items-center lg:p-12">
|
|
113
|
+
<div className="max-w-xl lg:flex-1">
|
|
114
|
+
<h1 className="text-4xl font-bold tracking-tight text-balance sm:text-5xl">
|
|
115
|
+
Welcome to the shop
|
|
116
|
+
</h1>
|
|
117
|
+
<p className="mt-4 text-muted-foreground text-pretty">
|
|
118
|
+
{hero.variantKey === 'b'
|
|
119
|
+
? 'Fresh picks, fair prices — see what everyone is adding to their carts.'
|
|
120
|
+
: 'This storefront is served by a ForgeCart channel. Browse the catalog and add products to your cart.'}
|
|
121
|
+
</p>
|
|
122
|
+
<div className="mt-8">
|
|
123
|
+
<LocaleLink href="/products" locale={locale} className={buttonVariants({ size: 'lg' })}>
|
|
124
|
+
Shop all products
|
|
125
|
+
</LocaleLink>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
{heroProduct?.featuredAsset && (
|
|
129
|
+
<LocaleLink
|
|
130
|
+
href={`/products/${heroProduct.slug}`}
|
|
131
|
+
locale={locale}
|
|
132
|
+
className="w-full max-w-md lg:flex-1"
|
|
133
|
+
aria-label={`View ${heroProduct.name}`}
|
|
134
|
+
>
|
|
135
|
+
<figure className="aspect-square overflow-hidden rounded-lg bg-muted">
|
|
136
|
+
{/* Plain <img> keeps the template dependency-free of next/image config. */}
|
|
137
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
138
|
+
<img
|
|
139
|
+
src={heroProduct.featuredAsset.preview}
|
|
140
|
+
alt={getAssetAlt(heroProduct.featuredAsset)}
|
|
141
|
+
className="h-full w-full object-cover"
|
|
142
|
+
/>
|
|
143
|
+
</figure>
|
|
144
|
+
</LocaleLink>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</section>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function FeaturedGrid({ locale }: { locale: LocaleBinding }) {
|
|
152
|
+
await connection();
|
|
153
|
+
const products = await featured();
|
|
154
|
+
|
|
155
|
+
if (products.length === 0) {
|
|
156
|
+
return (
|
|
157
|
+
<p className="text-muted-foreground">
|
|
158
|
+
No products yet — add your first product in the dashboard and it appears here.
|
|
159
|
+
</p>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<>
|
|
165
|
+
<div className="grid grid-cols-2 gap-4 sm:gap-6 lg:grid-cols-4">
|
|
166
|
+
{products.map((product, index) => (
|
|
167
|
+
<div key={product.id} className={index === 0 ? 'col-span-2 row-span-2' : undefined}>
|
|
168
|
+
<ProductCard product={product} locale={locale} emphasis={index === 0} />
|
|
169
|
+
</div>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<section className="mt-16 rounded-xl bg-muted">
|
|
174
|
+
<div className="flex flex-col items-center gap-6 px-6 py-14 text-center">
|
|
175
|
+
<h2 className="text-3xl font-bold tracking-tight text-balance">
|
|
176
|
+
Find something you like?
|
|
177
|
+
</h2>
|
|
178
|
+
<p className="max-w-md text-muted-foreground text-pretty">
|
|
179
|
+
The whole catalog ships from this store — browse everything in one place.
|
|
180
|
+
</p>
|
|
181
|
+
<LocaleLink href="/products" locale={locale} className={cn(buttonVariants(), 'w-64')}>
|
|
182
|
+
Browse the catalog
|
|
183
|
+
</LocaleLink>
|
|
184
|
+
</div>
|
|
185
|
+
</section>
|
|
186
|
+
</>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function HeroSkeleton() {
|
|
191
|
+
return (
|
|
192
|
+
<div className="rounded-xl border bg-card shadow-sm" aria-busy="true">
|
|
193
|
+
<div className="flex w-full flex-col gap-10 p-8 lg:flex-row lg:p-12">
|
|
194
|
+
<div className="w-full max-w-xl space-y-4 lg:flex-1">
|
|
195
|
+
<Skeleton className="h-12 w-3/4" />
|
|
196
|
+
<Skeleton className="h-4 w-full" />
|
|
197
|
+
<Skeleton className="h-4 w-2/3" />
|
|
198
|
+
<Skeleton className="mt-4 h-12 w-48" />
|
|
199
|
+
</div>
|
|
200
|
+
<Skeleton className="aspect-square w-full max-w-md lg:flex-1" />
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function GridSkeleton() {
|
|
207
|
+
return (
|
|
208
|
+
<div className="grid grid-cols-2 gap-4 sm:gap-6 lg:grid-cols-4" aria-busy="true">
|
|
209
|
+
<div className="col-span-2 row-span-2">
|
|
210
|
+
<Skeleton className="h-full min-h-72 w-full" />
|
|
211
|
+
</div>
|
|
212
|
+
<Skeleton className="aspect-square w-full" />
|
|
213
|
+
<Skeleton className="aspect-square w-full" />
|
|
214
|
+
<Skeleton className="aspect-square w-full" />
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { LocaleLink } from '../../../components/LocaleLink';
|
|
2
|
+
import { buttonVariants } from '../../../components/ui/button';
|
|
3
|
+
import { getRequestLocale } from '../../../lib/locale/request-binding';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a visitor sees at a page URL that holds nothing: an unknown route, or a
|
|
7
|
+
* page whose content is not published (or not published YET). The copy does not
|
|
8
|
+
* distinguish them on purpose — "this is scheduled for Tuesday" is the
|
|
9
|
+
* merchant's information, not the public's.
|
|
10
|
+
*/
|
|
11
|
+
export default async function AcfPageNotFound() {
|
|
12
|
+
const { binding } = await getRequestLocale();
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-4">
|
|
15
|
+
<h1 className="text-2xl font-bold tracking-tight text-foreground">Page not found</h1>
|
|
16
|
+
<p className="text-muted-foreground">
|
|
17
|
+
We couldn't find that page. It may have been moved or taken down.
|
|
18
|
+
</p>
|
|
19
|
+
<LocaleLink href="/" locale={binding} className={buttonVariants()}>
|
|
20
|
+
Back to the shop
|
|
21
|
+
</LocaleLink>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { notFound } from 'next/navigation';
|
|
3
|
+
import { cache } from 'react';
|
|
4
|
+
|
|
5
|
+
import { pageContentClaims } from '../../../lib/content/page-metadata';
|
|
6
|
+
import { PageFields } from '../../../lib/content/render-fields';
|
|
7
|
+
import { resolvePage } from '../../../lib/content/resolve-page';
|
|
8
|
+
import {
|
|
9
|
+
getPageByRoute,
|
|
10
|
+
getPageEntries,
|
|
11
|
+
type PageEntry,
|
|
12
|
+
type PageGroup,
|
|
13
|
+
} from '../../../lib/forgecart';
|
|
14
|
+
import { getRequestLocale } from '../../../lib/locale/request-binding';
|
|
15
|
+
import { routeMetadata } from '../../../lib/seo/metadata';
|
|
16
|
+
import type { RouteSearchParams } from '../../../lib/seo/noindex';
|
|
17
|
+
import { PAGE_ROUTE_PREFIX } from '../../../lib/seo/sitemap-entries';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The merchant's own pages (#1934, epic launch#50), served at the route the
|
|
21
|
+
* shop API minted for the page definition.
|
|
22
|
+
*
|
|
23
|
+
* Locale-prefixed addresses land here too and cost nothing: the route tree is
|
|
24
|
+
* FLAT by law (#1041) and the middleware rewrites `/de/pages/x` onto this same
|
|
25
|
+
* file with the locale carried on a request header, so `/pages/x` and every
|
|
26
|
+
* `/<xx>/pages/x` are one route with one decision.
|
|
27
|
+
*
|
|
28
|
+
* FORCE-DYNAMIC, explicitly. "An unpublished page answers 404 on the NEXT
|
|
29
|
+
* request" is this feature's contract, and a contract must not rest on the
|
|
30
|
+
* incidental fact that some module downstream happens to read `headers()`. No
|
|
31
|
+
* `revalidate`, no `use cache`: a window here would serve a page the merchant
|
|
32
|
+
* has already taken down, which is the one failure this route may not have.
|
|
33
|
+
*/
|
|
34
|
+
export const dynamic = 'force-dynamic';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The page's definition AND its visible record, read once per request.
|
|
38
|
+
*
|
|
39
|
+
* Both halves together, because the 404 decision needs both and Next invokes
|
|
40
|
+
* `generateMetadata` and the page as separate calls — without React's request
|
|
41
|
+
* cache the two would resolve independently, and a page unpublished between
|
|
42
|
+
* them would emit indexable metadata for a URL that then 404s.
|
|
43
|
+
*
|
|
44
|
+
* The entries read is skipped when nothing is mounted at the route: there is no
|
|
45
|
+
* definition code to ask about, and the resolution's answer is already settled.
|
|
46
|
+
*/
|
|
47
|
+
const pageForRequest = cache(
|
|
48
|
+
async (route: string): Promise<{ group: PageGroup | null; entries: PageEntry[] }> => {
|
|
49
|
+
const group = await getPageByRoute(route);
|
|
50
|
+
if (group === null) return { group: null, entries: [] };
|
|
51
|
+
return { group, entries: await getPageEntries(group.code) };
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A page's metadata comes from the same resolution the route acts on, so the
|
|
57
|
+
* head and the status can never disagree about what this URL is.
|
|
58
|
+
*
|
|
59
|
+
* Its CONTENT claims — title, description, social card, alternates and
|
|
60
|
+
* indexability — are the record's per-language SEO sidecar (#1375), decided in
|
|
61
|
+
* `lib/content/page-metadata.ts` by the rules the product route uses: the
|
|
62
|
+
* definition's `seoSources` mapping and the merchant's stored overrides arrive
|
|
63
|
+
* already resolved into each language, and a language with no copy of its own
|
|
64
|
+
* renders as a fallback that claims nothing (noindex, no canonical, never an
|
|
65
|
+
* hreflang alternate). This file only dispatches.
|
|
66
|
+
*/
|
|
67
|
+
export async function generateMetadata({
|
|
68
|
+
params,
|
|
69
|
+
searchParams,
|
|
70
|
+
}: {
|
|
71
|
+
params: Promise<{ slug: string }>;
|
|
72
|
+
searchParams: Promise<RouteSearchParams>;
|
|
73
|
+
}): Promise<Metadata> {
|
|
74
|
+
const [{ slug }, resolvedSearchParams, { binding, languageCodes, shopName, channelResolved }] =
|
|
75
|
+
await Promise.all([params, searchParams, getRequestLocale()]);
|
|
76
|
+
const { group, entries } = await pageForRequest(slug);
|
|
77
|
+
const resolution = resolvePage({ requestedRoute: slug, group, entries });
|
|
78
|
+
const pathname = `${PAGE_ROUTE_PREFIX}/${slug}`;
|
|
79
|
+
|
|
80
|
+
return routeMetadata({
|
|
81
|
+
binding,
|
|
82
|
+
shopName,
|
|
83
|
+
channelResolved,
|
|
84
|
+
pathname,
|
|
85
|
+
searchParams: resolvedSearchParams,
|
|
86
|
+
...pageContentClaims(resolution, { locale: binding.locale, languageCodes, pathname }),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The resolution runs in the page SHELL, above any boundary, and that placement
|
|
92
|
+
* is the whole correctness of this route's status codes: `notFound()` can only
|
|
93
|
+
* set a status where Next assigns one, and raised from inside a Suspense
|
|
94
|
+
* boundary the shell has already flushed a 200 — a crawler then INDEXES the
|
|
95
|
+
* not-found page instead of dropping it (the same law `products/[slug]`
|
|
96
|
+
* carries, measured there).
|
|
97
|
+
*
|
|
98
|
+
* There is no boundary at all here, and that follows from the same fact: the
|
|
99
|
+
* two reads ARE this page, so streaming a skeleton would buy nothing while the
|
|
100
|
+
* only content loaded.
|
|
101
|
+
*/
|
|
102
|
+
export default async function AcfPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
103
|
+
const { slug } = await params;
|
|
104
|
+
const { group, entries } = await pageForRequest(slug);
|
|
105
|
+
const resolution = resolvePage({ requestedRoute: slug, group, entries });
|
|
106
|
+
|
|
107
|
+
if (resolution.kind === 'notFound') notFound();
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<article className="space-y-8">
|
|
111
|
+
<h1 className="text-3xl font-bold tracking-tight text-balance">{resolution.group.name}</h1>
|
|
112
|
+
<PageFields definitions={resolution.group.fieldDefinitions} entry={resolution.entry} />
|
|
113
|
+
</article>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Liveness ping for the in-pod dev-server readiness probe.
|
|
3
|
+
*
|
|
4
|
+
* Returns a static 200 with no layout, data fetch, or session. The workspace's
|
|
5
|
+
* readiness probe hits THIS route (not `/`) on a fast cadence while the store is
|
|
6
|
+
* STARTING and then a slow liveness cadence forever — and `/` renders the
|
|
7
|
+
* cart-aware layout, whose `activeOrder` call MATERIALISES an active order on every
|
|
8
|
+
* request. Probing `/` therefore minted a throwaway order on every poll; probing a
|
|
9
|
+
* route handler renders nothing and touches no session, so readiness polling stays
|
|
10
|
+
* side-effect-free.
|
|
11
|
+
*
|
|
12
|
+
* Request access keeps it out of any static prerender so a production build serves
|
|
13
|
+
* it on demand exactly like `next dev` does — the probe always gets a fresh 200.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export function GET(): Response {
|
|
17
|
+
return new Response('ok', {
|
|
18
|
+
status: 200,
|
|
19
|
+
headers: { 'content-type': 'text/plain' },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LocaleLink } from '../../../components/LocaleLink';
|
|
2
|
+
import { buttonVariants } from '../../../components/ui/button';
|
|
3
|
+
import { getRequestLocale } from '../../../lib/locale/request-binding';
|
|
4
|
+
|
|
5
|
+
export default async function ProductNotFound() {
|
|
6
|
+
const { binding } = await getRequestLocale();
|
|
7
|
+
return (
|
|
8
|
+
<div className="space-y-4">
|
|
9
|
+
<h1 className="text-2xl font-bold tracking-tight text-foreground">Product not found</h1>
|
|
10
|
+
<p className="text-muted-foreground">
|
|
11
|
+
We couldn't find that product. It may have been removed.
|
|
12
|
+
</p>
|
|
13
|
+
<LocaleLink href="/products" locale={binding} className={buttonVariants()}>
|
|
14
|
+
Back to products
|
|
15
|
+
</LocaleLink>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|