@forgecart/cli 2.202610251457.0 → 2.202610260916.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgecart/cli",
3
- "version": "2.202610251457.0",
3
+ "version": "2.202610260916.0",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding and operating ForgeCart channel storefronts",
6
6
  "bin": {
@@ -79,8 +79,9 @@ file; your job is to call it, never to reimplement it.
79
79
  - **Product URLs resolve themselves.** `resolveProductPath` answers a stale slug, a renamed
80
80
  product's previous slug and another language's slug with one 308 to the current address. Product
81
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.
82
+ - **The XML sitemap** is `/sitemap.xml`, built by `src/app/sitemap.ts` from `STATIC_ROUTES` in
83
+ `src/seo/static-routes.ts`, the fragments in `src/seo/routes.d/`, the published pages under
84
+ `/pages/<route>` and the catalog, through the rules in `src/lib/seo/sitemap-entries.ts`.
84
85
  - **`robots.txt`** is built by `src/app/robots.ts` from `NOINDEX_PATHS` and `NOINDEX_QUERY_KEYS`.
85
86
  - **One noindex mechanism:** the metadata `robots` field. No `X-Robots-Tag` header anywhere.
86
87
  - **Structured data** is Product + BreadcrumbList on a product page and exactly one BlogPosting on a
@@ -113,7 +114,8 @@ The platform files, and the symbols they export:
113
114
  | `src/lib/seo/noindex.ts` | Which pages and query states are noindex | `NOINDEX_PATHS`, `NOINDEX_QUERY_KEYS`, `shouldNoindex`, `routeRobots`, `deploymentRefusesIndexing`, `NOINDEX_ROBOTS` |
114
115
  | `src/lib/seo/public-origin.ts` | The deployment's public origin, absent on previews | `getPublicOrigin`, `resolvePublicOrigin` |
115
116
  | `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/seo/static-routes.ts` | The route table: every non-entity route the storefront serves | `STATIC_ROUTES` |
118
+ | `src/lib/seo/sitemap-entries.ts` | The rules for the XML sitemap's rows | `StaticRoute`, `SCAFFOLDED_ROUTES_DIR`, `mergeScaffoldedRoutes`, `pageSitemapRoutes`, `MAX_SITEMAP_URLS` |
117
119
  | `src/lib/seo/scaffolded-routes.ts` | Reads the `src/seo/routes.d/*.json` fragments | `readScaffoldedRoutes` |
118
120
  | `src/app/robots.ts` | `robots.txt` | `robots`, `dynamic` |
119
121
  | `src/lib/seo/json-ld.ts` | Product, BreadcrumbList and BlogPosting nodes, and their safe serialization | `productJsonLd`, `breadcrumbJsonLd`, `blogPostingJsonLd`, `serializeJsonLd`, `JsonLdNode` |
@@ -419,12 +421,13 @@ verification tags the root layout serves.
419
421
 
420
422
  ### The data tables — never adopt them
421
423
 
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.
424
+ `src/seo/redirects.ts` (`REDIRECTS`) and `src/seo/static-routes.ts` (`STATIC_ROUTES`) are tables.
425
+ Never adopt either once it carries a row you or the merchant added: adopting replaces the table
426
+ with the platform's. `src/seo/redirects.ts` with your rows appears under `conflicts` in every later
427
+ refresh — expected. Do not add rows to `STATIC_ROUTES` in this migration: pages join the XML
428
+ sitemap through fragments (next section), which keeps `src/seo/static-routes.ts` identical to the
429
+ platform version, so later refreshes keep updating it. `src/lib/seo/sitemap-entries.ts` holds the
430
+ rules that read the table and no routes: never add one there.
428
431
 
429
432
  ## Existing rename redirects and static pages
430
433
 
@@ -460,7 +463,8 @@ the platform version, so later refreshes keep updating it.
460
463
 
461
464
  - `path` is rooted and unprefixed. One file per page; the file name names the page.
462
465
  - 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.
466
+ - A path already in `STATIC_ROUTES` (`src/seo/static-routes.ts`) keeps the platform's row; a
467
+ fragment cannot override it.
464
468
 
465
469
  2. A session or funnel page (sign-in, account, order status) gets `"indexable": false` in its
466
470
  fragment and `contentIndexable: false` in its `routeMetadata(...)`.
@@ -7,13 +7,13 @@ import { readScaffoldedRoutes } from '../lib/seo/scaffolded-routes';
7
7
  import { createSitemapCache, sitemapWindowMs } from '../lib/seo/sitemap-cache';
8
8
  import {
9
9
  MAX_SITEMAP_URLS,
10
- STATIC_ROUTES,
11
10
  collectProductSitemapUrls,
12
11
  mergeScaffoldedRoutes,
13
12
  pageSitemapRoutes,
14
13
  sitemapBudgetWarning,
15
14
  staticSitemapUrls,
16
15
  } from '../lib/seo/sitemap-entries';
16
+ import { STATIC_ROUTES } from '../seo/static-routes';
17
17
 
18
18
  /**
19
19
  * `sitemap.xml` (#1347, epic launch#54 W1-9) — the discovery half of the SEO
@@ -61,12 +61,13 @@ async function build(): Promise<MetadataRoute.Sitemap> {
61
61
  const { channel } = await resolveChannelLocales(null);
62
62
  const { languageCodes, defaultLanguageCode } = channel;
63
63
 
64
- // The template's own routes, every page scaffolded since, and every ACF page
65
- // the shop says it currently SERVES (#1934). Read per build rather than at
66
- // module load, so a page created a moment ago is in the document on the next
67
- // window — the reason `createStorefrontPage` writes a row at all, and the
68
- // reason the ACF feed is a live query rather than a file: an ACF page is
69
- // published through the API and writes nothing to this pod's disk.
64
+ // The row's own routes (`src/seo/static-routes.ts`, #3478), every page
65
+ // scaffolded since, and every ACF page the shop says it currently SERVES
66
+ // (#1934). Read per build rather than at module load, so a page created a
67
+ // moment ago is in the document on the next window — the reason
68
+ // `createStorefrontPage` writes a row at all, and the reason the ACF feed is
69
+ // a live query rather than a file: an ACF page is published through the API
70
+ // and writes nothing to this pod's disk.
70
71
  //
71
72
  // PRECEDENCE falls out of the merge and is worth naming: the base table wins
72
73
  // over both, and within the appended list the FIRST claimant of a path wins —
@@ -12,6 +12,11 @@ import type { LocaleBinding } from '../locale/localized-path';
12
12
  * that disagreed with the pages it lists is worse than no sitemap, because a
13
13
  * crawler reconciles the two and trusts neither. Sharing the builder makes
14
14
  * disagreement impossible rather than unlikely.
15
+ *
16
+ * The rules only, never the routes (#3478): which routes a row serves is that
17
+ * row's own table, `STATIC_ROUTES` in `src/seo/static-routes.ts`, handed in by
18
+ * the caller — so a row with fewer pages lists its own routes and this module
19
+ * stays identical in every row of the template set.
15
20
  */
16
21
 
17
22
  /** A route the template ships, as opposed to a catalog entity. */
@@ -21,46 +26,24 @@ export interface StaticRoute {
21
26
  * Whether the route belongs in the XML sitemap.
22
27
  *
23
28
  * Kept as data rather than derived from `NOINDEX_PATHS` so a scaffolded page
24
- * can join this table with its own answer (#1347's
25
- * `storefront-page-creation.service` append). A unit vector pins the two
26
- * against each other so they cannot drift.
29
+ * can join its row's route table with its own answer (#1347's
30
+ * `storefront-page-creation.service` append). Two unit laws pin the two
31
+ * against each other so they cannot drift: every route a table lists as
32
+ * non-indexable is one `NOINDEX_PATHS` claims, and every `NOINDEX_PATHS`
33
+ * entry a table lists is non-indexable.
27
34
  */
28
35
  indexable: boolean;
29
36
  }
30
37
 
31
- /**
32
- * Every non-entity route the storefront serves.
33
- *
34
- * Non-indexable routes stay listed: this table is the single source of truth
35
- * for what routes EXIST, and dropping them would make "is this route missing
36
- * or merely unlisted?" unanswerable when a page is scaffolded into it.
37
- */
38
- export const STATIC_ROUTES: readonly StaticRoute[] = [
39
- { path: '/', indexable: true },
40
- { path: '/products', indexable: true },
41
- { path: '/cart', indexable: false },
42
- { path: '/checkout', indexable: false },
43
- // The account-link routes (#1472). Listed-but-suppressed rather than absent,
44
- // for this table's stated reason: a page scaffolded at `/verify` later must
45
- // find the route already claimed and non-indexable, instead of adding a
46
- // fragment that submits a token page for indexing.
47
- { path: '/verify', indexable: false },
48
- { path: '/reset-password', indexable: false },
49
- // The register page (#1471). Suppressed for the same reason `/cart` is — a
50
- // form with no content of its own — and listed for this table's stated one.
51
- { path: '/register', indexable: false },
52
- // The account page (#1093): sign-in form or greeting, one shopper's own.
53
- { path: '/account', indexable: false },
54
- ];
55
-
56
38
  /**
57
39
  * The directory a scaffolded page drops its row into (#1347).
58
40
  *
59
- * One file per page rather than an append to the table above, and that is the
60
- * whole reason it is a directory: two pages scaffolded at once would both read
61
- * the table, both append, and the second write would erase the first. Separate
62
- * files cannot lose each other's writes, and re-scaffolding the same page
63
- * rewrites its own file instead of adding a duplicate row.
41
+ * One file per page rather than an append to the row's `STATIC_ROUTES`
42
+ * (`src/seo/static-routes.ts`), and that is the whole reason it is a directory:
43
+ * two pages scaffolded at once would both read the table, both append, and the
44
+ * second write would erase the first. Separate files cannot lose each other's
45
+ * writes, and re-scaffolding the same page rewrites its own file instead of
46
+ * adding a duplicate row.
64
47
  */
65
48
  export const SCAFFOLDED_ROUTES_DIR = 'src/seo/routes.d';
66
49
 
@@ -81,7 +64,8 @@ export function parseScaffoldedRoute(raw: unknown): StaticRoute | null {
81
64
  }
82
65
 
83
66
  /**
84
- * The template's own routes plus whatever has been scaffolded since.
67
+ * The row's own routes (its `STATIC_ROUTES`) plus whatever has been scaffolded
68
+ * since.
85
69
  *
86
70
  * The base table WINS every collision, and that is load-bearing rather than a
87
71
  * tie-break: `/cart` is non-indexable because this template decided so, and a
@@ -212,18 +196,21 @@ export function sitemapBudgetWarning(urlCount: number): string | null {
212
196
  }
213
197
 
214
198
  /**
215
- * One `<url>` per (route × language), each carrying the cluster's full
216
- * alternate set.
199
+ * One URL per (indexable route × offered language), the default one
200
+ * unprefixed; `routes` is the caller's table.
217
201
  *
218
202
  * Per language rather than per route because each locale's URL is a distinct
219
- * address a crawler must discover; the shared alternate set is what tells it
220
- * they are one page in several languages rather than several pages.
203
+ * address a crawler must discover; each `<url>` carries the cluster's full
204
+ * alternate set, which is what tells the crawler they are one page in several
205
+ * languages rather than several pages. `routes` has no default: the rules know
206
+ * no row, so a caller that forgot its table cannot quietly advertise another
207
+ * row's routes.
221
208
  */
222
209
  export function staticSitemapUrls(
223
210
  languageCodes: readonly string[],
224
211
  defaultLocale: string,
225
212
  publicOrigin: string,
226
- routes: readonly StaticRoute[] = STATIC_ROUTES,
213
+ routes: readonly StaticRoute[],
227
214
  ): SitemapUrl[] {
228
215
  const urls: SitemapUrl[] = [];
229
216
  for (const route of routes) {
@@ -0,0 +1,26 @@
1
+ import type { StaticRoute } from '../lib/seo/sitemap-entries';
2
+
3
+ /**
4
+ * Every non-entity route THIS ROW serves (#1347, launch#54 W1-9): the row's
5
+ * existence table. Non-indexable routes stay listed — the table is the single
6
+ * source of truth for what routes EXIST. `src/app/sitemap.ts` merges the
7
+ * scaffolded fragments of `src/seo/routes.d` onto it and hands the result to
8
+ * `staticSitemapUrls`; a fragment can add a route and never overrule one.
9
+ */
10
+ export const STATIC_ROUTES: readonly StaticRoute[] = [
11
+ { path: '/', indexable: true },
12
+ { path: '/products', indexable: true },
13
+ { path: '/cart', indexable: false },
14
+ { path: '/checkout', indexable: false },
15
+ // The account-link routes (#1472). Listed-but-suppressed rather than absent,
16
+ // for this table's stated reason: a page scaffolded at `/verify` later must
17
+ // find the route already claimed and non-indexable, instead of adding a
18
+ // fragment that submits a token page for indexing.
19
+ { path: '/verify', indexable: false },
20
+ { path: '/reset-password', indexable: false },
21
+ // The register page (#1471). Suppressed for the same reason `/cart` is — a
22
+ // form with no content of its own — and listed for this table's stated one.
23
+ { path: '/register', indexable: false },
24
+ // The account page (#1093): sign-in form or greeting, one shopper's own.
25
+ { path: '/account', indexable: false },
26
+ ];
@@ -79,8 +79,9 @@ file; your job is to call it, never to reimplement it.
79
79
  - **Product URLs resolve themselves.** `resolveProductPath` answers a stale slug, a renamed
80
80
  product's previous slug and another language's slug with one 308 to the current address. Product
81
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.
82
+ - **The XML sitemap** is `/sitemap.xml`, built by `src/app/sitemap.ts` from `STATIC_ROUTES` in
83
+ `src/seo/static-routes.ts`, the fragments in `src/seo/routes.d/`, the published pages under
84
+ `/pages/<route>` and the catalog, through the rules in `src/lib/seo/sitemap-entries.ts`.
84
85
  - **`robots.txt`** is built by `src/app/robots.ts` from `NOINDEX_PATHS` and `NOINDEX_QUERY_KEYS`.
85
86
  - **One noindex mechanism:** the metadata `robots` field. No `X-Robots-Tag` header anywhere.
86
87
  - **Structured data** is Product + BreadcrumbList on a product page and exactly one BlogPosting on a
@@ -113,7 +114,8 @@ The platform files, and the symbols they export:
113
114
  | `src/lib/seo/noindex.ts` | Which pages and query states are noindex | `NOINDEX_PATHS`, `NOINDEX_QUERY_KEYS`, `shouldNoindex`, `routeRobots`, `deploymentRefusesIndexing`, `NOINDEX_ROBOTS` |
114
115
  | `src/lib/seo/public-origin.ts` | The deployment's public origin, absent on previews | `getPublicOrigin`, `resolvePublicOrigin` |
115
116
  | `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/seo/static-routes.ts` | The route table: every non-entity route the storefront serves | `STATIC_ROUTES` |
118
+ | `src/lib/seo/sitemap-entries.ts` | The rules for the XML sitemap's rows | `StaticRoute`, `SCAFFOLDED_ROUTES_DIR`, `mergeScaffoldedRoutes`, `pageSitemapRoutes`, `MAX_SITEMAP_URLS` |
117
119
  | `src/lib/seo/scaffolded-routes.ts` | Reads the `src/seo/routes.d/*.json` fragments | `readScaffoldedRoutes` |
118
120
  | `src/app/robots.ts` | `robots.txt` | `robots`, `dynamic` |
119
121
  | `src/lib/seo/json-ld.ts` | Product, BreadcrumbList and BlogPosting nodes, and their safe serialization | `productJsonLd`, `breadcrumbJsonLd`, `blogPostingJsonLd`, `serializeJsonLd`, `JsonLdNode` |
@@ -419,12 +421,13 @@ verification tags the root layout serves.
419
421
 
420
422
  ### The data tables — never adopt them
421
423
 
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.
424
+ `src/seo/redirects.ts` (`REDIRECTS`) and `src/seo/static-routes.ts` (`STATIC_ROUTES`) are tables.
425
+ Never adopt either once it carries a row you or the merchant added: adopting replaces the table
426
+ with the platform's. `src/seo/redirects.ts` with your rows appears under `conflicts` in every later
427
+ refresh — expected. Do not add rows to `STATIC_ROUTES` in this migration: pages join the XML
428
+ sitemap through fragments (next section), which keeps `src/seo/static-routes.ts` identical to the
429
+ platform version, so later refreshes keep updating it. `src/lib/seo/sitemap-entries.ts` holds the
430
+ rules that read the table and no routes: never add one there.
428
431
 
429
432
  ## Existing rename redirects and static pages
430
433
 
@@ -460,7 +463,8 @@ the platform version, so later refreshes keep updating it.
460
463
 
461
464
  - `path` is rooted and unprefixed. One file per page; the file name names the page.
462
465
  - 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.
466
+ - A path already in `STATIC_ROUTES` (`src/seo/static-routes.ts`) keeps the platform's row; a
467
+ fragment cannot override it.
464
468
 
465
469
  2. A session or funnel page (sign-in, account, order status) gets `"indexable": false` in its
466
470
  fragment and `contentIndexable: false` in its `routeMetadata(...)`.
@@ -7,13 +7,13 @@ import { readScaffoldedRoutes } from '../lib/seo/scaffolded-routes';
7
7
  import { createSitemapCache, sitemapWindowMs } from '../lib/seo/sitemap-cache';
8
8
  import {
9
9
  MAX_SITEMAP_URLS,
10
- STATIC_ROUTES,
11
10
  collectProductSitemapUrls,
12
11
  mergeScaffoldedRoutes,
13
12
  pageSitemapRoutes,
14
13
  sitemapBudgetWarning,
15
14
  staticSitemapUrls,
16
15
  } from '../lib/seo/sitemap-entries';
16
+ import { STATIC_ROUTES } from '../seo/static-routes';
17
17
 
18
18
  /**
19
19
  * `sitemap.xml` (#1347, epic launch#54 W1-9) — the discovery half of the SEO
@@ -61,12 +61,13 @@ async function build(): Promise<MetadataRoute.Sitemap> {
61
61
  const { channel } = await resolveChannelLocales(null);
62
62
  const { languageCodes, defaultLanguageCode } = channel;
63
63
 
64
- // The template's own routes, every page scaffolded since, and every ACF page
65
- // the shop says it currently SERVES (#1934). Read per build rather than at
66
- // module load, so a page created a moment ago is in the document on the next
67
- // window — the reason `createStorefrontPage` writes a row at all, and the
68
- // reason the ACF feed is a live query rather than a file: an ACF page is
69
- // published through the API and writes nothing to this pod's disk.
64
+ // The row's own routes (`src/seo/static-routes.ts`, #3478), every page
65
+ // scaffolded since, and every ACF page the shop says it currently SERVES
66
+ // (#1934). Read per build rather than at module load, so a page created a
67
+ // moment ago is in the document on the next window — the reason
68
+ // `createStorefrontPage` writes a row at all, and the reason the ACF feed is
69
+ // a live query rather than a file: an ACF page is published through the API
70
+ // and writes nothing to this pod's disk.
70
71
  //
71
72
  // PRECEDENCE falls out of the merge and is worth naming: the base table wins
72
73
  // over both, and within the appended list the FIRST claimant of a path wins —
@@ -12,6 +12,11 @@ import type { LocaleBinding } from '../locale/localized-path';
12
12
  * that disagreed with the pages it lists is worse than no sitemap, because a
13
13
  * crawler reconciles the two and trusts neither. Sharing the builder makes
14
14
  * disagreement impossible rather than unlikely.
15
+ *
16
+ * The rules only, never the routes (#3478): which routes a row serves is that
17
+ * row's own table, `STATIC_ROUTES` in `src/seo/static-routes.ts`, handed in by
18
+ * the caller — so a row with fewer pages lists its own routes and this module
19
+ * stays identical in every row of the template set.
15
20
  */
16
21
 
17
22
  /** A route the template ships, as opposed to a catalog entity. */
@@ -21,46 +26,24 @@ export interface StaticRoute {
21
26
  * Whether the route belongs in the XML sitemap.
22
27
  *
23
28
  * Kept as data rather than derived from `NOINDEX_PATHS` so a scaffolded page
24
- * can join this table with its own answer (#1347's
25
- * `storefront-page-creation.service` append). A unit vector pins the two
26
- * against each other so they cannot drift.
29
+ * can join its row's route table with its own answer (#1347's
30
+ * `storefront-page-creation.service` append). Two unit laws pin the two
31
+ * against each other so they cannot drift: every route a table lists as
32
+ * non-indexable is one `NOINDEX_PATHS` claims, and every `NOINDEX_PATHS`
33
+ * entry a table lists is non-indexable.
27
34
  */
28
35
  indexable: boolean;
29
36
  }
30
37
 
31
- /**
32
- * Every non-entity route the storefront serves.
33
- *
34
- * Non-indexable routes stay listed: this table is the single source of truth
35
- * for what routes EXIST, and dropping them would make "is this route missing
36
- * or merely unlisted?" unanswerable when a page is scaffolded into it.
37
- */
38
- export const STATIC_ROUTES: readonly StaticRoute[] = [
39
- { path: '/', indexable: true },
40
- { path: '/products', indexable: true },
41
- { path: '/cart', indexable: false },
42
- { path: '/checkout', indexable: false },
43
- // The account-link routes (#1472). Listed-but-suppressed rather than absent,
44
- // for this table's stated reason: a page scaffolded at `/verify` later must
45
- // find the route already claimed and non-indexable, instead of adding a
46
- // fragment that submits a token page for indexing.
47
- { path: '/verify', indexable: false },
48
- { path: '/reset-password', indexable: false },
49
- // The register page (#1471). Suppressed for the same reason `/cart` is — a
50
- // form with no content of its own — and listed for this table's stated one.
51
- { path: '/register', indexable: false },
52
- // The account page (#1093): sign-in form or greeting, one shopper's own.
53
- { path: '/account', indexable: false },
54
- ];
55
-
56
38
  /**
57
39
  * The directory a scaffolded page drops its row into (#1347).
58
40
  *
59
- * One file per page rather than an append to the table above, and that is the
60
- * whole reason it is a directory: two pages scaffolded at once would both read
61
- * the table, both append, and the second write would erase the first. Separate
62
- * files cannot lose each other's writes, and re-scaffolding the same page
63
- * rewrites its own file instead of adding a duplicate row.
41
+ * One file per page rather than an append to the row's `STATIC_ROUTES`
42
+ * (`src/seo/static-routes.ts`), and that is the whole reason it is a directory:
43
+ * two pages scaffolded at once would both read the table, both append, and the
44
+ * second write would erase the first. Separate files cannot lose each other's
45
+ * writes, and re-scaffolding the same page rewrites its own file instead of
46
+ * adding a duplicate row.
64
47
  */
65
48
  export const SCAFFOLDED_ROUTES_DIR = 'src/seo/routes.d';
66
49
 
@@ -81,7 +64,8 @@ export function parseScaffoldedRoute(raw: unknown): StaticRoute | null {
81
64
  }
82
65
 
83
66
  /**
84
- * The template's own routes plus whatever has been scaffolded since.
67
+ * The row's own routes (its `STATIC_ROUTES`) plus whatever has been scaffolded
68
+ * since.
85
69
  *
86
70
  * The base table WINS every collision, and that is load-bearing rather than a
87
71
  * tie-break: `/cart` is non-indexable because this template decided so, and a
@@ -212,18 +196,21 @@ export function sitemapBudgetWarning(urlCount: number): string | null {
212
196
  }
213
197
 
214
198
  /**
215
- * One `<url>` per (route × language), each carrying the cluster's full
216
- * alternate set.
199
+ * One URL per (indexable route × offered language), the default one
200
+ * unprefixed; `routes` is the caller's table.
217
201
  *
218
202
  * Per language rather than per route because each locale's URL is a distinct
219
- * address a crawler must discover; the shared alternate set is what tells it
220
- * they are one page in several languages rather than several pages.
203
+ * address a crawler must discover; each `<url>` carries the cluster's full
204
+ * alternate set, which is what tells the crawler they are one page in several
205
+ * languages rather than several pages. `routes` has no default: the rules know
206
+ * no row, so a caller that forgot its table cannot quietly advertise another
207
+ * row's routes.
221
208
  */
222
209
  export function staticSitemapUrls(
223
210
  languageCodes: readonly string[],
224
211
  defaultLocale: string,
225
212
  publicOrigin: string,
226
- routes: readonly StaticRoute[] = STATIC_ROUTES,
213
+ routes: readonly StaticRoute[],
227
214
  ): SitemapUrl[] {
228
215
  const urls: SitemapUrl[] = [];
229
216
  for (const route of routes) {
@@ -0,0 +1,26 @@
1
+ import type { StaticRoute } from '../lib/seo/sitemap-entries';
2
+
3
+ /**
4
+ * Every non-entity route THIS ROW serves (#1347, launch#54 W1-9): the row's
5
+ * existence table. Non-indexable routes stay listed — the table is the single
6
+ * source of truth for what routes EXIST. `src/app/sitemap.ts` merges the
7
+ * scaffolded fragments of `src/seo/routes.d` onto it and hands the result to
8
+ * `staticSitemapUrls`; a fragment can add a route and never overrule one.
9
+ */
10
+ export const STATIC_ROUTES: readonly StaticRoute[] = [
11
+ { path: '/', indexable: true },
12
+ { path: '/products', indexable: true },
13
+ { path: '/cart', indexable: false },
14
+ { path: '/checkout', indexable: false },
15
+ // The account-link routes (#1472). Listed-but-suppressed rather than absent,
16
+ // for this table's stated reason: a page scaffolded at `/verify` later must
17
+ // find the route already claimed and non-indexable, instead of adding a
18
+ // fragment that submits a token page for indexing.
19
+ { path: '/verify', indexable: false },
20
+ { path: '/reset-password', indexable: false },
21
+ // The register page (#1471). Suppressed for the same reason `/cart` is — a
22
+ // form with no content of its own — and listed for this table's stated one.
23
+ { path: '/register', indexable: false },
24
+ // The account page (#1093): sign-in form or greeting, one shopper's own.
25
+ { path: '/account', indexable: false },
26
+ ];