@forgecart/cli 2.202610280649.0 → 2.202610280926.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/dist/src/commands/init-questions.d.ts +12 -11
- package/dist/src/commands/init-questions.js +15 -23
- package/dist/src/commands/init-questions.js.map +1 -1
- package/dist/src/commands/init.d.ts +13 -13
- package/dist/src/commands/init.js +75 -144
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/refresh.d.ts +3 -3
- package/dist/src/commands/refresh.js +4 -14
- package/dist/src/commands/refresh.js.map +1 -1
- package/dist/src/commands/template-manifest.d.ts +2 -5
- package/dist/src/commands/template-manifest.js +1 -1
- package/dist/src/commands/template-manifest.js.map +1 -1
- package/dist/src/commands/type/init.input.d.ts +1 -1
- package/dist/src/commands/type/init.types.d.ts +4 -4
- package/dist/src/template/template-set.d.ts +17 -27
- package/dist/src/template/template-set.js +25 -42
- package/dist/src/template/template-set.js.map +1 -1
- package/dist/src/template/template-unavailable-error.d.ts +3 -4
- package/dist/src/template/template-unavailable-error.js +3 -4
- package/dist/src/template/template-unavailable-error.js.map +1 -1
- package/dist/src/template/type/template-set.types.d.ts +14 -17
- package/dist/src/template/type/template-set.types.js +6 -1
- package/dist/src/template/type/template-set.types.js.map +1 -1
- package/dist/src/template/util/template-identity.util.d.ts +1 -3
- package/dist/src/template/util/template-identity.util.js +0 -5
- package/dist/src/template/util/template-identity.util.js.map +1 -1
- package/package.json +1 -1
- package/templates/storefront-blank/.forgecartignore +2 -0
- package/templates/storefront-blank/Procfile +1 -0
- package/templates/storefront-blank/README.md +86 -0
- package/templates/storefront-blank/SEO-MIGRATION.md +718 -0
- package/templates/storefront-blank/_gitignore +21 -0
- package/templates/storefront-blank/next.config.js +105 -0
- package/templates/storefront-blank/package.json +35 -0
- package/templates/storefront-blank/postcss.config.js +5 -0
- package/templates/storefront-blank/src/app/%5F%5Ffc/identify/route.ts +205 -0
- package/templates/storefront-blank/src/app/%5F%5Ffc/track/route.ts +189 -0
- package/templates/storefront-blank/src/app/%5F%5Fforge_beacon/route.ts +87 -0
- package/templates/storefront-blank/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
- package/templates/storefront-blank/src/app/error.tsx +21 -0
- package/templates/storefront-blank/src/app/global-error.tsx +23 -0
- package/templates/storefront-blank/src/app/globals.css +110 -0
- package/templates/storefront-blank/src/app/layout.tsx +102 -0
- package/templates/storefront-blank/src/app/page.tsx +52 -0
- package/templates/storefront-blank/src/app/pages/[slug]/not-found.tsx +23 -0
- package/templates/storefront-blank/src/app/pages/[slug]/page.tsx +115 -0
- package/templates/storefront-blank/src/app/ping/route.ts +21 -0
- package/templates/storefront-blank/src/app/robots.ts +69 -0
- package/templates/storefront-blank/src/app/sitemap.ts +107 -0
- package/templates/storefront-blank/src/components/ForgeErrorBeacon.tsx +102 -0
- package/templates/storefront-blank/src/components/ForgeTracker.tsx +479 -0
- package/templates/storefront-blank/src/components/ForgecartDesigner.tsx +43 -0
- package/templates/storefront-blank/src/components/LocaleLink.tsx +49 -0
- package/templates/storefront-blank/src/instrumentation.ts +109 -0
- package/templates/storefront-blank/src/lib/account/account-link.ts +76 -0
- package/templates/storefront-blank/src/lib/account/account-state.ts +152 -0
- package/templates/storefront-blank/src/lib/account/register-state.ts +133 -0
- package/templates/storefront-blank/src/lib/account/reset-password-state.ts +111 -0
- package/templates/storefront-blank/src/lib/account/verify-state.ts +56 -0
- package/templates/storefront-blank/src/lib/account-actions.ts +76 -0
- package/templates/storefront-blank/src/lib/account-session.ts +114 -0
- package/templates/storefront-blank/src/lib/action-result.ts +30 -0
- package/templates/storefront-blank/src/lib/asset-alt.ts +34 -0
- package/templates/storefront-blank/src/lib/backend-actions.ts +20 -0
- package/templates/storefront-blank/src/lib/backend-client.ts +47 -0
- package/templates/storefront-blank/src/lib/cart-context.tsx +236 -0
- package/templates/storefront-blank/src/lib/checkout-session.ts +185 -0
- package/templates/storefront-blank/src/lib/content/page-metadata.ts +113 -0
- package/templates/storefront-blank/src/lib/content/render-fields.tsx +256 -0
- package/templates/storefront-blank/src/lib/content/resolve-page.ts +143 -0
- package/templates/storefront-blank/src/lib/error-messages.ts +118 -0
- package/templates/storefront-blank/src/lib/experiments.ts +333 -0
- package/templates/storefront-blank/src/lib/forgecart.ts +464 -0
- package/templates/storefront-blank/src/lib/format.ts +89 -0
- package/templates/storefront-blank/src/lib/identify-forward.ts +152 -0
- package/templates/storefront-blank/src/lib/locale/channel-locales-loader.ts +169 -0
- package/templates/storefront-blank/src/lib/locale/channel-locales-map.ts +46 -0
- package/templates/storefront-blank/src/lib/locale/channel-locales.ts +191 -0
- package/templates/storefront-blank/src/lib/locale/grammar.ts +194 -0
- package/templates/storefront-blank/src/lib/locale/localized-path.ts +55 -0
- package/templates/storefront-blank/src/lib/locale/middleware-plan.ts +107 -0
- package/templates/storefront-blank/src/lib/locale/request-binding.ts +80 -0
- package/templates/storefront-blank/src/lib/locale/request-locale.ts +66 -0
- package/templates/storefront-blank/src/lib/marketing-params.ts +213 -0
- package/templates/storefront-blank/src/lib/money.ts +50 -0
- package/templates/storefront-blank/src/lib/seo/alternates.ts +123 -0
- package/templates/storefront-blank/src/lib/seo/json-ld.ts +266 -0
- package/templates/storefront-blank/src/lib/seo/metadata.ts +419 -0
- package/templates/storefront-blank/src/lib/seo/noindex.ts +223 -0
- package/templates/storefront-blank/src/lib/seo/public-origin.ts +166 -0
- package/templates/storefront-blank/src/lib/seo/redirect-plan.ts +86 -0
- package/templates/storefront-blank/src/lib/seo/resolve-path.ts +107 -0
- package/templates/storefront-blank/src/lib/seo/scaffolded-routes.ts +83 -0
- package/templates/storefront-blank/src/lib/seo/sidecar.ts +75 -0
- package/templates/storefront-blank/src/lib/seo/site-verification.ts +98 -0
- package/templates/storefront-blank/src/lib/seo/sitemap-cache.ts +114 -0
- package/templates/storefront-blank/src/lib/seo/sitemap-entries.ts +310 -0
- package/templates/storefront-blank/src/lib/session-actions.ts +61 -0
- package/templates/storefront-blank/src/lib/session-cookies.ts +98 -0
- package/templates/storefront-blank/src/lib/shop-config.ts +51 -0
- package/templates/storefront-blank/src/lib/shop-session.ts +204 -0
- package/templates/storefront-blank/src/lib/track-forward.ts +200 -0
- package/templates/storefront-blank/src/lib/uuid.ts +19 -0
- package/templates/storefront-blank/src/middleware.ts +379 -0
- package/templates/storefront-blank/src/seo/redirects.ts +44 -0
- package/templates/storefront-blank/src/seo/static-routes.ts +12 -0
- package/templates/storefront-blank/src/server/app.module.ts +18 -0
- package/templates/storefront-blank/src/server/backend-api.ts +21 -0
- package/templates/storefront-blank/src/server/backend-method.decorator.ts +23 -0
- package/templates/storefront-blank/src/server/bootstrap.ts +122 -0
- package/templates/storefront-blank/src/server/forge/live-revision.ts +158 -0
- package/templates/storefront-blank/src/server/forgecart/forgecart-client.factory.ts +73 -0
- package/templates/storefront-blank/src/server/forgecart/forgecart.module.ts +9 -0
- package/templates/storefront-blank/src/server/runner.ts +90 -0
- package/templates/storefront-blank/src/server/types.ts +36 -0
- package/templates/storefront-blank/tsconfig.json +25 -0
- package/templates/storefront-blank-sdk-floor.json +1174 -0
- package/templates/storefront-shadcn-blank/.forgecartignore +2 -0
- package/templates/storefront-shadcn-blank/Procfile +1 -0
- package/templates/storefront-shadcn-blank/README.md +93 -0
- package/templates/storefront-shadcn-blank/SEO-MIGRATION.md +718 -0
- package/templates/storefront-shadcn-blank/_gitignore +21 -0
- package/templates/storefront-shadcn-blank/components.json +20 -0
- package/templates/storefront-shadcn-blank/next.config.js +105 -0
- package/templates/storefront-shadcn-blank/package.json +42 -0
- package/templates/storefront-shadcn-blank/postcss.config.js +5 -0
- package/templates/storefront-shadcn-blank/src/app/%5F%5Ffc/identify/route.ts +205 -0
- package/templates/storefront-shadcn-blank/src/app/%5F%5Ffc/track/route.ts +189 -0
- package/templates/storefront-shadcn-blank/src/app/%5F%5Fforge_beacon/route.ts +87 -0
- package/templates/storefront-shadcn-blank/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
- package/templates/storefront-shadcn-blank/src/app/error.tsx +23 -0
- package/templates/storefront-shadcn-blank/src/app/global-error.tsx +23 -0
- package/templates/storefront-shadcn-blank/src/app/globals.css +156 -0
- package/templates/storefront-shadcn-blank/src/app/layout.tsx +102 -0
- package/templates/storefront-shadcn-blank/src/app/page.tsx +53 -0
- package/templates/storefront-shadcn-blank/src/app/pages/[slug]/not-found.tsx +24 -0
- package/templates/storefront-shadcn-blank/src/app/pages/[slug]/page.tsx +115 -0
- package/templates/storefront-shadcn-blank/src/app/ping/route.ts +21 -0
- package/templates/storefront-shadcn-blank/src/app/robots.ts +69 -0
- package/templates/storefront-shadcn-blank/src/app/sitemap.ts +107 -0
- package/templates/storefront-shadcn-blank/src/components/ForgeErrorBeacon.tsx +102 -0
- package/templates/storefront-shadcn-blank/src/components/ForgeTracker.tsx +479 -0
- package/templates/storefront-shadcn-blank/src/components/ForgecartDesigner.tsx +43 -0
- package/templates/storefront-shadcn-blank/src/components/LocaleLink.tsx +49 -0
- package/templates/storefront-shadcn-blank/src/components/ui/alert.tsx +75 -0
- package/templates/storefront-shadcn-blank/src/components/ui/badge.tsx +40 -0
- package/templates/storefront-shadcn-blank/src/components/ui/button.tsx +64 -0
- package/templates/storefront-shadcn-blank/src/components/ui/card.tsx +28 -0
- package/templates/storefront-shadcn-blank/src/components/ui/input.tsx +26 -0
- package/templates/storefront-shadcn-blank/src/components/ui/label.tsx +22 -0
- package/templates/storefront-shadcn-blank/src/components/ui/native-select.tsx +27 -0
- package/templates/storefront-shadcn-blank/src/components/ui/skeleton.tsx +21 -0
- package/templates/storefront-shadcn-blank/src/components/ui/utils.ts +16 -0
- package/templates/storefront-shadcn-blank/src/instrumentation.ts +109 -0
- package/templates/storefront-shadcn-blank/src/lib/account/account-link.ts +76 -0
- package/templates/storefront-shadcn-blank/src/lib/account/account-state.ts +152 -0
- package/templates/storefront-shadcn-blank/src/lib/account/register-state.ts +133 -0
- package/templates/storefront-shadcn-blank/src/lib/account/reset-password-state.ts +111 -0
- package/templates/storefront-shadcn-blank/src/lib/account/verify-state.ts +56 -0
- package/templates/storefront-shadcn-blank/src/lib/account-actions.ts +76 -0
- package/templates/storefront-shadcn-blank/src/lib/account-session.ts +114 -0
- package/templates/storefront-shadcn-blank/src/lib/action-result.ts +30 -0
- package/templates/storefront-shadcn-blank/src/lib/asset-alt.ts +34 -0
- package/templates/storefront-shadcn-blank/src/lib/backend-actions.ts +20 -0
- package/templates/storefront-shadcn-blank/src/lib/backend-client.ts +47 -0
- package/templates/storefront-shadcn-blank/src/lib/cart-context.tsx +236 -0
- package/templates/storefront-shadcn-blank/src/lib/checkout-session.ts +185 -0
- package/templates/storefront-shadcn-blank/src/lib/content/page-metadata.ts +113 -0
- package/templates/storefront-shadcn-blank/src/lib/content/render-fields.tsx +256 -0
- package/templates/storefront-shadcn-blank/src/lib/content/resolve-page.ts +143 -0
- package/templates/storefront-shadcn-blank/src/lib/error-messages.ts +118 -0
- package/templates/storefront-shadcn-blank/src/lib/experiments.ts +333 -0
- package/templates/storefront-shadcn-blank/src/lib/forgecart.ts +464 -0
- package/templates/storefront-shadcn-blank/src/lib/format.ts +89 -0
- package/templates/storefront-shadcn-blank/src/lib/identify-forward.ts +152 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/channel-locales-loader.ts +169 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/channel-locales-map.ts +46 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/channel-locales.ts +191 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/grammar.ts +194 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/localized-path.ts +55 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/middleware-plan.ts +107 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/request-binding.ts +80 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/request-locale.ts +66 -0
- package/templates/storefront-shadcn-blank/src/lib/marketing-params.ts +213 -0
- package/templates/storefront-shadcn-blank/src/lib/money.ts +50 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/alternates.ts +123 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/json-ld.ts +266 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/metadata.ts +419 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/noindex.ts +223 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/public-origin.ts +166 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/redirect-plan.ts +86 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/resolve-path.ts +107 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/scaffolded-routes.ts +83 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/sidecar.ts +75 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/site-verification.ts +98 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/sitemap-cache.ts +114 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/sitemap-entries.ts +310 -0
- package/templates/storefront-shadcn-blank/src/lib/session-actions.ts +61 -0
- package/templates/storefront-shadcn-blank/src/lib/session-cookies.ts +98 -0
- package/templates/storefront-shadcn-blank/src/lib/shop-config.ts +51 -0
- package/templates/storefront-shadcn-blank/src/lib/shop-session.ts +204 -0
- package/templates/storefront-shadcn-blank/src/lib/track-forward.ts +200 -0
- package/templates/storefront-shadcn-blank/src/lib/uuid.ts +19 -0
- package/templates/storefront-shadcn-blank/src/middleware.ts +379 -0
- package/templates/storefront-shadcn-blank/src/seo/redirects.ts +44 -0
- package/templates/storefront-shadcn-blank/src/seo/static-routes.ts +12 -0
- package/templates/storefront-shadcn-blank/src/server/app.module.ts +18 -0
- package/templates/storefront-shadcn-blank/src/server/backend-api.ts +21 -0
- package/templates/storefront-shadcn-blank/src/server/backend-method.decorator.ts +23 -0
- package/templates/storefront-shadcn-blank/src/server/bootstrap.ts +122 -0
- package/templates/storefront-shadcn-blank/src/server/forge/live-revision.ts +158 -0
- package/templates/storefront-shadcn-blank/src/server/forgecart/forgecart-client.factory.ts +73 -0
- package/templates/storefront-shadcn-blank/src/server/forgecart/forgecart.module.ts +9 -0
- package/templates/storefront-shadcn-blank/src/server/runner.ts +90 -0
- package/templates/storefront-shadcn-blank/src/server/types.ts +36 -0
- package/templates/storefront-shadcn-blank/tsconfig.json +25 -0
- package/templates/storefront-shadcn-blank-sdk-floor.json +1174 -0
- package/templates/template-set.json +15 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEV-ONLY same-origin error-beacon forwarder.
|
|
3
|
+
*
|
|
4
|
+
* The storefront's client beacon (`ForgeErrorBeacon`) POSTs a browser-surfaced
|
|
5
|
+
* runtime error here — same-origin, so the browser never learns the pod-internal
|
|
6
|
+
* receiver address. This handler forwards the body SERVER-side to the in-pod
|
|
7
|
+
* workspace-manager receiver at `http://127.0.0.1:<FORGE_BEACON_PORT>/__forge_beacon`,
|
|
8
|
+
* which folds it into the supervisor's runtime state so the shop's recovery brain
|
|
9
|
+
* can originate a fix for a crash the dev-server's stderr scan cannot see.
|
|
10
|
+
*
|
|
11
|
+
* Gated entirely on `NODE_ENV === 'development'`: a deployed `next start` storefront
|
|
12
|
+
* answers 404 here and forwards nothing — the whole beacon path ships only in the
|
|
13
|
+
* in-pod dev-server. Request access keeps it out of any static prerender so it
|
|
14
|
+
* behaves identically under `next dev` and a dev build.
|
|
15
|
+
*
|
|
16
|
+
* Folder name: `%5F%5Fforge_beacon` is URL-encoded `__forge_beacon`. The App Router
|
|
17
|
+
* treats a folder whose name starts with `_` as PRIVATE and leaves it out of routing
|
|
18
|
+
* entirely, so the literal spelling this route shipped with answered the not-found
|
|
19
|
+
* page to every POST — silently, because the client swallows delivery failures by
|
|
20
|
+
* design (#2103). The `%5F` escape is Next's documented way to serve a URL segment
|
|
21
|
+
* that really starts with an underscore, and only the FOLDER may change: the URL is
|
|
22
|
+
* a contract with the in-pod receiver (`StorefrontBeaconService`'s `BEACON_PATH`),
|
|
23
|
+
* which this template's server half (`src/instrumentation.ts`) posts to as well.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const BEACON_PORT = process.env.FORGE_BEACON_PORT ?? '3002';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The revision the reporting page was SERVED at, lifted out of the forwarded
|
|
30
|
+
* body for the log line below, or `null` when the report is untagged.
|
|
31
|
+
*
|
|
32
|
+
* Reads the body it is ALREADY forwarding verbatim rather than re-deriving
|
|
33
|
+
* anything: the value was stamped into the document at render time, and this
|
|
34
|
+
* handler is a forwarder, not a second opinion. The parse is guarded because a
|
|
35
|
+
* malformed body must not change what this route does — the forward still
|
|
36
|
+
* happens and the client still gets its 204, exactly as before. `JSON.parse` is
|
|
37
|
+
* the one throw on this path and it carries no code to classify, which is why it
|
|
38
|
+
* is caught here and nowhere else (`instrumentation.ts` swallows its own boot
|
|
39
|
+
* failure in the same template for the same reason); judging the payload stays
|
|
40
|
+
* the receiver's job.
|
|
41
|
+
*/
|
|
42
|
+
function servedRevision(body: string): string | null {
|
|
43
|
+
try {
|
|
44
|
+
const parsed: unknown = JSON.parse(body);
|
|
45
|
+
if (typeof parsed !== 'object' || parsed === null) return null;
|
|
46
|
+
const value = (parsed as { atRevision?: unknown }).atRevision;
|
|
47
|
+
if (typeof value !== 'string' || value.length === 0) return null;
|
|
48
|
+
return value;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function POST(request: Request): Promise<Response> {
|
|
55
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
56
|
+
return new Response(null, { status: 404 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const body = await request.text();
|
|
60
|
+
// Make the dev-server's output ring revision-attributable (#847): this line is
|
|
61
|
+
// written to the ring the supervisor retains and `TailServerLogs` serves, so a
|
|
62
|
+
// human or the recovery brain reading the ring can tell WHICH tree a
|
|
63
|
+
// browser-surfaced crash belongs to instead of guessing from timing.
|
|
64
|
+
//
|
|
65
|
+
// It deliberately carries the revision and NOTHING ELSE — no message, no stack,
|
|
66
|
+
// no route. The ambient scanner matches dev-server output case-insensitively
|
|
67
|
+
// against RUNTIME_ERROR_PATTERNS ('error:', 'typeerror', 'unhandled', …;
|
|
68
|
+
// `runtime-error-scanner.types.ts`), so echoing the reported message here would
|
|
69
|
+
// put the crash text INTO the stream that scanner reads and fold a second,
|
|
70
|
+
// independent ambient error for the very report already on its way to the
|
|
71
|
+
// receiver — a self-inflicted double-fire. An untagged report stays silent
|
|
72
|
+
// rather than logging a placeholder, which keeps today's output byte-identical
|
|
73
|
+
// for every producer that does not stamp (the server `onRequestError` path
|
|
74
|
+
// never reaches this route at all — it POSTs the receiver directly).
|
|
75
|
+
const revision = servedRevision(body);
|
|
76
|
+
if (revision !== null) console.log(`[forge-beacon browser rev=${revision}]`);
|
|
77
|
+
// Forward server-side to the loopback receiver. A delivery failure is swallowed —
|
|
78
|
+
// the beacon is a best-effort backstop, never a hard dependency of the page — but
|
|
79
|
+
// the client still gets a clean 204 so it never retries against a flapping pod.
|
|
80
|
+
await fetch(`http://127.0.0.1:${BEACON_PORT}/__forge_beacon`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'content-type': 'application/json' },
|
|
83
|
+
body,
|
|
84
|
+
}).catch(() => undefined);
|
|
85
|
+
|
|
86
|
+
return new Response(null, { status: 204 });
|
|
87
|
+
}
|
|
@@ -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,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,102 @@
|
|
|
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 { getRequestLocale } from '../lib/locale/request-binding';
|
|
9
|
+
import { shellMetadata } from '../lib/seo/metadata';
|
|
10
|
+
import { getSiteVerifications } from '../lib/seo/site-verification';
|
|
11
|
+
import { FORGE_REVISION_META, resolveLiveRevision } from '../server/forge/live-revision';
|
|
12
|
+
|
|
13
|
+
import './globals.css';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The deployment-wide metadata floor.
|
|
17
|
+
*
|
|
18
|
+
* Resolved per REQUEST rather than declared as a static `metadata` object,
|
|
19
|
+
* because everything in it arrives in the environment of the running server
|
|
20
|
+
* rather than of the machine that built the bundle: the public origin from
|
|
21
|
+
* `.env` (see `lib/seo/public-origin.ts`) and the shop name from the channel
|
|
22
|
+
* itself. A build-time answer would bake the builder's empty environment into
|
|
23
|
+
* every store shipped from that image.
|
|
24
|
+
*
|
|
25
|
+
* The title is the STORE's name, so a merchant who scaffolds a page and writes
|
|
26
|
+
* no metadata gets their own store in the browser tab. Route-specific claims
|
|
27
|
+
* live on the routes; this is only the floor, and it deliberately carries no
|
|
28
|
+
* canonical (it does not know which route it is wrapping).
|
|
29
|
+
*
|
|
30
|
+
* The site-verification placements (#1530) are read on the same request cache
|
|
31
|
+
* and handed to the SAME composer, so `lib/seo/metadata.ts` stays the only
|
|
32
|
+
* thing in this template that turns a placement into a `<meta>`. They ride the
|
|
33
|
+
* FLOOR rather than a route, because the verifier chooses which page it fetches
|
|
34
|
+
* and a tag on only some of them is a verification that works until it does not.
|
|
35
|
+
*
|
|
36
|
+
* It also carries the DEV-ONLY revision stamp (#847), written where the page is
|
|
37
|
+
* RENDERED: the browser beacon reads it back at throw time, so a promote landing
|
|
38
|
+
* between serve and throw cannot make the report blame the new revision for the
|
|
39
|
+
* old one's crash. Dev-gated by the literal `NODE_ENV` check, so a production
|
|
40
|
+
* build eliminates the resolution and a deployed storefront leaks no revision.
|
|
41
|
+
* Unresolvable is a NORMAL answer and means no tag.
|
|
42
|
+
*/
|
|
43
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
44
|
+
const { shopName, channelResolved } = await getRequestLocale();
|
|
45
|
+
const siteVerifications = await getSiteVerifications();
|
|
46
|
+
const shell = shellMetadata({ shopName, channelResolved, siteVerifications });
|
|
47
|
+
if (process.env.NODE_ENV !== 'development') return shell;
|
|
48
|
+
const revision = resolveLiveRevision();
|
|
49
|
+
if (revision === null) return shell;
|
|
50
|
+
// MERGED into the shell's `other`, never written over it: that key carries the
|
|
51
|
+
// site-verification placements (#1530), and a storefront pod serves through
|
|
52
|
+
// `npm run dev`, so an overwrite here would strip the provider's tag from
|
|
53
|
+
// every page its verifier can fetch.
|
|
54
|
+
return { ...shell, other: { ...shell.other, [FORGE_REVISION_META]: revision } };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The root layout of a blank storefront (#3480) — the platform's shell with no
|
|
59
|
+
* example storefront on it: no header, no footer navigation and no cart. What a
|
|
60
|
+
* blank workspace builds on is all here: the document language, the locale
|
|
61
|
+
* contract, the metadata floor above and the invisible client plumbing.
|
|
62
|
+
*
|
|
63
|
+
* DYNAMIC on every route, by design (D8). It reads the request locale to write
|
|
64
|
+
* `<html lang>`, and `lang` is an attribute on the document root: structurally
|
|
65
|
+
* outside every Suspense boundary, so it cannot come from a streamed hole.
|
|
66
|
+
* `next.config.js` carries the reasoning — Cache Components is off because the
|
|
67
|
+
* escape hatch it would force (a `<Suspense>` above `<body>`) is exactly what
|
|
68
|
+
* makes the two throws below unable to set a status.
|
|
69
|
+
*
|
|
70
|
+
* Those throws are the locale contract's teeth. Raised HERE, in the shell,
|
|
71
|
+
* they reach the one place Next assigns a status: `permanentRedirect` gives a
|
|
72
|
+
* real 308 with a `Location`, and `notFound` a real 404. Raised from inside
|
|
73
|
+
* any boundary they would resolve as client-rendered content instead, and the
|
|
74
|
+
* response would be a 200 carrying a 404 page — a soft-404, which a crawler
|
|
75
|
+
* indexes rather than drops.
|
|
76
|
+
*/
|
|
77
|
+
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
78
|
+
const { decision, binding } = await getRequestLocale();
|
|
79
|
+
if (decision.kind === 'redirect') permanentRedirect(decision.to);
|
|
80
|
+
if (decision.kind === 'notFound') notFound();
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<html lang={binding.locale}>
|
|
84
|
+
<body className="min-h-screen bg-background font-sans text-foreground antialiased">
|
|
85
|
+
<main className="mx-auto max-w-6xl px-4 py-10">{children}</main>
|
|
86
|
+
{/* Invisible client plumbing. The tracker reads usePathname(), which
|
|
87
|
+
needs a boundary of its own — null fallback, these render nothing.
|
|
88
|
+
This must stay INSIDE <body>: a boundary above it would make the
|
|
89
|
+
shell resumable and cost the statuses above. */}
|
|
90
|
+
<Suspense fallback={null}>
|
|
91
|
+
<ForgecartDesigner />
|
|
92
|
+
<ForgeErrorBeacon />
|
|
93
|
+
<ForgeTracker
|
|
94
|
+
enabled={Boolean(
|
|
95
|
+
process.env.FORGECART_SHOP_API_URL && process.env.FORGECART_CHANNEL_TOKEN,
|
|
96
|
+
)}
|
|
97
|
+
/>
|
|
98
|
+
</Suspense>
|
|
99
|
+
</body>
|
|
100
|
+
</html>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
|
|
3
|
+
import { getRequestLocale } from '../lib/locale/request-binding';
|
|
4
|
+
import { staticPathsByLocale } from '../lib/seo/alternates';
|
|
5
|
+
import { pageTitle, routeMetadata } from '../lib/seo/metadata';
|
|
6
|
+
import type { RouteSearchParams } from '../lib/seo/noindex';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The landing surface is indexable — unless the URL carries query state (an
|
|
10
|
+
* experiment pin, a preview) that makes it a VIEW of the page competing with
|
|
11
|
+
* the page's own address.
|
|
12
|
+
*
|
|
13
|
+
* `title: null` because the store's landing page has no name of its own: it IS
|
|
14
|
+
* the store, so it takes the shop name alone rather than composing something
|
|
15
|
+
* like `Home | Acme`. The description is the shell's, inherited by omission.
|
|
16
|
+
*/
|
|
17
|
+
export async function generateMetadata({
|
|
18
|
+
searchParams,
|
|
19
|
+
}: {
|
|
20
|
+
searchParams: Promise<RouteSearchParams>;
|
|
21
|
+
}): Promise<Metadata> {
|
|
22
|
+
const [resolvedSearchParams, { binding, languageCodes, shopName, channelResolved }] =
|
|
23
|
+
await Promise.all([searchParams, getRequestLocale()]);
|
|
24
|
+
return routeMetadata({
|
|
25
|
+
binding,
|
|
26
|
+
shopName,
|
|
27
|
+
channelResolved,
|
|
28
|
+
// The canonical is built from the BASE path, so `/?fc-exp=hero:b` declares
|
|
29
|
+
// `/` as its address — the query-state copy points at the page it is a view
|
|
30
|
+
// of, which is the whole reason those URLs are noindex rather than ignored.
|
|
31
|
+
pathname: '/',
|
|
32
|
+
pathsByLocale: staticPathsByLocale(languageCodes, '/'),
|
|
33
|
+
searchParams: resolvedSearchParams,
|
|
34
|
+
title: null,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Home — the blank storefront's one page (#3480): the store's name as its one
|
|
40
|
+
* heading, and nothing else. No example content to delete: everything the
|
|
41
|
+
* merchant builds starts here, on the shell's platform plumbing and the
|
|
42
|
+
* vendored components in `src/components/ui/`. The heading reads the name the
|
|
43
|
+
* document title carries, so a channel that has not named its store still
|
|
44
|
+
* renders one.
|
|
45
|
+
*/
|
|
46
|
+
export default async function HomePage() {
|
|
47
|
+
const { shopName } = await getRequestLocale();
|
|
48
|
+
return (
|
|
49
|
+
<h1 className="text-4xl font-bold tracking-tight text-balance">
|
|
50
|
+
{pageTitle(null, shopName)}
|
|
51
|
+
</h1>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -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
|
+
}
|