@forgecart/cli 2.202610260010.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 +1 -1
- package/templates/storefront/SEO-MIGRATION.md +14 -10
- package/templates/storefront/src/app/sitemap.ts +8 -7
- package/templates/storefront/src/lib/seo/sitemap-entries.ts +26 -39
- package/templates/storefront/src/seo/static-routes.ts +26 -0
- package/templates/storefront-shadcn/SEO-MIGRATION.md +14 -10
- package/templates/storefront-shadcn/src/app/sitemap.ts +8 -7
- package/templates/storefront-shadcn/src/lib/seo/sitemap-entries.ts +26 -39
- package/templates/storefront-shadcn/src/seo/static-routes.ts +26 -0
package/package.json
CHANGED
|
@@ -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
|
|
83
|
-
fragments in `src/seo/routes.d/`, the published pages under
|
|
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/
|
|
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/
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
sitemap through fragments (next section), which keeps `src/
|
|
427
|
-
|
|
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
|
|
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
|
|
65
|
-
// the shop says it currently SERVES
|
|
66
|
-
// module load, so a page created a
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
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
|
|
25
|
-
* `storefront-page-creation.service` append).
|
|
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
|
|
60
|
-
* whole reason it is a directory:
|
|
61
|
-
* the table, both append, and the
|
|
62
|
-
* files cannot lose each other's
|
|
63
|
-
* rewrites its own file instead of
|
|
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
|
|
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
|
|
216
|
-
*
|
|
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;
|
|
220
|
-
* they are one page in several
|
|
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[]
|
|
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
|
|
83
|
-
fragments in `src/seo/routes.d/`, the published pages under
|
|
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/
|
|
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/
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
sitemap through fragments (next section), which keeps `src/
|
|
427
|
-
|
|
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
|
|
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
|
|
65
|
-
// the shop says it currently SERVES
|
|
66
|
-
// module load, so a page created a
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
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
|
|
25
|
-
* `storefront-page-creation.service` append).
|
|
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
|
|
60
|
-
* whole reason it is a directory:
|
|
61
|
-
* the table, both append, and the
|
|
62
|
-
* files cannot lose each other's
|
|
63
|
-
* rewrites its own file instead of
|
|
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
|
|
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
|
|
216
|
-
*
|
|
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;
|
|
220
|
-
* they are one page in several
|
|
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[]
|
|
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
|
+
];
|