@warlock.js/sitemap 5.19.1 → 5.21.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.
@@ -1,174 +1,203 @@
1
- ---
2
- name: sitemap-overview
3
- description: "@warlock.js/sitemap — overview in @warlock.js/sitemap; use when you need to sitemap overview."
4
- ---
5
-
6
- # `@warlock.js/sitemap` — overview
7
-
8
- A sitemap builder that knows nothing about any framework. Entries in, a valid
9
- sitemaps.org document out. **Zero runtime dependencies.** Generation only; no
10
- remote sitemap parser.
11
-
12
- ## It does not import Warlock
13
-
14
- There is no connector, no config module, no lifecycle and no page registry in
15
- this package. It owns the sitemap PROTOCOL and every byte it emits, and
16
- nothing else. `@warlock.js/web` is a CALLER of it, exactly like an Express app
17
- or a cron script is — which is what makes the same three lines work everywhere.
18
-
19
- If you are looking for page discovery, `sitemap: false` exclusion, locale
20
- expansion, the `/sitemap.xml` route or `robots.txt`, those belong to
21
- `@warlock.js/web`, not here.
22
-
23
- ## The whole common case
24
-
25
- ```ts
26
- import { Sitemap } from "@warlock.js/sitemap";
27
-
28
- const sitemap = new Sitemap({
29
- baseUrl: "https://example.com",
30
- changefreq: "weekly",
31
- priority: 0.5,
32
- });
33
-
34
- sitemap.add({ path: "/" });
35
-
36
- declare const posts: { slug: string; updatedAt: Date }[];
37
-
38
- for (const post of posts) {
39
- sitemap.add({
40
- name: "post-details",
41
- route: "/posts/:id",
42
- path: `/posts/${post.slug}`,
43
- lastmod: post.updatedAt,
44
- priority: 0.8,
45
- });
46
- }
47
-
48
- const xml = sitemap.toXML();
49
-
50
- await sitemap.saveTo("public/sitemap.xml");
51
- ```
52
-
53
- ## `baseUrl` is validated in the CONSTRUCTOR
54
-
55
- A `Sitemap` that cannot produce a valid URL should not exist. Anything that is
56
- not an absolute `http(s)` URL throws `InvalidBaseUrlError` at construction, so
57
- a typo surfaces at the line that wrote it rather than at the first request.
58
- `new URL()` accepts `mailto:` and `file:` happily, so the protocol is checked
59
- explicitly.
60
-
61
- This replaces the 5.15.0 config key, its `PUBLIC_APP_URL` env fallback and its
62
- boot refusal entirely: the origin is an argument now, so it cannot be missing.
63
-
64
- ## The entry
65
-
66
- Only `path` is required, and it must be a **concrete path** — `/posts/123`,
67
- never `/posts/:id`. A pattern is not a URL.
68
-
69
- | Field | Meaning |
70
- | --- | --- |
71
- | `path` | Required. Resolved against `baseUrl`; an absolute URL is used as given. |
72
- | `name` | Optional route label, for your own diagnostics. Never serialised. |
73
- | `route` | Optional pattern this URL came from. Feeds `routes()` and `duplicates()`. |
74
- | `lastmod` | `Date` → W3C datetime. A string is passed through UNTOUCHED, never re-parsed. |
75
- | `changefreq` | One of the seven protocol values; anything else throws. |
76
- | `priority` | `0.0`–`1.0`; anything else throws. |
1
+ ---
2
+ name: sitemap-overview
3
+ description: 'Front-door orientation for `@warlock.js/sitemap`: framework-blind `Sitemap` and streaming `SitemapIndex` builders, URL metadata, hreflang, Google image entries, sharding, diagnostics, and atomic publication. Use when adding or debugging sitemap output; Warlock page discovery and `/sitemap.xml` routing belong to `@warlock.js/web`.'
4
+ ---
5
+
6
+ # `@warlock.js/sitemap` — overview
7
+
8
+ A sitemap builder that knows nothing about any framework. Entries in, a valid
9
+ sitemaps.org document out. **Zero runtime dependencies.** Generation only; no
10
+ remote sitemap parser.
11
+
12
+ ## It does not import Warlock
13
+
14
+ There is no connector, no config module, no lifecycle and no page registry in
15
+ this package. It owns the sitemap PROTOCOL and every byte it emits, and
16
+ nothing else. `@warlock.js/web` is a CALLER of it, exactly like an Express app
17
+ or a cron script is — which is what makes the same three lines work everywhere.
18
+
19
+ If you are looking for page discovery, `sitemap: false` exclusion, locale
20
+ expansion, the `/sitemap.xml` route or `robots.txt`, those belong to
21
+ `@warlock.js/web`, not here.
22
+
23
+ ## The whole common case
24
+
25
+ ```ts
26
+ import { Sitemap } from "@warlock.js/sitemap";
27
+
28
+ const sitemap = new Sitemap({
29
+ baseUrl: "https://example.com",
30
+ changefreq: "weekly",
31
+ priority: 0.5,
32
+ });
33
+
34
+ sitemap.add({ path: "/" });
35
+
36
+ declare const posts: { slug: string; updatedAt: Date }[];
37
+
38
+ for (const post of posts) {
39
+ sitemap.add({
40
+ name: "post-details",
41
+ route: "/posts/:id",
42
+ path: `/posts/${post.slug}`,
43
+ lastmod: post.updatedAt,
44
+ priority: 0.8,
45
+ });
46
+ }
47
+
48
+ const xml = sitemap.toXML();
49
+
50
+ await sitemap.saveTo("public/sitemap.xml");
51
+ ```
52
+
53
+ ## `baseUrl` is validated in the CONSTRUCTOR
54
+
55
+ A `Sitemap` that cannot produce a valid URL should not exist. Anything that is
56
+ not an absolute `http(s)` URL throws `InvalidBaseUrlError` at construction, so
57
+ a typo surfaces at the line that wrote it rather than at the first request.
58
+ `new URL()` accepts `mailto:` and `file:` happily, so the protocol is checked
59
+ explicitly.
60
+
61
+ This replaces the 5.15.0 config key, its `PUBLIC_APP_URL` env fallback and its
62
+ boot refusal entirely: the origin is an argument now, so it cannot be missing.
63
+
64
+ ## The entry
65
+
66
+ Only `path` is required, and it must be a **concrete path** — `/posts/123`,
67
+ never `/posts/:id`. A pattern is not a URL.
68
+
69
+ | Field | Meaning |
70
+ | --- | --- |
71
+ | `path` | Required. Resolved against `baseUrl`; an absolute URL is used as given. |
72
+ | `name` | Optional route label, for your own diagnostics. Never serialised. |
73
+ | `route` | Optional pattern this URL came from. Feeds `routes()` and `duplicates()`. |
74
+ | `lastmod` | `Date` → W3C datetime. A string is passed through UNTOUCHED, never re-parsed. |
75
+ | `changefreq` | One of the seven protocol values; anything else throws. |
76
+ | `priority` | `0.0`–`1.0`; anything else throws. |
77
77
  | `alternates` | `{ hreflang, path }[]` — language versions of this page. |
78
-
79
- Invalid entries throw `InvalidSitemapEntryError` from `add()`. Paths are
80
- normalised to carry a leading slash, so `a` and `/a` are ONE entry.
81
-
82
- ## Duplicates: silent, but not hidden
83
-
84
- Entries are stored keyed by `path`. Adding the same path twice keeps the later
85
- one, silently — a duplicate `<loc>` makes the document invalid, and two loops
86
- legitimately covering an overlapping set is the normal cause.
87
-
88
- `duplicates()` reports every collision with its count and the `route` of each
89
- contributing add, so a caller who cares can fail their own build:
90
-
91
- ```ts
92
- sitemap.duplicates();
93
- // [ { path: "/posts/1", count: 2, routes: ["/posts/:id", "/:slug"] } ]
94
- ```
95
-
96
- ## `routes()` and why `declareRoute()` exists
78
+ | `images` | `{ loc }[]` — absolute HTTP(S) image URLs, including verified external CDNs. |
79
+
80
+ Invalid entries throw `InvalidSitemapEntryError` from `add()`. Paths are
81
+ normalised to carry a leading slash, so `a` and `/a` are ONE entry.
82
+
83
+ ## Duplicates: silent, but not hidden
84
+
85
+ Entries are stored keyed by `path`. Adding the same path twice keeps the later
86
+ one, silently — a duplicate `<loc>` makes the document invalid, and two loops
87
+ legitimately covering an overlapping set is the normal cause.
88
+
89
+ `duplicates()` reports every collision with its count and the `route` of each
90
+ contributing add, so a caller who cares can fail their own build:
91
+
92
+ ```ts
93
+ sitemap.duplicates();
94
+ // [ { path: "/posts/1", count: 2, routes: ["/posts/:id", "/:slug"] } ]
95
+ ```
96
+
97
+ ## `routes()` and why `declareRoute()` exists
98
+
99
+ ```ts
100
+ sitemap.declareRoute("/products/:slug");
101
+
102
+ sitemap.routes();
103
+ // [ { route: "/posts/:id", count: 400 }, { route: "/products/:slug", count: 0 } ]
104
+ ```
105
+
106
+ **The zero is the interesting row.** It means a route you expected to
107
+ contribute URLs contributed none, and a whole section of the site is missing
108
+ from a document that otherwise looks perfect.
109
+
110
+ Without `declareRoute()` a route only becomes known by appearing on an entry,
111
+ so every route has at least one URL, `count: 0` can never occur and the
112
+ diagnostic is decorative. Declaring the pattern is what turns a silent loss
113
+ into a visible one.
114
+
115
+ **This package reports; it never prints.** Whether a zero is a warning or a
116
+ build failure is the caller's decision.
117
+
118
+ ## Alternates
119
+
120
+ `alternates` emits `<xhtml:link rel="alternate" hreflang="…">` inside each
121
+ `<url>`. Two rules:
122
+
123
+ 1. **Every language version is also its own `<url>`, carrying the complete
124
+ alternate set including itself.** Listing alternates on only one of them is
125
+ the usual way this ships broken.
126
+ 2. **The package does not know what a locale is.** `hreflang` is any string —
127
+ `en`, `en-GB`, `x-default` — and it never derives `/{locale}/…` for you.
128
+ Each path is supplied explicitly, which is the only thing that works when
129
+ slugs diverge between languages.
130
+
131
+ The `xhtml` namespace is declared only when something uses it. `SitemapIndex`
132
+ applies the same rule PER SHARD — a shard declares `xmlns:xhtml` only when its
133
+ own entries carry alternates — and an alternate's `<xhtml:link>` bytes count
134
+ toward that shard's byte ceiling exactly like the rest of its `<url>` block.
135
+
136
+ ## Images
137
+
138
+ Use `images` when a page has image assets to expose:
97
139
 
98
140
  ```ts
99
- sitemap.declareRoute("/products/:slug");
100
-
101
- sitemap.routes();
102
- // [ { route: "/posts/:id", count: 400 }, { route: "/products/:slug", count: 0 } ]
141
+ sitemap.add({
142
+ path: "/products/widget",
143
+ images: [{ loc: "https://cdn.example.com/widget.jpg" }],
144
+ });
103
145
  ```
104
146
 
105
- **The zero is the interesting row.** It means a route you expected to
106
- contribute URLs contributed none, and a whole section of the site is missing
107
- from a document that otherwise looks perfect.
108
-
109
- Without `declareRoute()` a route only becomes known by appearing on an entry,
110
- so every route has at least one URL, `count: 0` can never occur and the
111
- diagnostic is decorative. Declaring the pattern is what turns a silent loss
112
- into a visible one.
113
-
114
- **This package reports; it never prints.** Whether a zero is a warning or a
115
- build failure is the caller's decision.
116
-
117
- ## Alternates
147
+ Each image `loc` must be an absolute HTTP(S) URL. The builder emits Google's
148
+ `<image:image><image:loc>…</image:loc></image:image>` extension and declares
149
+ `xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"` only in
150
+ documents that need it. An external CDN URL is valid when that host is verified
151
+ for the site.
118
152
 
119
- `alternates` emits `<xhtml:link rel="alternate" hreflang="…">` inside each
120
- `<url>`. Two rules:
121
-
122
- 1. **Every language version is also its own `<url>`, carrying the complete
123
- alternate set including itself.** Listing alternates on only one of them is
124
- the usual way this ships broken.
125
- 2. **The package does not know what a locale is.** `hreflang` is any string —
126
- `en`, `en-GB`, `x-default` — and it never derives `/{locale}/…` for you.
127
- Each path is supplied explicitly, which is the only thing that works when
128
- slugs diverge between languages.
129
-
130
- The `xhtml` namespace is declared only when something uses it. `SitemapIndex`
131
- applies the same rule PER SHARD — a shard declares `xmlns:xhtml` only when its
132
- own entries carry alternates — and an alternate's `<xhtml:link>` bytes count
133
- toward that shard's byte ceiling exactly like the rest of its `<url>` block.
153
+ Google permits at most 1,000 images for one `<url>`. Extra images are dropped.
154
+ Pass `onImageLimitExceeded({ route, dropped })` in `SitemapOptions` to receive
155
+ the one diagnostic for that route; without it, the builder issues one
156
+ `console.warn` per route. Image XML and its namespace count toward the shard byte
157
+ ceiling.
134
158
 
135
159
  ## `toXML()` is pure, repeatable and synchronous
136
-
137
- Calling it twice returns the same string; it mutates nothing and drains
138
- nothing. It has no I/O and stays synchronous.
139
-
140
- ## The ceiling
141
-
142
- The protocol caps one file at **50,000 URLs or 50MB uncompressed**. `Sitemap`
143
- retains every entry — that is what makes `entries()` and a repeatable
144
- `toXML()` possible — so it is right up to that ceiling and wrong above it. A
145
- site past it needs the streaming writer, which retains nothing and emits shards
146
- plus an index.
147
-
160
+
161
+ Calling it twice returns the same string; it mutates nothing and drains
162
+ nothing. It has no I/O and stays synchronous.
163
+
164
+ ## The ceiling
165
+
166
+ The protocol caps one file at **50,000 URLs or 50MB uncompressed**. `Sitemap`
167
+ retains every entry — that is what makes `entries()` and a repeatable
168
+ `toXML()` possible — so it is right up to that ceiling and wrong above it. A
169
+ site past it needs the streaming writer, which retains nothing and emits shards
170
+ plus an index.
171
+
148
172
  `maxBytesPerFile` is enforced at shard boundaries, not within an entry: a
149
173
  single entry too large for the ceiling is written alone in its own shard
150
174
  rather than split — an entry can't be split.
151
175
 
152
- ## `SitemapIndex.saveTo(outDir)` owns the whole directory
153
-
154
- It publishes atomically by swapping `outDir` for a freshly written temp
155
- directory in one `rename` — which means `outDir` must be a directory
156
- dedicated to this sitemap set, never an app's `public/` or anything else
157
- something else writes to. Every publish marks `outDir` with
158
- `.sitemap-set.json`; a later call only swaps a directory that is absent,
159
- empty, or already carries that marker. A non-empty, unmarked `outDir` gets
160
- `UnownedOutputDirectoryError` instead, and is left completely untouched.
161
-
162
- ## Errors
163
-
164
- | Error | When |
165
- | --- | --- |
166
- | `InvalidBaseUrlError` | `baseUrl` missing, relative, or not `http(s)`. From the constructor. |
167
- | `InvalidSitemapEntryError` | No path, priority outside `0.0`–`1.0`, unknown `changefreq`, invalid `Date`, alternate with no `hreflang`. From `add()`. |
168
- | `UnownedOutputDirectoryError` | `SitemapIndex.saveTo(outDir)`: `outDir` is non-empty with no `.sitemap-set.json` marker. From `saveTo()`, before anything is written. |
169
-
170
- ## Also exported
171
-
176
+ For `SitemapIndex`, `shardPathPrefix` changes the URL directory used for shard
177
+ links in the index without changing where `saveTo(outDir)` writes those files.
178
+ Use it when a proxy serves generated shards from a different public path.
179
+
180
+ ## `SitemapIndex.saveTo(outDir)` owns the whole directory
181
+
182
+ It publishes atomically by swapping `outDir` for a freshly written temp
183
+ directory in one `rename` — which means `outDir` must be a directory
184
+ dedicated to this sitemap set, never an app's `public/` or anything else
185
+ something else writes to. Every publish marks `outDir` with
186
+ `.sitemap-set.json`; a later call only swaps a directory that is absent,
187
+ empty, or already carries that marker. A non-empty, unmarked `outDir` gets
188
+ `UnownedOutputDirectoryError` instead, and is left completely untouched.
189
+
190
+ ## Errors
191
+
192
+ | Error | When |
193
+ | --- | --- |
194
+ | `InvalidBaseUrlError` | `baseUrl` missing, relative, or not `http(s)`. From the constructor. |
195
+ | `InvalidSitemapEntryError` | No path, priority outside `0.0`–`1.0`, unknown `changefreq`, invalid `Date`, alternate with no `hreflang`. From `add()`. |
196
+ | `UnownedOutputDirectoryError` | `SitemapIndex.saveTo(outDir)`: `outDir` is non-empty with no `.sitemap-set.json` marker. From `saveTo()`, before anything is written. |
197
+
198
+ ## Also exported
199
+
172
200
  `buildSitemapXml(entries, baseUrl)`, `escapeXml(value)`, `joinOrigin(origin,
173
- path)` — the pieces the class is built from, for a caller who wants the
174
- serialiser without the builder.
201
+ path)`, `SitemapImage`, and `SitemapImageLimitExceeded` — the pieces the class
202
+ is built from, for a caller who wants the serialiser or image diagnostics
203
+ without the builder.