@forgecart/cli 2.202610052143.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,708 @@
|
|
|
1
|
+
# Storefront SEO migration
|
|
2
|
+
|
|
3
|
+
This document migrates a storefront that was built before the platform's locale and SEO layer
|
|
4
|
+
existed. It is written for the agent that edits this storefront. The operator (the person who runs
|
|
5
|
+
this store) hands it to you; no platform process runs it. Follow it top to bottom, as one piece of
|
|
6
|
+
work, and finish with the DONE summary in the last section.
|
|
7
|
+
|
|
8
|
+
**This file is platform-owned. Do not edit this file**, and do not record progress in it. The
|
|
9
|
+
operator does not edit it either. `forgecart refresh` replaces an unchanged copy with every newer
|
|
10
|
+
version and leaves an edited copy alone, reporting it under `conflicts` — an edited copy stops
|
|
11
|
+
receiving corrections. Your progress goes in the DONE summary.
|
|
12
|
+
|
|
13
|
+
**How it arrived.** The operator ran the template refresh with no `adopt` list. The refresh:
|
|
14
|
+
|
|
15
|
+
- wrote every platform file that was missing, or unchanged since it was generated, at the current
|
|
16
|
+
platform version, and listed it under `rendered`;
|
|
17
|
+
- left every platform file that differs from the current platform version untouched, and listed it
|
|
18
|
+
under `conflicts`;
|
|
19
|
+
- listed every platform file that already matched under `upToDate`;
|
|
20
|
+
- never touched a file the platform does not ship.
|
|
21
|
+
|
|
22
|
+
**Does the migration apply?** Yes, when that refresh listed `src/lib/locale/grammar.ts` or
|
|
23
|
+
`src/lib/seo/metadata.ts` under `rendered`: neither file existed in this storefront before. There
|
|
24
|
+
is no version number to check instead. If you do not have the report, run the sweep in the
|
|
25
|
+
deletion list: any match outside the listed platform matches means the migration applies. With no
|
|
26
|
+
such match there is nothing to migrate — report that, with the sweep as evidence, and stop.
|
|
27
|
+
|
|
28
|
+
**What `conflicts` contains.** A provisioned storefront usually has no
|
|
29
|
+
`.forgecart/template-manifest.json` before its first refresh, so the refresh cannot tell a file the
|
|
30
|
+
merchant customized from a file that is only an older platform version: `conflicts` lists every
|
|
31
|
+
platform file that differs from the current version. The merge steps handle both kinds the same
|
|
32
|
+
way. The refresh wrote the manifest, so every later refresh is exact.
|
|
33
|
+
|
|
34
|
+
**The tree is mixed until you finish.** New platform modules now sit next to old files. For
|
|
35
|
+
example, `src/app/sitemap.ts` imports `getProductSeoEntries` and `getServedPageRoutes` from
|
|
36
|
+
`src/lib/forgecart.ts`; an older `src/lib/forgecart.ts` exports neither, so `/sitemap.xml` fails
|
|
37
|
+
until that file is merged. Therefore:
|
|
38
|
+
|
|
39
|
+
1. Do the migration as its own work. Do not mix feature changes into the same turns.
|
|
40
|
+
2. Put the deletions and the platform-file merges in the same promote.
|
|
41
|
+
3. Do not ask the operator to publish until every step of the verification section passes.
|
|
42
|
+
|
|
43
|
+
**The refresh operation.** You call it to take the platform version of a file (to adopt it):
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
CallAdminOperation { "name": "workspaceTemplate.adminRefreshWorkspaceTemplate", "args": { "input": { "adopt": ["src/middleware.ts", "src/lib/forgecart.ts"] } } }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It is the admin mutation `refreshWorkspaceTemplate(input: { adopt: [...] })` and returns
|
|
50
|
+
`rendered`, `conflicts`, `adopted`, `upToDate` and `dependenciesChanged`. Each `adopt` path must
|
|
51
|
+
be one a refresh reported under `conflicts`. When `dependenciesChanged` is `true`, dependencies were
|
|
52
|
+
re-installed before the call returned. `WORKSPACE_TEMPLATE_REFRESH_UNAVAILABLE` means the
|
|
53
|
+
workspace is not running yet; retry.
|
|
54
|
+
|
|
55
|
+
## What the platform now ships
|
|
56
|
+
|
|
57
|
+
The layer the migration moves this storefront onto. Every rule below is implemented by a platform
|
|
58
|
+
file; your job is to call it, never to reimplement it.
|
|
59
|
+
|
|
60
|
+
- **The URL is the only language selector.** The channel's default language is served unprefixed
|
|
61
|
+
(`/products`); every other language under its two-letter prefix (`/de/products`). Both render the
|
|
62
|
+
SAME route: the middleware rewrites `/de/products` to `/products` and passes the language on. There
|
|
63
|
+
is no `[locale]` segment, no query parameter and no cookie that selects a language.
|
|
64
|
+
- **Two-letter top-level segments are reserved** for languages. Routes that exist once per store —
|
|
65
|
+
`/sitemap.xml`, `/robots.txt`, `/ping`, `/api`, `/__fc`, `/__forge_beacon`, `/verify`,
|
|
66
|
+
`/reset-password` — never take a prefix.
|
|
67
|
+
- **The root layout decides the status.** A prefix naming the default language answers 308 to the
|
|
68
|
+
unprefixed URL (`/en/products` → `/products`); a prefix the channel does not offer answers 404.
|
|
69
|
+
- **Links stay in their language.** Every internal link is a `LocaleLink` with the unprefixed path;
|
|
70
|
+
outside JSX, `localizedPath(path, binding)`. The language switcher uses plain anchors with
|
|
71
|
+
`hrefLang`, so switching loads a new document.
|
|
72
|
+
- **`src/middleware.ts` is the only `Content-Language` emitter.** Every page under a language prefix
|
|
73
|
+
answers with exactly one `Content-Language: <xx>`; an unprefixed page answers with none — its
|
|
74
|
+
`<html lang>` states the language.
|
|
75
|
+
- **Metadata is composed, never hand-built.** Every route's `generateMetadata` returns
|
|
76
|
+
`routeMetadata(...)`; the root layout returns `shellMetadata(...)`; an entity's title, description
|
|
77
|
+
and social image come from `entityMetadata(...)`. Canonical, hreflang, `x-default`, robots and
|
|
78
|
+
social tags are produced together and cannot disagree.
|
|
79
|
+
- **Product URLs resolve themselves.** `resolveProductPath` answers a stale slug, a renamed
|
|
80
|
+
product's previous slug and another language's slug with one 308 to the current address. Product
|
|
81
|
+
renames never need a redirect entry.
|
|
82
|
+
- **The XML sitemap** is `/sitemap.xml`, built by `src/app/sitemap.ts` from `STATIC_ROUTES`, the
|
|
83
|
+
fragments in `src/seo/routes.d/`, the published pages under `/pages/<route>` and the catalog.
|
|
84
|
+
- **`robots.txt`** is built by `src/app/robots.ts` from `NOINDEX_PATHS` and `NOINDEX_QUERY_KEYS`.
|
|
85
|
+
- **One noindex mechanism:** the metadata `robots` field. No `X-Robots-Tag` header anywhere.
|
|
86
|
+
- **Structured data** is Product + BreadcrumbList on a product page and exactly one BlogPosting on a
|
|
87
|
+
blog post, nothing else. Organization, WebSite, logo, `sameAs` and SearchAction are refused.
|
|
88
|
+
- **Rename redirects** live in `REDIRECTS` in `src/seo/redirects.ts` and work without a restart.
|
|
89
|
+
- **Preview posture.** A deployment without `FORGECART_PUBLIC_ORIGIN` — every preview — marks every
|
|
90
|
+
page noindex, emits relative canonicals, serves `robots.txt` as `Disallow: /` and an empty XML
|
|
91
|
+
sitemap. A published deployment gets its origin from `forgecart init --public-origin`.
|
|
92
|
+
- **One SDK client per language.** Changing the language of a shared client is the defect this
|
|
93
|
+
replaces.
|
|
94
|
+
- **Site verification.** The root layout reads the search-console verification placements with
|
|
95
|
+
`getSiteVerifications()` and passes them to `shellMetadata`, so every page serves them as
|
|
96
|
+
`<meta>` tags.
|
|
97
|
+
|
|
98
|
+
The platform files, and the symbols they export:
|
|
99
|
+
|
|
100
|
+
| File | What it gives you | Symbols |
|
|
101
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
102
|
+
| `src/lib/locale/grammar.ts` | The URL grammar: default language unprefixed, others under `/<xx>/`, reserved segments | `isLocaleSegment`, `isUnprefixedPath`, `parseLocalePath`, `stripLocalePrefix`, `localePrefixedPath` |
|
|
103
|
+
| `src/lib/locale/middleware-plan.ts` | The locale-rewrite decision the middleware applies | `planLocaleRewrite`, `localeRequestHeaders`, `hasServerAuthoredHeaders`, `LOCALE_REQUEST_HEADER` |
|
|
104
|
+
| `src/middleware.ts` | Rename redirects, the locale rewrite, the `Content-Language` header, visitor and campaign cookies | `middleware`, `config` |
|
|
105
|
+
| `src/lib/locale/request-binding.ts` | The request's language, resolved once per request | `getRequestLocale`, `RequestLocale` |
|
|
106
|
+
| `src/lib/locale/request-locale.ts` | Render, 308 to the unprefixed default, or 404 for a language the channel does not offer | `decideRequestLocale`, `LocaleDecision` |
|
|
107
|
+
| `src/lib/locale/localized-path.ts` | Paths in the current language, and the switcher's target | `localizedPath`, `switchedLocalePath`, `LocaleBinding` |
|
|
108
|
+
| `src/components/LocaleLink.tsx` | The internal link | `LocaleLink`, `LocaleLinkProps` |
|
|
109
|
+
| `src/components/LanguageSwitcher.tsx` | The language switcher | `LanguageSwitcher` |
|
|
110
|
+
| `src/lib/seo/metadata.ts` | Title, description, canonical, hreflang, robots, social and verification tags, composed | `routeMetadata`, `shellMetadata`, `entityMetadata`, `buildMetadata`, `buildShellMetadata`, `pageTitle`, `SiteVerificationMeta` |
|
|
111
|
+
| `src/lib/seo/alternates.ts` | Self-canonical and reciprocal hreflang with `x-default` | `buildAlternates`, `staticPathsByLocale`, `PathsByLocale` |
|
|
112
|
+
| `src/lib/seo/resolve-path.ts` | The product slug law: 308 for stale, previous and other-language slugs | `resolveProductPath`, `productPathsByLocale`, `PathResolution` |
|
|
113
|
+
| `src/lib/seo/noindex.ts` | Which pages and query states are noindex | `NOINDEX_PATHS`, `NOINDEX_QUERY_KEYS`, `shouldNoindex`, `routeRobots`, `deploymentRefusesIndexing`, `NOINDEX_ROBOTS` |
|
|
114
|
+
| `src/lib/seo/public-origin.ts` | The deployment's public origin, absent on previews | `getPublicOrigin`, `resolvePublicOrigin` |
|
|
115
|
+
| `src/app/sitemap.ts` | The XML sitemap at `/sitemap.xml` | `sitemap`, `dynamic` |
|
|
116
|
+
| `src/lib/seo/sitemap-entries.ts` | The route table and the rules for the XML sitemap's rows | `STATIC_ROUTES`, `StaticRoute`, `SCAFFOLDED_ROUTES_DIR`, `mergeScaffoldedRoutes`, `pageSitemapRoutes`, `MAX_SITEMAP_URLS` |
|
|
117
|
+
| `src/lib/seo/scaffolded-routes.ts` | Reads the `src/seo/routes.d/*.json` fragments | `readScaffoldedRoutes` |
|
|
118
|
+
| `src/app/robots.ts` | `robots.txt` | `robots`, `dynamic` |
|
|
119
|
+
| `src/lib/seo/json-ld.ts` | Product, BreadcrumbList and BlogPosting nodes, and their safe serialization | `productJsonLd`, `breadcrumbJsonLd`, `blogPostingJsonLd`, `serializeJsonLd`, `JsonLdNode` |
|
|
120
|
+
| `src/seo/redirects.ts` | The rename-redirect table you fill | `REDIRECTS`, `RedirectRule` |
|
|
121
|
+
| `src/lib/seo/redirect-plan.ts` | The redirect rule: exact match, chains collapsed, cycles refused | `planRedirect`, `MAX_REDIRECT_HOPS` |
|
|
122
|
+
| `src/lib/seo/site-verification.ts` | The search-console verification placements, read per request | `getSiteVerifications` |
|
|
123
|
+
| `src/lib/forgecart.ts` | One shop client per language, and the reads the XML sitemap and the product page use | `getShopClientForLocale`, `getShopClient`, `getProductBySlug`, `ProductWithSeo`, `getProductSeoEntries`, `getServedPageRoutes` |
|
|
124
|
+
| `src/app/layout.tsx` | `<html lang>`, the 308 and 404 decisions, and the metadata floor every page inherits | `generateMetadata`, `RootLayout` |
|
|
125
|
+
| `src/app/products/[slug]/page.tsx` | The product page: slug law, entity metadata, Product and BreadcrumbList | `generateMetadata`, `ProductDetailPage` |
|
|
126
|
+
| `src/lib/asset-alt.ts` | The `alt` text of an asset image | `getAssetAlt` |
|
|
127
|
+
|
|
128
|
+
## The deletion list
|
|
129
|
+
|
|
130
|
+
Each class below is improvised code that the platform now provides. For every class:
|
|
131
|
+
|
|
132
|
+
1. Find it with the sweep rows the class names.
|
|
133
|
+
2. Delete the files listed under **Delete:** — under whatever name this storefront used.
|
|
134
|
+
3. Remove the code listed under **Remove:** from the files that stay.
|
|
135
|
+
4. Use what **Replaced by:** names instead.
|
|
136
|
+
5. Add every deleted path to the DONE summary's `Deleted:` list.
|
|
137
|
+
|
|
138
|
+
**Who deletes.** The platform's storefront agent cannot delete files yet: its file tools are
|
|
139
|
+
`Read`, `Write`, `Edit`, `Glob` and `Grep`. A `DeleteFile` storefront tool is a tracked follow-up.
|
|
140
|
+
Until it ships, the **Delete:** steps are carried out by an agent or operator with file-deletion
|
|
141
|
+
capability on the workspace. Before the promote that carries the deletions, confirm with `Glob`
|
|
142
|
+
that every path on your `Deleted:` list is gone.
|
|
143
|
+
|
|
144
|
+
Rules that apply to every class:
|
|
145
|
+
|
|
146
|
+
- **Replace, never keep both.** An improvised helper left beside the platform one is a second source
|
|
147
|
+
of truth, and the next change calls the wrong one.
|
|
148
|
+
- **Never delete a platform file.** Every platform file appears in exactly one list of the refresh
|
|
149
|
+
report (`rendered`, `conflicts`, `adopted`, `upToDate`). A path on any of those lists is merged —
|
|
150
|
+
see the merge steps — never deleted.
|
|
151
|
+
- **Delete a file only when nothing imports it.** Grep for its module path first and move every
|
|
152
|
+
importer onto the platform helper in the same change.
|
|
153
|
+
|
|
154
|
+
### The sweep
|
|
155
|
+
|
|
156
|
+
Run every row on the whole storefront. `Grep` rows are extended regular expressions (the Grep tool
|
|
157
|
+
runs `grep -E`); escape them as JSON strings when you pass them. `Glob` rows are file patterns from
|
|
158
|
+
the storefront root. The platform matches listed on each row are platform files that match on
|
|
159
|
+
purpose — leave them. Every other match is improvised code this migration removes.
|
|
160
|
+
|
|
161
|
+
- **S1** Glob `middleware.*` — platform matches: none
|
|
162
|
+
- **S2** Glob `src/middleware.*` — platform matches: `src/middleware.ts`
|
|
163
|
+
- **S3** Glob `proxy.*` — platform matches: none
|
|
164
|
+
- **S4** Glob `src/proxy.*` — platform matches: none
|
|
165
|
+
- **S5** Grep `NextResponse\.rewrite` in `src` — platform matches: `src/middleware.ts`
|
|
166
|
+
- **S6** Grep `(function|const) (localizedPath|withLocale|buildHref|localizeHref|localePath)` in `src` — platform matches: `src/lib/locale/localized-path.ts`
|
|
167
|
+
- **S7** Grep `/\$\{(lang|locale|language)\}` in `src` — platform matches: `src/lib/locale/grammar.ts`
|
|
168
|
+
- **S8** Grep `from ['"]next/link['"]` in `src` — platform matches: `src/components/LocaleLink.tsx`
|
|
169
|
+
- **S9** Glob `src/lib/seo.*` — platform matches: none
|
|
170
|
+
- **S10** Grep `<link rel=['"](canonical|alternate)` in `src` — platform matches: `src/components/LanguageSwitcher.tsx`, `src/lib/seo/json-ld.ts`
|
|
171
|
+
- **S11** Grep `<html lang=['"]` in `src` — platform matches: `src/app/global-error.tsx`, `src/lib/locale/channel-locales-loader.ts`
|
|
172
|
+
- **S12** Glob `src/app/sitemap*` — platform matches: `src/app/sitemap.ts`
|
|
173
|
+
- **S13** Glob `public/sitemap*` — platform matches: none
|
|
174
|
+
- **S14** Glob `next-sitemap.config.*` — platform matches: none
|
|
175
|
+
- **S15** Glob `src/app/robots*` — platform matches: `src/app/robots.ts`
|
|
176
|
+
- **S16** Glob `public/robots.txt` — platform matches: none
|
|
177
|
+
- **S17** Grep `(redirects|headers)(\(\) *\{| *:)|i18n *:` in `next.config.js` — platform matches: none
|
|
178
|
+
- **S18** Grep `Response\.redirect\(` in `src` — platform matches: `src/middleware.ts`
|
|
179
|
+
- **S19** Grep `['"]Content-Language['"]` in `src` — platform matches: `src/middleware.ts`
|
|
180
|
+
- **S20** Grep `[hH]ttp-?[eE]quiv` in `src` — platform matches: none
|
|
181
|
+
- **S21** Grep `[?&](lang|locale)=` in `src` — platform matches: none
|
|
182
|
+
- **S22** Grep `[pP]arams\.(lang|locale)|\.get\(['"][A-Za-z_-]*(lang|locale|language)['"]\)` in `src` — platform matches: none
|
|
183
|
+
- **S23** Glob `src/components/language-switcher.*` — platform matches: none
|
|
184
|
+
- **S24** Grep `[Aa]ccept-[Ll]anguage|NEXT_LOCALE` in `src` — platform matches: none
|
|
185
|
+
- **S25** Grep `application/ld\+json` in `src` — platform matches: `src/app/products/[slug]/page.tsx`, `src/lib/seo/json-ld.ts`
|
|
186
|
+
- **S26** Grep `['"]@type['"]: *['"](Organization|WebSite)` in `src` — platform matches: none
|
|
187
|
+
- **S27** Grep `\.setLanguageCode\(` in `src` — platform matches: `src/lib/shop-session.ts`
|
|
188
|
+
- **S28** Grep `['"]X-Robots-Tag['"]` in `src` — platform matches: none
|
|
189
|
+
|
|
190
|
+
One check is by inspection: Glob `src/app/*` and read the top-level route directories. A
|
|
191
|
+
`[locale]`, `[lang]` or two-letter directory (`src/app/de/`) belongs to the `[locale]` class below.
|
|
192
|
+
|
|
193
|
+
### Improvised middleware locale stages
|
|
194
|
+
|
|
195
|
+
- **Find:** S1–S5. A second middleware or proxy file, or a rewrite outside `buildBaseResponse` in
|
|
196
|
+
`src/middleware.ts`.
|
|
197
|
+
- **Delete:** `middleware.ts`, `middleware.js`, `src/middleware.js`, `proxy.ts`, `src/proxy.ts`
|
|
198
|
+
- **Remove:** a language rewrite, redirect or matcher inside `src/middleware.ts` — by merging that
|
|
199
|
+
file (merge steps).
|
|
200
|
+
- **Replaced by:** the platform `src/middleware.ts` and `planLocaleRewrite`. That file is merged,
|
|
201
|
+
never deleted: it also carries the rename redirects and the visitor and campaign cookies.
|
|
202
|
+
|
|
203
|
+
### `localizedPath` twins and bare links
|
|
204
|
+
|
|
205
|
+
- **Find:** S6–S8. A link helper defined outside the platform files, a path built by hand as
|
|
206
|
+
`/${locale}/…`, or `next/link` imported anywhere but `LocaleLink`.
|
|
207
|
+
- **Delete:** `src/lib/localized-path.ts`
|
|
208
|
+
- **Remove:** hand-prefixed hrefs and bare `next/link` imports in components that stay.
|
|
209
|
+
- **Replaced by:** `LocaleLink` in JSX (`href` is the unprefixed path, `locale` is the request's
|
|
210
|
+
`LocaleBinding`) and `localizedPath(path, binding)` outside JSX. Trap: the platform file
|
|
211
|
+
`src/lib/locale/localized-path.ts` has the same basename as the usual twin. Keep it.
|
|
212
|
+
|
|
213
|
+
### Hand-written `seo.ts` metadata helpers
|
|
214
|
+
|
|
215
|
+
- **Find:** S9–S11. A metadata helper module, hand-written head tags, a literal `<html lang>`.
|
|
216
|
+
- **Delete:** `src/lib/seo.ts`
|
|
217
|
+
- **Remove:** hand-written `<link rel="canonical">` and `<link rel="alternate" hreflang>` tags,
|
|
218
|
+
static `export const metadata` objects, and `alternates`, `canonical` or `robots` values built
|
|
219
|
+
by hand in a route's metadata.
|
|
220
|
+
- **Replaced by:** `routeMetadata(...)` in every route's `generateMetadata`, `shellMetadata(...)` in
|
|
221
|
+
the root layout, and `entityMetadata(...)` for an entity (`src/lib/seo/metadata.ts`).
|
|
222
|
+
|
|
223
|
+
### XML sitemap emitters other than `src/app/sitemap.ts`
|
|
224
|
+
|
|
225
|
+
- **Find:** S12–S14.
|
|
226
|
+
- **Delete:** `src/app/sitemap.xml/route.ts`, `public/sitemap.xml`, `next-sitemap.config.js`
|
|
227
|
+
- **Remove:** a `postbuild` script that runs `next-sitemap`, and the `next-sitemap` dependency (see
|
|
228
|
+
`package.json` in the merge steps).
|
|
229
|
+
- **Replaced by:** `src/app/sitemap.ts`. A route handler at `src/app/sitemap.xml/route.ts` claims
|
|
230
|
+
the same URL as that file and must go.
|
|
231
|
+
|
|
232
|
+
### `robots.txt` files other than `src/app/robots.ts`
|
|
233
|
+
|
|
234
|
+
- **Find:** S15–S16.
|
|
235
|
+
- **Delete:** `public/robots.txt`, `src/app/robots.txt/route.ts`
|
|
236
|
+
- **Replaced by:** `src/app/robots.ts`. To keep a page out of search, make it noindex (see the
|
|
237
|
+
static pages below); never hand-edit a disallow list.
|
|
238
|
+
|
|
239
|
+
### Ad-hoc rename redirects and Pages Router `i18n`
|
|
240
|
+
|
|
241
|
+
- **Find:** S17–S18. `redirects()` or an `i18n` key in `next.config.js`, a redirect map in the old
|
|
242
|
+
middleware, a route handler that only redirects a moved page.
|
|
243
|
+
- **Delete:** `src/app/<old-path>/route.ts`
|
|
244
|
+
- **Remove:** the `redirects()` function and the `i18n` key from `next.config.js` (merge steps).
|
|
245
|
+
- **Replaced by:** one `REDIRECTS` entry per moved page (see the rename redirects below) and the
|
|
246
|
+
locale grammar.
|
|
247
|
+
|
|
248
|
+
### Duplicate `Content-Language` emitters
|
|
249
|
+
|
|
250
|
+
- **Find:** S17, S19–S20.
|
|
251
|
+
- **Delete:** no file.
|
|
252
|
+
- **Remove:** every `Content-Language` header set outside the rewrite branch of `src/middleware.ts`
|
|
253
|
+
— a `headers()` entry in `next.config.js`, a second `headers.set('Content-Language', …)`, a route
|
|
254
|
+
handler — and every `<meta httpEquiv="content-language">`.
|
|
255
|
+
- **Replaced by:** the rewrite branch of `src/middleware.ts` (`buildBaseResponse`). Do not add a
|
|
256
|
+
header to unprefixed pages: they carry none by design, and a second emitter produces two header
|
|
257
|
+
lines, which the promote gate reports as `SEO_DUPLICATE_LOCALE_HEADER`.
|
|
258
|
+
|
|
259
|
+
### `?lang=` and `?locale=` switchers
|
|
260
|
+
|
|
261
|
+
- **Find:** S21–S23.
|
|
262
|
+
- **Delete:** `src/components/language-switcher.tsx`
|
|
263
|
+
- **Remove:** every read of a `lang` or `locale` query parameter, and every link or `router.push`
|
|
264
|
+
that sets one.
|
|
265
|
+
- **Replaced by:** `LanguageSwitcher` (`src/components/LanguageSwitcher.tsx`), which
|
|
266
|
+
`src/components/Header.tsx` already renders. Trap: `language-switcher.tsx` and
|
|
267
|
+
`LanguageSwitcher.tsx` differ only in letter case. Delete the lowercase file by its exact path;
|
|
268
|
+
the platform file exports `LanguageSwitcher` and imports `switchedLocalePath`.
|
|
269
|
+
|
|
270
|
+
### Language state outside the URL
|
|
271
|
+
|
|
272
|
+
- **Find:** S24, plus any cookie or `localStorage` entry that stores the language and any redirect
|
|
273
|
+
of `/` by language.
|
|
274
|
+
- **Delete:** no file.
|
|
275
|
+
- **Remove:** the cookie and storage reads and writes, the `Accept-Language` negotiation, the
|
|
276
|
+
redirect.
|
|
277
|
+
- **Replaced by:** the URL: the default language unprefixed, every other language under `/<xx>/`.
|
|
278
|
+
|
|
279
|
+
### `[locale]` segments and per-language route copies
|
|
280
|
+
|
|
281
|
+
- **Find:** the inspection of `src/app/*` above.
|
|
282
|
+
- **Delete:** `src/app/[locale]/`, `src/app/[lang]/`, `src/app/de/`
|
|
283
|
+
- **Remove:** `generateStaticParams` over languages in the pages you keep.
|
|
284
|
+
- **Replaced by:** one route per page in the flat tree: `src/app/our-story/page.tsx` serves
|
|
285
|
+
`/our-story` and, through the middleware rewrite, `/de/our-story`. Move each page to its
|
|
286
|
+
unprefixed route before you delete the segment, and merge per-language copies into that one route:
|
|
287
|
+
it reads its copy in the request's language (`getRequestLocale()`). A route left in a two-letter
|
|
288
|
+
directory is reported as `SEO_RESERVED_LOCALE_SEGMENT`.
|
|
289
|
+
|
|
290
|
+
### Hand-built JSON-LD
|
|
291
|
+
|
|
292
|
+
- **Find:** S25–S26.
|
|
293
|
+
- **Delete:** no file.
|
|
294
|
+
- **Remove:** every hand-built `<script type="application/ld+json">` and every Organization,
|
|
295
|
+
WebSite, logo, `sameAs` or SearchAction node.
|
|
296
|
+
- **Replaced by:** `productJsonLd`, `breadcrumbJsonLd`, `blogPostingJsonLd` and `serializeJsonLd`
|
|
297
|
+
(`src/lib/seo/json-ld.ts`). Anything else is reported as `SEO_FORBIDDEN_JSONLD`.
|
|
298
|
+
|
|
299
|
+
### Language set on a shared SDK client
|
|
300
|
+
|
|
301
|
+
- **Find:** S27.
|
|
302
|
+
- **Delete:** no file.
|
|
303
|
+
- **Remove:** every `setLanguageCode(` call on a server-side client, and every module-level client
|
|
304
|
+
shared across languages.
|
|
305
|
+
- **Replaced by:** `getShopClient()` for the request's language and `getShopClientForLocale(locale)`
|
|
306
|
+
for an explicit one (`src/lib/forgecart.ts`). Trap: `src/lib/shop-session.ts` calls
|
|
307
|
+
`client.setLanguageCode(...)` on the shopper's browser session client when the language changes.
|
|
308
|
+
That is platform code; leave it.
|
|
309
|
+
|
|
310
|
+
### A second noindex mechanism
|
|
311
|
+
|
|
312
|
+
- **Find:** S17, S28.
|
|
313
|
+
- **Delete:** no file.
|
|
314
|
+
- **Remove:** every `X-Robots-Tag` header.
|
|
315
|
+
- **Replaced by:** the metadata `robots` field, which `routeMetadata` sets from `NOINDEX_PATHS`,
|
|
316
|
+
`NOINDEX_QUERY_KEYS`, the deployment's posture and the route's `contentIndexable`.
|
|
317
|
+
|
|
318
|
+
## Merge steps per platform file — replace, don't add
|
|
319
|
+
|
|
320
|
+
Every file the refresh listed under `conflicts` is merged with this procedure:
|
|
321
|
+
|
|
322
|
+
1. **Read it and list what the merchant customized:** markup, copy, styling, extra components,
|
|
323
|
+
extra helpers, non-SEO settings. Anything the deletion list covers is not a customization — it is
|
|
324
|
+
what the migration replaces.
|
|
325
|
+
2. **Adopt it** — take the platform version:
|
|
326
|
+
`CallAdminOperation { "name": "workspaceTemplate.adminRefreshWorkspaceTemplate", "args": { "input": { "adopt": ["<path>"] } } }`.
|
|
327
|
+
Adopt a file BEFORE you edit it in the turn, never after: adopting writes the live file at once,
|
|
328
|
+
and an edit made before it was based on the old content, so the promote reports it as a conflict.
|
|
329
|
+
Adopt several files in one call when you can.
|
|
330
|
+
3. **Re-apply the customizations from step 1 onto the platform version**, through the platform
|
|
331
|
+
helpers: links as `LocaleLink`, metadata through `routeMetadata`, structured data through
|
|
332
|
+
`json-ld.ts`. Never paste the old implementation back beside the new one.
|
|
333
|
+
4. **Record it.** With nothing to re-apply, the file is "Adopted as-is". Otherwise it is "Merged".
|
|
334
|
+
|
|
335
|
+
A merged file keeps appearing under `conflicts` in every later refresh: it now differs from the
|
|
336
|
+
platform version on purpose. An adopted-as-is file appears under `upToDate`.
|
|
337
|
+
|
|
338
|
+
### `src/middleware.ts`
|
|
339
|
+
|
|
340
|
+
- This is the one middleware. Keep the platform order inside `middleware()`: `planRedirect(...)`
|
|
341
|
+
with `REDIRECTS` first (308), then `planLocaleRewrite`, then the visitor and campaign cookie
|
|
342
|
+
stages.
|
|
343
|
+
- Merchant logic that is not about language or SEO (an access gate, extra response headers) goes
|
|
344
|
+
inside `middleware()` after the redirect stage. It never rewrites or redirects by language.
|
|
345
|
+
- Keep the platform `export const config` matcher. Never add a second `config`.
|
|
346
|
+
- Never set `Content-Language` or `X-Robots-Tag` here or anywhere else.
|
|
347
|
+
- A rename redirect the old middleware carried becomes a `REDIRECTS` entry, not code here.
|
|
348
|
+
|
|
349
|
+
### `src/app/layout.tsx`
|
|
350
|
+
|
|
351
|
+
- `RootLayout` stays `async` and reads `await getRequestLocale()`. It raises
|
|
352
|
+
`permanentRedirect(decision.to)` and `notFound()` in the shell, before any markup.
|
|
353
|
+
- `<html lang={binding.locale}>` — never a literal language.
|
|
354
|
+
- `generateMetadata` returns `shellMetadata({ shopName, channelResolved, siteVerifications })` with
|
|
355
|
+
`siteVerifications` from `await getSiteVerifications()`. No static `export const metadata`.
|
|
356
|
+
- Keep the development-only revision stamp: it is merged into `other`, which carries the
|
|
357
|
+
verification tags — never overwrite `other`.
|
|
358
|
+
- No `<Suspense>` above `<body>`. Keep `CartProvider` with
|
|
359
|
+
`getBrowserShopConfig(binding.locale)` and `<Header locale={binding} languageCodes={languageCodes} />`.
|
|
360
|
+
- Re-apply the merchant's header and footer chrome and copy; every internal link is
|
|
361
|
+
`<LocaleLink href="/unprefixed-path" locale={binding}>`.
|
|
362
|
+
|
|
363
|
+
### `src/app/products/[slug]/page.tsx`
|
|
364
|
+
|
|
365
|
+
- Keep the slug law in the page shell: `resolveProductPath({ requestedSlug, product, binding })`,
|
|
366
|
+
`notFound()` for a missing product, `permanentRedirect(resolution.to)` for a redirect.
|
|
367
|
+
- Keep the one product read shared by `generateMetadata` and the page (`productForRequest`).
|
|
368
|
+
- `generateMetadata` returns `routeMetadata` with `pathsByLocale: productPathsByLocale(product)`,
|
|
369
|
+
title, description and social image from `entityMetadata(product, binding.locale)`, and
|
|
370
|
+
`contentIndexable` from the resolution.
|
|
371
|
+
- Structured data only through `productJsonLd`, `breadcrumbJsonLd` and `serializeJsonLd`, gated on
|
|
372
|
+
`routeRobots` exactly as the platform file does it.
|
|
373
|
+
- Re-apply the merchant's display markup inside `ProductDetail`: exactly one `<h1>`, images with
|
|
374
|
+
`alt={getAssetAlt(asset)}`.
|
|
375
|
+
- Delete language-specific slug lookups: the shop API resolves slugs per language, including a
|
|
376
|
+
product's previous slug.
|
|
377
|
+
|
|
378
|
+
### `src/lib/forgecart.ts`
|
|
379
|
+
|
|
380
|
+
- One shop client per language: `getShopClient()` for the request's language,
|
|
381
|
+
`getShopClientForLocale(locale)` for an explicit one. No module-level shared client, no
|
|
382
|
+
`setLanguageCode(`.
|
|
383
|
+
- Keep `getProductBySlug` returning `ProductWithSeo`, `getProductSeoEntries` and
|
|
384
|
+
`getServedPageRoutes`: `src/app/sitemap.ts` and the product page import them.
|
|
385
|
+
- Re-apply merchant reads as functions on `await getShopClient()`, through the SDK's typed methods.
|
|
386
|
+
|
|
387
|
+
### `next.config.js`
|
|
388
|
+
|
|
389
|
+
- Remove `redirects()` (its entries become `REDIRECTS` entries), every `headers()` entry that sets
|
|
390
|
+
`Content-Language` or `X-Robots-Tag`, and the Pages Router `i18n` key.
|
|
391
|
+
- Keep `cacheComponents: false` as an explicit key. Next re-enables an absent key, and with the
|
|
392
|
+
option on, the root layout can no longer answer its 308 and 404.
|
|
393
|
+
- Keep the platform's `output`, `serverExternalPackages`, `allowedDevOrigins`, `experimental` and
|
|
394
|
+
`turbopack` settings.
|
|
395
|
+
- Re-apply merchant settings that are not about language or SEO — for example a narrower
|
|
396
|
+
`images.remotePatterns`.
|
|
397
|
+
|
|
398
|
+
### Every other file in `conflicts`
|
|
399
|
+
|
|
400
|
+
Typical ones: `src/app/page.tsx`, `src/app/products/page.tsx`, `src/components/Header.tsx`,
|
|
401
|
+
`src/components/ProductCard.tsx`, `package.json`. The same procedure applies:
|
|
402
|
+
|
|
403
|
+
- links as `LocaleLink`; route metadata through `routeMetadata` with
|
|
404
|
+
`staticPathsByLocale(languageCodes, '<route>')`; images with `alt={getAssetAlt(asset)}`;
|
|
405
|
+
- `package.json`: adopt it. Adopting keeps every package the storefront declares that the platform
|
|
406
|
+
version does not, and takes the platform's range for a package both declare. Then delete the
|
|
407
|
+
packages the deletion list removed, such as `next-sitemap`, from it; the promote reinstalls the
|
|
408
|
+
dependencies. A `package.json` that still declares a package of the storefront's is Merged, with
|
|
409
|
+
those packages as its customizations.
|
|
410
|
+
|
|
411
|
+
### Routes the platform does not ship
|
|
412
|
+
|
|
413
|
+
A merchant route (for example `src/app/our-story/page.tsx`) is not in the refresh report. Migrate
|
|
414
|
+
it in place: `generateMetadata` returns `routeMetadata(...)`, every internal link is a
|
|
415
|
+
`LocaleLink`, the page has exactly one `<h1>`, and it joins the XML sitemap (next section). A route
|
|
416
|
+
that sets `other` in its metadata passes `siteVerifications: await getSiteVerifications()` to
|
|
417
|
+
`routeMetadata` and merges its own keys into the returned `other` — otherwise it replaces the
|
|
418
|
+
verification tags the root layout serves.
|
|
419
|
+
|
|
420
|
+
### The data tables — never adopt them
|
|
421
|
+
|
|
422
|
+
`src/seo/redirects.ts` (`REDIRECTS`) and `src/lib/seo/sitemap-entries.ts` (`STATIC_ROUTES`) are
|
|
423
|
+
tables. Never adopt either once it carries a row you or the merchant added: adopting replaces the
|
|
424
|
+
table with the platform's. `src/seo/redirects.ts` with your rows appears under `conflicts` in every
|
|
425
|
+
later refresh — expected. Do not add rows to `STATIC_ROUTES` in this migration: pages join the XML
|
|
426
|
+
sitemap through fragments (next section), which keeps `src/lib/seo/sitemap-entries.ts` identical to
|
|
427
|
+
the platform version, so later refreshes keep updating it.
|
|
428
|
+
|
|
429
|
+
## Existing rename redirects and static pages
|
|
430
|
+
|
|
431
|
+
### Rename redirects
|
|
432
|
+
|
|
433
|
+
1. Collect every rename the old storefront served: `redirects()` in `next.config.js`, redirect
|
|
434
|
+
maps in the old middleware, route handlers that only redirect a moved page (sweep rows S17, S18).
|
|
435
|
+
2. For each moved page, add one entry to `REDIRECTS` in `src/seo/redirects.ts`:
|
|
436
|
+
`{ from: '/old-path', to: '/new-path' }`.
|
|
437
|
+
- Paths are rooted and unprefixed. `/old-path` covers `/de/old-path` too, and each redirects
|
|
438
|
+
inside its own language. Never write a prefixed path: it covers one language only.
|
|
439
|
+
- Matching is exact: one entry per moved page. Chains collapse into one 308, and cycles are
|
|
440
|
+
refused.
|
|
441
|
+
- Routes that exist once per store (`/sitemap.xml`, `/robots.txt`, `/api/…`, `/ping`,
|
|
442
|
+
`/verify`, `/reset-password`) are never redirected.
|
|
443
|
+
3. **Product renames are never `REDIRECTS` entries.** The shop API keeps a product's previous slug,
|
|
444
|
+
and `resolveProductPath` answers it with a 308. Drop the old product redirect instead of moving
|
|
445
|
+
it.
|
|
446
|
+
4. Remove the old mechanism in the same change (deletion list).
|
|
447
|
+
5. A page that only moved out of a `[locale]` segment or a two-letter directory keeps its URLs —
|
|
448
|
+
`/de/our-story` is now served by `src/app/our-story/page.tsx` through the rewrite — and needs no
|
|
449
|
+
entry. A default-language URL the old storefront served under a prefix (`/en/our-story`) is
|
|
450
|
+
redirected by the platform; it needs no entry either.
|
|
451
|
+
|
|
452
|
+
### Static pages
|
|
453
|
+
|
|
454
|
+
1. Every page that is not a platform file and not an ACF page joins the XML sitemap with one
|
|
455
|
+
fragment file, `src/seo/routes.d/<page>.json`:
|
|
456
|
+
|
|
457
|
+
```json
|
|
458
|
+
{ "path": "/our-story", "indexable": true }
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
- `path` is rooted and unprefixed. One file per page; the file name names the page.
|
|
462
|
+
- A malformed fragment is skipped and named in the server log.
|
|
463
|
+
- A path already in `STATIC_ROUTES` keeps the platform's row; a fragment cannot override it.
|
|
464
|
+
|
|
465
|
+
2. A session or funnel page (sign-in, account, order status) gets `"indexable": false` in its
|
|
466
|
+
fragment and `contentIndexable: false` in its `routeMetadata(...)`.
|
|
467
|
+
3. The page's `generateMetadata` has the shape of the one in `src/app/products/page.tsx`:
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
export async function generateMetadata({
|
|
471
|
+
searchParams,
|
|
472
|
+
}: {
|
|
473
|
+
searchParams: Promise<RouteSearchParams>;
|
|
474
|
+
}): Promise<Metadata> {
|
|
475
|
+
const [resolvedSearchParams, { binding, languageCodes, shopName, channelResolved }] =
|
|
476
|
+
await Promise.all([searchParams, getRequestLocale()]);
|
|
477
|
+
return routeMetadata({
|
|
478
|
+
binding,
|
|
479
|
+
shopName,
|
|
480
|
+
channelResolved,
|
|
481
|
+
pathname: '/our-story',
|
|
482
|
+
pathsByLocale: staticPathsByLocale(languageCodes, '/our-story'),
|
|
483
|
+
searchParams: resolvedSearchParams,
|
|
484
|
+
title: 'Our story',
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
4. ACF pages under `/pages/<route>` join the XML sitemap by themselves. Write no fragment for them.
|
|
490
|
+
5. A page in a two-letter top-level directory moves to a name of three letters or more, with a
|
|
491
|
+
`REDIRECTS` entry for the old path.
|
|
492
|
+
|
|
493
|
+
### SEO source mapping for imported page-kind definitions
|
|
494
|
+
|
|
495
|
+
A route that renders the records of a page-kind definition the merchant imported — a blog, a
|
|
496
|
+
journal, a lookbook, for example `src/app/blog/[slug]/page.tsx` — maps its SEO sources at the route:
|
|
497
|
+
its `generateMetadata` passes the record's own fields, in the request's language, to
|
|
498
|
+
`routeMetadata`. The target is no `SEO_UNMAPPED_PAGE_KIND` on those routes.
|
|
499
|
+
|
|
500
|
+
ACF pages the platform serves at `/pages/<route>` render the page's name as their title and no
|
|
501
|
+
description, so they report `SEO_UNMAPPED_PAGE_KIND` (a WARN) whatever you do in this migration. Do
|
|
502
|
+
not edit `src/app/pages/[slug]/page.tsx` to silence it; list those routes under Remaining WARNs.
|
|
503
|
+
|
|
504
|
+
Sign off every imported definition against this table:
|
|
505
|
+
|
|
506
|
+
| Element | Where it comes from | How you check it |
|
|
507
|
+
| ---------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
508
|
+
| Title tag | `routeMetadata({ title })` from the record's title field; the shop name is appended | `<title>` on every language's URL; `SEO_TITLE_MISSING` |
|
|
509
|
+
| Meta description | `routeMetadata({ description })` from the record's excerpt field — never the body | `<meta name="description">` on every language's URL; `SEO_UNMAPPED_PAGE_KIND` |
|
|
510
|
+
| H1 | Exactly one `<h1>`, carrying the text the title comes from | `SEO_H1_MISSING`, `SEO_H1_DUPLICATE` |
|
|
511
|
+
| Slug | The route path, unprefixed; a changed code-page path gets a `REDIRECTS` entry | `SEO_RENAME_WITHOUT_REDIRECT` |
|
|
512
|
+
| Excerpt (dek) | Rendered as the intro paragraph under the `<h1>`, and the same words as the description | The snapshot of each language's page |
|
|
513
|
+
| Social opt-in | `socialImage` only when the record has a stored social image; otherwise leave it out | No `og:*` or `twitter:*` tags without one |
|
|
514
|
+
| Canonical | Produced by `routeMetadata`; never written by hand | `SEO_CANONICAL_MISSING_OR_FOREIGN` on the published deployment |
|
|
515
|
+
| Author | `authorName` in `blogPostingJsonLd(...)` | The BlogPosting node |
|
|
516
|
+
| Dates | `publishedAt` and `updatedAt` in `blogPostingJsonLd(...)`: the shop API's ISO-8601 stamps | `SEO_SCHEMA_BLOGPOSTING_INVALID` |
|
|
517
|
+
| BlogPosting | Exactly one node per post, from `blogPostingJsonLd(...)` and `serializeJsonLd(...)` | `SEO_SCHEMA_BLOGPOSTING_INVALID` |
|
|
518
|
+
| Alt | `alt={getAssetAlt(asset)}` for asset images; an embed's own `alt` for body images | `SEO_IMG_ALT_MISSING` |
|
|
519
|
+
| Indexability | `contentIndexable` in `routeMetadata(...)`, and `indexable` in the page's fragment | `SEO_NOINDEX_MISSING` |
|
|
520
|
+
|
|
521
|
+
### Alt-text backfill
|
|
522
|
+
|
|
523
|
+
Migrated media arrive with no alt text: an image rendered through `getAssetAlt(asset)` renders
|
|
524
|
+
`alt=""` until its asset is described.
|
|
525
|
+
|
|
526
|
+
1. Describe each content image once, on its asset — one value for every language:
|
|
527
|
+
`CallAdminOperation { "name": "asset.updateAsset", "args": { "input": { "id": "<asset id>", "altText": "<what the image shows>" } } }`.
|
|
528
|
+
The operator can do the same in the dashboard's media library.
|
|
529
|
+
2. Start with hero and featured images, then images inside bodies. An image embedded in a rich-text
|
|
530
|
+
body carries its own `alt`, per language, in that body.
|
|
531
|
+
3. Decorative images stay `alt=""`. Never fill alt text with a file name or a product name.
|
|
532
|
+
|
|
533
|
+
Until the backfill is done, `SEO_IMG_ALT_MISSING` WARNs on the routes you change are expected and
|
|
534
|
+
do not block the migration. List the remaining ones in the DONE summary.
|
|
535
|
+
|
|
536
|
+
## Verification — the crawler assertions and the per-locale browser round
|
|
537
|
+
|
|
538
|
+
Every step runs on the final tree, after the promote that applied it. Read the channel's languages
|
|
539
|
+
first: `CallAdminOperation { "name": "channel.channels", "args": { "options": { "take": 1 } } }`
|
|
540
|
+
returns `defaultLanguageCode` and `availableLanguageCodes`. The examples use `en` as the default and
|
|
541
|
+
`de` as another language.
|
|
542
|
+
|
|
543
|
+
### 1. The sweep is clean
|
|
544
|
+
|
|
545
|
+
Run every sweep row (S1–S28) again. Each returns exactly its platform matches. Then check:
|
|
546
|
+
|
|
547
|
+
- every path on your `Deleted:` list is gone;
|
|
548
|
+
- no path on your `Deleted:` list appears in any list of the refresh report.
|
|
549
|
+
|
|
550
|
+
### 2. The crawler assertions — the promote gate
|
|
551
|
+
|
|
552
|
+
Every promote runs the platform's SEO gate: it crawls the routes the promote changed, in every
|
|
553
|
+
language, on the storefront's own server, plus `/`, `/robots.txt` and `/sitemap.xml`. It is the
|
|
554
|
+
crawler that verifies this migration; `SEO_RED` rolls the promote back and names each finding with
|
|
555
|
+
its route.
|
|
556
|
+
|
|
557
|
+
- Findings on the routes the promote changed are RED. Findings on routes it did not change, on the
|
|
558
|
+
links out of `/`, and four codes on every route (`SEO_H1_MISSING`, `SEO_H1_DUPLICATE`,
|
|
559
|
+
`SEO_IMG_ALT_MISSING`, `SEO_UNMAPPED_PAGE_KIND`) are WARN. `/robots.txt` and `/sitemap.xml` are
|
|
560
|
+
always RED.
|
|
561
|
+
- It plans at most 25 changed routes per promote and names the ones it skipped. Migrate merchant
|
|
562
|
+
routes in batches of at most 25, after the promote that carries the deletions and the platform
|
|
563
|
+
merges.
|
|
564
|
+
- **The preview asserts nothing about its address.** It has no public origin — whether it is the
|
|
565
|
+
local `<code>.127.0.0.1.nip.io:8081` host or a hosted preview — so every page is noindex, canonicals
|
|
566
|
+
are relative, `robots.txt` is `Disallow: /` and the XML sitemap is empty. That is correct. The
|
|
567
|
+
gate therefore SKIPS `SEO_CANONICAL_MISSING_OR_FOREIGN`, `SEO_HREFLANG_SET_INCOMPLETE`,
|
|
568
|
+
`SEO_XDEFAULT_MISSING` and `SEO_SITEMAP_ROUTE_MISSING` there and prints why. A skip is not a pass:
|
|
569
|
+
step 6 checks them on the published deployment. A wrong claim is still reported on the preview —
|
|
570
|
+
a foreign canonical, a `?lang=` hreflang, a set that is not reciprocated.
|
|
571
|
+
- Fix a RED at the named route before anything else, and never re-promote an unchanged tree hoping
|
|
572
|
+
it passes: the gate is deterministic.
|
|
573
|
+
|
|
574
|
+
| Code | Severity on a changed route | What it means after a migration | Fix |
|
|
575
|
+
| ---------------------------------- | -------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
576
|
+
| `SEO_HTML_LANG_MISMATCH` | RED | `<html lang>` is a literal or ignores the URL's language | `<html lang={binding.locale}>` in `src/app/layout.tsx` |
|
|
577
|
+
| `SEO_CANONICAL_MISSING_OR_FOREIGN` | RED | A hand-written or missing canonical | `routeMetadata(...)`; delete the hand-written tag |
|
|
578
|
+
| `SEO_HREFLANG_NOT_RECIPROCAL` | RED | Language versions list different alternate sets | `staticPathsByLocale` or `productPathsByLocale` for `pathsByLocale` |
|
|
579
|
+
| `SEO_HREFLANG_SET_INCOMPLETE` | RED on catalog routes, else WARN | The alternate set omits a language the store serves | Build `pathsByLocale` from `languageCodes` |
|
|
580
|
+
| `SEO_HREFLANG_QUERY_PARAM` | RED | An alternate points at `?lang=` | The deletion list's `?lang=` class |
|
|
581
|
+
| `SEO_XDEFAULT_MISSING` | RED on catalog routes, else WARN | Hand-built alternates without `x-default` | `routeMetadata(...)` |
|
|
582
|
+
| `SEO_LOCALE_PREFIX_NOT_REDIRECTED` | RED | `/en/…` answers 200: an improvised stage or segment serves it | Delete the improvised stage or `[locale]` segment |
|
|
583
|
+
| `SEO_LOCALE_ORPHAN_LINK` | RED | A link leaves its language: a hand-built or bare link remains | `LocaleLink` or `localizedPath` |
|
|
584
|
+
| `SEO_TITLE_MISSING` | RED | A route emits no title | `generateMetadata` returning `routeMetadata({ title })` |
|
|
585
|
+
| `SEO_RESERVED_LOCALE_SEGMENT` | RED | A route sits in a two-letter or infrastructure directory | Move it to a longer name, with a `REDIRECTS` entry |
|
|
586
|
+
| `SEO_RENAME_WITHOUT_REDIRECT` | RED | A moved or removed route has no redirect | A `REDIRECTS` entry |
|
|
587
|
+
| `SEO_NOINDEX_MISSING` | RED | A session, funnel or query-state page is indexable | `contentIndexable: false`; delete a second noindex mechanism |
|
|
588
|
+
| `SEO_FORBIDDEN_JSONLD` | RED | Hand-built or forbidden structured data | `json-ld.ts` builders only |
|
|
589
|
+
| `SEO_SITEMAP_INVALID` | RED | An improvised emitter answers `/sitemap.xml`, or it fails | Delete the emitter; merge `src/lib/forgecart.ts` |
|
|
590
|
+
| `SEO_SITEMAP_ROUTE_MISSING` | RED | An indexable route has no XML sitemap row | A `src/seo/routes.d/<page>.json` fragment |
|
|
591
|
+
| `SEO_ROUTE_UNREACHABLE` | RED | A changed route does not answer 200 | `TailServerLogs`: usually an import of a deleted file or missing export |
|
|
592
|
+
| `SEO_DUPLICATE_LOCALE_HEADER` | RED | Two `Content-Language` values on one response | Delete the duplicate emitter |
|
|
593
|
+
| `SEO_SCHEMA_BLOGPOSTING_INVALID` | RED | A blog post's node is missing, doubled or unreadable | One `blogPostingJsonLd(...)` node |
|
|
594
|
+
| `SEO_H1_MISSING` | WARN | No `<h1>` | Exactly one `<h1>` |
|
|
595
|
+
| `SEO_H1_DUPLICATE` | WARN | More than one `<h1>` | Exactly one `<h1>` |
|
|
596
|
+
| `SEO_IMG_ALT_MISSING` | WARN | An image has no alt text | The alt-text backfill |
|
|
597
|
+
| `SEO_UNMAPPED_PAGE_KIND` | WARN | A page-kind route renders no description of its own | The route-level SEO source mapping |
|
|
598
|
+
|
|
599
|
+
### 3. Route probes
|
|
600
|
+
|
|
601
|
+
`ProbeRoute` requests one route on the storefront's server without following redirects and reports
|
|
602
|
+
the status and the start of the body. Probe:
|
|
603
|
+
|
|
604
|
+
| Route | Expected |
|
|
605
|
+
| ---------------------------------------------------- | -------------------------------------------------------- |
|
|
606
|
+
| `/` | 200; the `<html` tag carries `lang="en"` |
|
|
607
|
+
| `/de` | 200; `lang="de"` |
|
|
608
|
+
| `/de/products` | 200; `lang="de"` |
|
|
609
|
+
| `/en/products` | 308 — the default language is never prefixed |
|
|
610
|
+
| `/zz/products`, with `zz` not offered | 404 |
|
|
611
|
+
| `/products?lang=de` | 200; `lang="en"` — a query parameter selects no language |
|
|
612
|
+
| Every `from` path in `REDIRECTS`, and its `/de` form | 308 |
|
|
613
|
+
| `/sitemap.xml` | 200; empty on the preview |
|
|
614
|
+
| `/robots.txt` | 200; `Disallow: /` on the preview |
|
|
615
|
+
|
|
616
|
+
`ProbeRoute` does not report headers. `Content-Language` is covered twice: in the source by sweep
|
|
617
|
+
row S19, whose only match is `src/middleware.ts`, and on the wire by the promote gate
|
|
618
|
+
(`SEO_DUPLICATE_LOCALE_HEADER`). The expected wire state: exactly one `Content-Language: de` on
|
|
619
|
+
`/de` and every `/de/…` page, and none on an unprefixed page.
|
|
620
|
+
|
|
621
|
+
### 4. The per-locale browser round
|
|
622
|
+
|
|
623
|
+
After the promote — the preview serves the promoted tree — `browser_navigate` to every language's
|
|
624
|
+
URL of every route the migration touched: the default language unprefixed, every other language
|
|
625
|
+
under its prefix. For each one, `browser_snapshot` and confirm:
|
|
626
|
+
|
|
627
|
+
- the page shows that language's own copy, not the default language's;
|
|
628
|
+
- `html lang` equals that language;
|
|
629
|
+
- the language switcher's target is the same page in the other language;
|
|
630
|
+
- every internal link stays in the language: `/de/…` on a German page, unprefixed on a
|
|
631
|
+
default-language page.
|
|
632
|
+
|
|
633
|
+
### 5. The refresh settles
|
|
634
|
+
|
|
635
|
+
After the last promote, call the refresh operation once more with an empty `adopt` list. Without
|
|
636
|
+
an `adopt` list it never overwrites a file that differs from the platform version. Expect:
|
|
637
|
+
|
|
638
|
+
- `rendered` and `adopted` are empty;
|
|
639
|
+
- `conflicts` is exactly your Merged list, plus `src/seo/redirects.ts` when you added redirects;
|
|
640
|
+
- every Adopted-as-is file and `SEO-MIGRATION.md` are in `upToDate`.
|
|
641
|
+
|
|
642
|
+
A file in `conflicts` that is not on your Merged list was never merged: merge it.
|
|
643
|
+
|
|
644
|
+
### 6. Publish, then check the published deployment
|
|
645
|
+
|
|
646
|
+
Ask the operator to publish the storefront; an agent cannot deploy. On the published deployment:
|
|
647
|
+
|
|
648
|
+
- `robots.txt` names `Sitemap: <origin>/sitemap.xml` and disallows the noindex paths;
|
|
649
|
+
- `/sitemap.xml` lists every indexable route in every language;
|
|
650
|
+
- every indexable page carries an absolute canonical and a complete hreflang set with `x-default`;
|
|
651
|
+
- every page serves the search-console verification `<meta>` tags. Until the published deployment
|
|
652
|
+
serves them, the platform records `SEO_VERIFICATION_PLACEMENT_NOT_SERVED` for the connected
|
|
653
|
+
search console. That is expected and is not a failed verification (`SITE_NOT_VERIFIED`); it
|
|
654
|
+
clears on the platform's next sync after the publish.
|
|
655
|
+
|
|
656
|
+
### How the platform tests this document
|
|
657
|
+
|
|
658
|
+
The `retrofit` phase, the last phase of the platform's storefront crawler harness
|
|
659
|
+
(`tool/storefront-verify`), follows this document on a storefront built by the real CLI. It turns
|
|
660
|
+
that storefront into an improvised pre-SEO one: ten defects drawn from the deletion list, and no
|
|
661
|
+
`.forgecart/template-manifest.json`, like a provisioned storefront. It runs the refresh with no
|
|
662
|
+
`adopt` list. It then applies a DONE summary in the form of the last section, in this document's
|
|
663
|
+
order: the deletions; one adopting refresh of the merged and adopted files, before any edit to
|
|
664
|
+
them; the re-applied customizations; a rename moved into `src/seo/redirects.ts`; a merchant page
|
|
665
|
+
migrated in place and joined by a `src/seo/routes.d/<page>.json` fragment; the final refresh. Its
|
|
666
|
+
49 assertions run in three stages:
|
|
667
|
+
|
|
668
|
+
- `migrated` (5) reads the tree and the three refresh reports. The first refresh renders this
|
|
669
|
+
document and passes the applicability test above. The deletion list names every class the
|
|
670
|
+
storefront carried (`retrofit-the-document-names-every-deletion-class`). No improvisation
|
|
671
|
+
survives beside its replacement. The final refresh settles exactly as step 5 expects
|
|
672
|
+
(`retrofit-the-final-refresh-is-settled`).
|
|
673
|
+
- `served` (43) serves the migrated storefront with one `next dev` boot. There is one
|
|
674
|
+
`Content-Language` emitter, the migrated page stays in its language, `?lang=` selects nothing,
|
|
675
|
+
the moved rename answers 308 in every language, and the XML sitemap lists the migrated page with
|
|
676
|
+
the hreflang set the page emits. Every re-applied customization still acts, and the crawler's 37
|
|
677
|
+
locale and SEO assertions pass again, each as `retrofit:<id>`.
|
|
678
|
+
- `restored` (1) proves that the harness's workspace is back byte for byte, whether the phase
|
|
679
|
+
passed or failed (`retrofit-the-workspace-is-restored-byte-for-byte`).
|
|
680
|
+
|
|
681
|
+
It is a platform test: your workspace does not contain it, and nothing here asks you to run it.
|
|
682
|
+
|
|
683
|
+
## The DONE summary lists the deleted files
|
|
684
|
+
|
|
685
|
+
Report the migration in exactly this form:
|
|
686
|
+
|
|
687
|
+
```text
|
|
688
|
+
SEO migration — DONE
|
|
689
|
+
Deleted:
|
|
690
|
+
- <every removed path, one per line>
|
|
691
|
+
Merged (platform version taken, customizations re-applied):
|
|
692
|
+
- <path> — <the customizations re-applied>
|
|
693
|
+
Adopted as-is:
|
|
694
|
+
- <path>
|
|
695
|
+
Redirects added to src/seo/redirects.ts:
|
|
696
|
+
- /old-path → /new-path
|
|
697
|
+
Routes joined the XML sitemap:
|
|
698
|
+
- src/seo/routes.d/<page>.json — /<page>, indexable: true|false
|
|
699
|
+
Remaining WARNs:
|
|
700
|
+
- <CODE> at <url> — <why it remains>
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
- The summary lists every deleted file: `Deleted:` names every path the migration removed, and
|
|
704
|
+
nothing else. `src/middleware.ts` and every other platform file belong under Merged or Adopted
|
|
705
|
+
as-is, never under `Deleted:`.
|
|
706
|
+
- A summary whose `Deleted:` list is empty has replaced nothing: either the sweep found no
|
|
707
|
+
improvised code — then say so and quote it — or the migration is not done.
|
|
708
|
+
- Write "none" under a heading that has no entries. Never leave a heading out.
|