@warlock.js/sitemap 5.15.0 → 5.17.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +179 -137
  3. package/cjs/index.cjs +677 -201
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/atomic-publish.mjs +85 -0
  6. package/esm/atomic-publish.mjs.map +1 -0
  7. package/esm/atomic-write-file.mjs +50 -0
  8. package/esm/atomic-write-file.mjs.map +1 -0
  9. package/esm/duplicate-path-tracker.mjs +36 -0
  10. package/esm/duplicate-path-tracker.mjs.map +1 -0
  11. package/esm/errors.d.mts +39 -0
  12. package/esm/errors.mjs +52 -0
  13. package/esm/errors.mjs.map +1 -0
  14. package/esm/index.d.mts +8 -8
  15. package/esm/index.mjs +6 -6
  16. package/esm/lastmod.mjs +23 -0
  17. package/esm/lastmod.mjs.map +1 -0
  18. package/esm/normalize-entry.mjs +59 -0
  19. package/esm/normalize-entry.mjs.map +1 -0
  20. package/esm/route-counter.mjs +19 -0
  21. package/esm/route-counter.mjs.map +1 -0
  22. package/esm/shard-name.mjs +28 -0
  23. package/esm/shard-name.mjs.map +1 -0
  24. package/esm/sitemap-index-options.mjs +31 -0
  25. package/esm/sitemap-index-options.mjs.map +1 -0
  26. package/esm/sitemap-index-types.d.mts +39 -0
  27. package/esm/sitemap-index-xml.mjs +20 -0
  28. package/esm/sitemap-index-xml.mjs.map +1 -0
  29. package/esm/sitemap-index.d.mts +31 -0
  30. package/esm/sitemap-index.mjs +96 -0
  31. package/esm/sitemap-index.mjs.map +1 -0
  32. package/esm/sitemap-shard-writer.mjs +89 -0
  33. package/esm/sitemap-shard-writer.mjs.map +1 -0
  34. package/esm/sitemap.d.mts +63 -0
  35. package/esm/sitemap.mjs +129 -0
  36. package/esm/sitemap.mjs.map +1 -0
  37. package/esm/types.d.mts +51 -20
  38. package/esm/url.d.mts +1 -20
  39. package/esm/url.mjs +26 -19
  40. package/esm/url.mjs.map +1 -1
  41. package/esm/xml.d.mts +15 -4
  42. package/esm/xml.mjs +30 -8
  43. package/esm/xml.mjs.map +1 -1
  44. package/llms-full.txt +139 -159
  45. package/llms.txt +2 -2
  46. package/package.json +2 -14
  47. package/skills/sitemap-overview/SKILL.md +139 -159
  48. package/esm/collect-entries.d.mts +0 -44
  49. package/esm/collect-entries.mjs +0 -73
  50. package/esm/collect-entries.mjs.map +0 -1
  51. package/esm/diagnostic.d.mts +0 -13
  52. package/esm/diagnostic.mjs +0 -19
  53. package/esm/diagnostic.mjs.map +0 -1
  54. package/esm/routable-page.d.mts +0 -27
  55. package/esm/sitemap-connector.d.mts +0 -60
  56. package/esm/sitemap-connector.mjs +0 -117
  57. package/esm/sitemap-connector.mjs.map +0 -1
package/llms-full.txt CHANGED
@@ -8,197 +8,177 @@
8
8
 
9
9
  ---
10
10
  name: sitemap-overview
11
- description: 'Front-door orientation for `@warlock.js/sitemap` — three usage modes (standalone any Node app, Warlock API-only via `sitemapConnector({ entries })`, Warlock web via the page registry), the `sitemapConnector()` + `src/config/sitemap.ts` (`SitemapConfig`: `enabled`, `path`, `defaults.changefreq`/`defaults.priority`) wiring, exclusion rules (not-found, error page, `metadata.robots: noindex`, `sitemap: false`), the page-level `sitemap` export for dynamic routes, the app-supplied/page-derived entries merge-and-dedupe rule, `NoPageRegistryError`, and the dev-mode diagnostic for a dynamic route with no `sitemap` export. TRIGGER when: code imports anything from `@warlock.js/sitemap`; user asks "what does @warlock.js/sitemap do", "how do I add a page to the sitemap", "why is my dynamic route missing from sitemap.xml", "sitemap changefreq/priority", "sitemapConnector", "SitemapConfig", "sitemap without @warlock.js/web", "NoPageRegistryError"; package.json adds `@warlock.js/sitemap`; user is scaffolding `warlock add sitemap`; user is building an API-only Warlock app or a plain Node app and wants a sitemap. Skip: user wants to PARSE or fetch a remote sitemap — this package only generates one, in this release; the `app.publicUrl`/`PUBLIC_APP_URL` config key itself lives in `@warlock.js/core/configure-app/SKILL.md`.'
11
+ description: 'Front-door orientation for `@warlock.js/sitemap` — the framework-blind `Sitemap` builder class (`new Sitemap({ baseUrl, changefreq, priority, lastmod })`, `add`/`addMany`/`declareRoute`, `size`/`entries()`/`routes()`/`duplicates()`, sync `toXML()`, `saveTo()`), the constructor-time `baseUrl` validation and `InvalidBaseUrlError`, entry validation and `InvalidSitemapEntryError`, the keyed-by-path silent last-write-wins rule and how `duplicates()` makes it visible, the `count: 0` route diagnostic that `declareRoute()` exists to make possible, `hreflang` alternates via `xhtml:link`, and the 50,000-URL / 50MB protocol ceiling that separates this bounded builder from the streaming writer. TRIGGER when: code imports anything from `@warlock.js/sitemap`; user asks "what does @warlock.js/sitemap do", "how do I generate a sitemap", "how do I add a URL to the sitemap", "why is my dynamic route missing from sitemap.xml", "sitemap changefreq/priority", "sitemap hreflang", "sitemap baseUrl", "InvalidBaseUrlError", "sitemap without Warlock", "sitemap in Express"; package.json adds `@warlock.js/sitemap`; user is scaffolding `warlock add sitemap`. Skip: user wants to PARSE or fetch a remote sitemap — this package only generates one; the Warlock page-registry discovery, locale expansion, the `/sitemap.xml` route and `robots.txt` live in `@warlock.js/web`, not here.'
12
12
  ---
13
13
 
14
14
  # `@warlock.js/sitemap` — overview
15
15
 
16
- Builds `sitemap.xml` at RUNTIME, against whatever source of URLs the app has
17
- a page registry, app-supplied entries, or both — a build-time walk can
18
- read neither. Generation only; no remote sitemap parser in this release.
16
+ A sitemap builder that knows nothing about any framework. Entries in, a valid
17
+ sitemaps.org document out. **Zero runtime dependencies.** Generation only; no
18
+ remote sitemap parser.
19
19
 
20
- ## `@warlock.js/core` and `@warlock.js/web` are optional peers
20
+ ## It does not import Warlock
21
21
 
22
- Both are declared in `peerDependenciesMeta` as optional. Everything in the
23
- package except `src/sitemap-connector.ts` imports nothing from either, and
24
- the connector itself only reaches them through a lazy `import()` inside
25
- `boot()` never at module load. This is what makes all three modes below
26
- possible from one package.
22
+ There is no connector, no config module, no lifecycle and no page registry in
23
+ this package. It owns the sitemap PROTOCOL and every byte it emits, and
24
+ nothing else. `@warlock.js/web` is a CALLER of it, exactly like an Express app
25
+ or a cron script is which is what makes the same three lines work everywhere.
27
26
 
28
- ## Three ways to use it
27
+ If you are looking for page discovery, `sitemap: false` exclusion, locale
28
+ expansion, the `/sitemap.xml` route or `robots.txt`, those belong to
29
+ `@warlock.js/web`, not here.
29
30
 
30
- 1. **Standalone, any Node app** — no Warlock at all. Build a
31
- `RoutablePage[]` yourself and call `collectSitemapEntries` +
32
- `buildSitemapXml` directly. Nothing on this path resolves
33
- `@warlock.js/core` or `@warlock.js/web`.
34
- 2. **Warlock, API-only** — an API-only app has no page registry for
35
- `listRoutablePages()` to read. Pass `sitemapConnector({ entries })` with
36
- an app-supplied entries function instead; `@warlock.js/web` is not
37
- required on this path.
38
- 3. **Warlock web** — `warlock add sitemap` wires `sitemapConnector()`
39
- against the page registry `@warlock.js/web` exposes, plus the page-level
40
- `sitemap` export for dynamic routes.
31
+ ## The whole common case
41
32
 
42
- Modes 2 and 3 aren't exclusive: `entries` and a page registry can both be
43
- present on the same connector — see "Merging page-derived and app-supplied
44
- entries" below.
33
+ ```ts
34
+ import { Sitemap } from "@warlock.js/sitemap";
45
35
 
46
- ## Server-only package
36
+ const sitemap = new Sitemap({
37
+ baseUrl: "https://example.com",
38
+ changefreq: "weekly",
39
+ priority: 0.5,
40
+ });
47
41
 
48
- `@warlock.js/sitemap`'s entire runtime surface is server-only — its
49
- `package.json` declares `"warlock": { "environment": "server" }`. It reads
50
- the page registry and mounts a route; it has no reason to reach the client
51
- bundle.
42
+ sitemap.add({ path: "/" });
52
43
 
53
- ## Wiring it up `sitemapConnector()` and `src/config/sitemap.ts` (Modes 2 and 3)
44
+ declare const posts: { slug: string; updatedAt: Date }[];
54
45
 
55
- `warlock add sitemap` (`requires: ["web"]`) writes both halves for Mode 3.
56
- Doing it by hand is the same two pieces:
46
+ for (const post of posts) {
47
+ sitemap.add({
48
+ name: "post-details",
49
+ route: "/posts/:id",
50
+ path: `/posts/${post.slug}`,
51
+ lastmod: post.updatedAt,
52
+ priority: 0.8,
53
+ });
54
+ }
57
55
 
58
- ```ts
59
- // warlock.config.ts
60
- import { defineConfig } from "@warlock.js/core";
61
- import { sitemapConnector } from "@warlock.js/sitemap";
56
+ const xml = sitemap.toXML();
62
57
 
63
- export default defineConfig({ connectors: [sitemapConnector()] });
58
+ await sitemap.saveTo("public/sitemap.xml");
64
59
  ```
65
60
 
66
- ```ts
67
- // src/config/sitemap.ts
68
- import type { SitemapConfig } from "@warlock.js/sitemap";
61
+ ## `baseUrl` is validated in the CONSTRUCTOR
69
62
 
70
- const sitemapConfig: SitemapConfig = {
71
- enabled: true,
72
- path: "/sitemap.xml",
73
- defaults: { changefreq: "weekly", priority: 0.5 },
74
- };
63
+ A `Sitemap` that cannot produce a valid URL should not exist. Anything that is
64
+ not an absolute `http(s)` URL throws `InvalidBaseUrlError` at construction, so
65
+ a typo surfaces at the line that wrote it rather than at the first request.
66
+ `new URL()` accepts `mailto:` and `file:` happily, so the protocol is checked
67
+ explicitly.
75
68
 
76
- export default sitemapConfig;
77
- ```
69
+ This replaces the 5.15.0 config key, its `PUBLIC_APP_URL` env fallback and its
70
+ boot refusal entirely: the origin is an argument now, so it cannot be missing.
78
71
 
79
- `SitemapConfig` is `{ enabled: boolean; path: string; defaults?: SitemapDefaults }`
80
- — `defaults` (`changefreq`/`priority`) backs any entry that omits them.
81
- `sitemapConnector({ config })` accepts the config object directly instead of
82
- reading the `sitemap` config key, for tests or non-standard wiring.
83
-
84
- At `boot()` (priority `5.6` — after HTTP's `5` and web's `5.5`, so the route
85
- it registers lands on the router web already populated) it is a no-op unless
86
- `enabled` is `true`. When enabled, it resolves `app.publicUrl` /
87
- `PUBLIC_APP_URL` **once, at boot** via `resolveOrigin()` — throwing
88
- `MissingPublicUrlError` and refusing to start rather than falling back to a
89
- request-derived host — then attempts a lazy `import("@warlock.js/web/build")` to
90
- get `listRoutablePages`. If that import fails **and** no `entries` option
91
- was supplied, it throws `NoPageRegistryError` — there is nothing to serve.
92
- Otherwise it registers `GET <path>`. The route itself re-reads the page
93
- graph on every request via `listRoutablePages()`, not once at boot, so it
94
- never goes stale under `warlock dev`.
95
-
96
- ### `NoPageRegistryError`
97
-
98
- Raised at `boot()`, before the route is registered, when `@warlock.js/web`
99
- is not installed and no `entries` option was supplied. Refuses to boot
100
- rather than registering a route that would silently serve an empty
101
- `<urlset>` — the same reasoning as `MissingPublicUrlError` above. Fix by
102
- installing `@warlock.js/web`, or by passing `sitemapConnector({ entries })`.
103
-
104
- ### Merging page-derived and app-supplied entries
105
-
106
- When both a page registry (Mode 3) and `entries` (Mode 2's option, usable
107
- alongside Mode 3) are present, `mergeSitemapEntries` (`src/collect-entries.ts`)
108
- combines them, deduplicated by `path` — not one replacing the other. Where
109
- the same `path` appears in both, the `entries` version wins, since it was
110
- written for that exact path on purpose.
111
-
112
- ## The exclusion rules — read these once and you know the shape
113
-
114
- | case | behaviour |
72
+ ## The entry
73
+
74
+ Only `path` is required, and it must be a **concrete path** — `/posts/123`,
75
+ never `/posts/:id`. A pattern is not a URL.
76
+
77
+ | Field | Meaning |
115
78
  | --- | --- |
116
- | static route | included |
117
- | not-found route | excluded |
118
- | error page | excluded it isn't a routable page at all |
119
- | page whose `metadata.robots` says `noindex` | excluded |
120
- | page exporting `sitemap: false` | excluded |
121
- | dynamic route (`[id]`, `[...slug]`) **with** a `sitemap` export | the entries that export returns |
122
- | dynamic route **without** a `sitemap` export | **omitted, and named in a dev-mode diagnostic** |
79
+ | `path` | Required. Resolved against `baseUrl`; an absolute URL is used as given. |
80
+ | `name` | Optional route label, for your own diagnostics. Never serialised. |
81
+ | `route` | Optional pattern this URL came from. Feeds `routes()` and `duplicates()`. |
82
+ | `lastmod` | `Date` W3C datetime. A string is passed through UNTOUCHED, never re-parsed. |
83
+ | `changefreq` | One of the seven protocol values; anything else throws. |
84
+ | `priority` | `0.0`–`1.0`; anything else throws. |
85
+ | `alternates` | `{ hreflang, path }[]` language versions of this page. |
86
+
87
+ Invalid entries throw `InvalidSitemapEntryError` from `add()`. Paths are
88
+ normalised to carry a leading slash, so `a` and `/a` are ONE entry.
123
89
 
124
- That last row is the whole reason this package exists: a dynamic route cannot
125
- be enumerated without application data, so silence there would mean a
126
- sitemap that looks complete while it quietly omits every product page on the
127
- site. `describeUnresolvedDynamicRoutes` (`src/diagnostic.ts`) is the message a
128
- developer sees when that happens.
90
+ ## Duplicates: silent, but not hidden
129
91
 
130
- ## Standalone usage (Mode 1) no `sitemapConnector()` at all
92
+ Entries are stored keyed by `path`. Adding the same path twice keeps the later
93
+ one, silently — a duplicate `<loc>` makes the document invalid, and two loops
94
+ legitimately covering an overlapping set is the normal cause.
131
95
 
132
- Call the building blocks directly against a hand-built `RoutablePage[]`:
96
+ `duplicates()` reports every collision with its count and the `route` of each
97
+ contributing add, so a caller who cares can fail their own build:
133
98
 
134
99
  ```ts
135
- import { buildSitemapXml, collectSitemapEntries, type RoutablePage } from "@warlock.js/sitemap";
136
-
137
- const pages: RoutablePage[] = [
138
- { routeName: "home", routePath: "/" },
139
- {
140
- routeName: "post-details",
141
- routePath: "/posts/:id",
142
- sitemap: async () => (await db.posts.find()).map((post) => ({ path: `/posts/${post.slug}` })),
143
- },
144
- ];
145
-
146
- const { entries } = await collectSitemapEntries(pages, { defaults: { changefreq: "weekly" } });
147
- const xml = buildSitemapXml(entries, "https://example.com");
100
+ sitemap.duplicates();
101
+ // [ { path: "/posts/1", count: 2, routes: ["/posts/:id", "/:slug"] } ]
148
102
  ```
149
103
 
150
- This is the only mode where `buildSitemapXml`'s `origin` argument is
151
- supplied by hand instead of `resolveOrigin()` — there is no `app.publicUrl`
152
- to read without `@warlock.js/core`.
153
-
154
- ## The page-level `sitemap` export
104
+ ## `routes()` and why `declareRoute()` exists
155
105
 
156
106
  ```ts
157
- // any *.page.tsx — only needed for a dynamic route
158
- export const sitemap: SitemapEntries = async () => [
159
- { path: "/posts/hello-world", lastmod: "2026-09-17", priority: 0.8 },
160
- ];
107
+ sitemap.declareRoute("/products/:slug");
161
108
 
162
- // or, to keep a page out of the sitemap deliberately
163
- export const sitemap = false;
109
+ sitemap.routes();
110
+ // [ { route: "/posts/:id", count: 400 }, { route: "/products/:slug", count: 0 } ]
164
111
  ```
165
112
 
166
- `changefreq` and `priority` are per-entry and optional, falling back to the
167
- config's `defaults`. They are not part of `PageMetadata` they mean nothing
168
- outside a sitemap.
169
-
170
- ## Building blocks
171
-
172
- - `sitemapConnector(options?)` (`src/sitemap-connector.ts`) the connector
173
- `warlock.config.ts` registers; `options.entries` for Mode 2; see "Wiring it
174
- up" above. The only module that imports `@warlock.js/core`/`@warlock.js/web`,
175
- and only lazily.
176
- - `NoPageRegistryError` (`src/sitemap-connector.ts`) see above.
177
- - `collectSitemapEntries(pages, options)` (`src/collect-entries.ts`) — applies
178
- every exclusion rule above and returns `{ entries, unresolvedDynamicRoutes }`.
179
- Takes a `RoutablePage[]` — a minimal shape either the runtime wiring adapts
180
- from `@warlock.js/web`'s page registry (Mode 3), or an app builds by hand
181
- (Mode 1) — not `@warlock.js/web`'s own discovery type.
182
- - `mergeSitemapEntries(pageEntries, appEntries)` / `withDefaults(entry, defaults)`
183
- (`src/collect-entries.ts`) the dedupe-by-`path` merge (app-supplied wins
184
- on collision) and the per-entry default-applying helper, shared by
185
- page-derived and app-supplied entries alike.
186
- - `buildSitemapXml(entries, origin)` (`src/xml.ts`) serialises entries into
187
- the sitemaps.org `urlset` document: correct namespace, element order
188
- (`loc`/`lastmod`/`changefreq`/`priority`), and XML escaping of `&`, `<`,
189
- `>`, `"`, `'` in every URL (a URL with a query string contains `&`).
190
- - `resolveOrigin(options)` / `joinOrigin(origin, path)` (`src/url.ts`) — the
191
- configured public origin (`app.publicUrl`, env fallback `PUBLIC_APP_URL`),
192
- and joining it to a route path with exactly one slash regardless of
193
- trailing slashes on either side. Throws `MissingPublicUrlError` when the
194
- sitemap is enabled and no origin is configured a boot-time failure, not a
195
- request-time fallback.
196
- - `describeUnresolvedDynamicRoutes(routeNames)` (`src/diagnostic.ts`) the
197
- dev-mode message for the row above.
198
-
199
- ## See also
200
-
201
- - [`@warlock.js/core/warlock-conventions/SKILL.md`](@warlock.js/core/warlock-conventions/SKILL.md) — the parent framework's conventions.
202
- - `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `sitemap-overview/SKILL.md` becomes the front-door skill in `.claude/skills/warlock-js-sitemap-overview/`.
113
+ **The zero is the interesting row.** It means a route you expected to
114
+ contribute URLs contributed none, and a whole section of the site is missing
115
+ from a document that otherwise looks perfect.
116
+
117
+ Without `declareRoute()` a route only becomes known by appearing on an entry,
118
+ so every route has at least one URL, `count: 0` can never occur and the
119
+ diagnostic is decorative. Declaring the pattern is what turns a silent loss
120
+ into a visible one.
121
+
122
+ **This package reports; it never prints.** Whether a zero is a warning or a
123
+ build failure is the caller's decision.
124
+
125
+ ## Alternates
126
+
127
+ `alternates` emits `<xhtml:link rel="alternate" hreflang="…">` inside each
128
+ `<url>`. Two rules:
129
+
130
+ 1. **Every language version is also its own `<url>`, carrying the complete
131
+ alternate set including itself.** Listing alternates on only one of them is
132
+ the usual way this ships broken.
133
+ 2. **The package does not know what a locale is.** `hreflang` is any string
134
+ `en`, `en-GB`, `x-default` and it never derives `/{locale}/…` for you.
135
+ Each path is supplied explicitly, which is the only thing that works when
136
+ slugs diverge between languages.
137
+
138
+ The `xhtml` namespace is declared only when something uses it. `SitemapIndex`
139
+ applies the same rule PER SHARD a shard declares `xmlns:xhtml` only when its
140
+ own entries carry alternates and an alternate's `<xhtml:link>` bytes count
141
+ toward that shard's byte ceiling exactly like the rest of its `<url>` block.
142
+
143
+ ## `toXML()` is pure, repeatable and synchronous
144
+
145
+ Calling it twice returns the same string; it mutates nothing and drains
146
+ nothing. It has no I/O and stays synchronous.
147
+
148
+ ## The ceiling
149
+
150
+ The protocol caps one file at **50,000 URLs or 50MB uncompressed**. `Sitemap`
151
+ retains every entry — that is what makes `entries()` and a repeatable
152
+ `toXML()` possible — so it is right up to that ceiling and wrong above it. A
153
+ site past it needs the streaming writer, which retains nothing and emits shards
154
+ plus an index.
155
+
156
+ `maxBytesPerFile` is enforced at shard boundaries, not within an entry: a
157
+ single entry too large for the ceiling is written alone in its own shard
158
+ rather than split — an entry can't be split.
159
+
160
+ ## `SitemapIndex.saveTo(outDir)` owns the whole directory
161
+
162
+ It publishes atomically by swapping `outDir` for a freshly written temp
163
+ directory in one `rename` — which means `outDir` must be a directory
164
+ dedicated to this sitemap set, never an app's `public/` or anything else
165
+ something else writes to. Every publish marks `outDir` with
166
+ `.sitemap-set.json`; a later call only swaps a directory that is absent,
167
+ empty, or already carries that marker. A non-empty, unmarked `outDir` gets
168
+ `UnownedOutputDirectoryError` instead, and is left completely untouched.
169
+
170
+ ## Errors
171
+
172
+ | Error | When |
173
+ | --- | --- |
174
+ | `InvalidBaseUrlError` | `baseUrl` missing, relative, or not `http(s)`. From the constructor. |
175
+ | `InvalidSitemapEntryError` | No path, priority outside `0.0`–`1.0`, unknown `changefreq`, invalid `Date`, alternate with no `hreflang`. From `add()`. |
176
+ | `UnownedOutputDirectoryError` | `SitemapIndex.saveTo(outDir)`: `outDir` is non-empty with no `.sitemap-set.json` marker. From `saveTo()`, before anything is written. |
177
+
178
+ ## Also exported
179
+
180
+ `buildSitemapXml(entries, baseUrl)`, `escapeXml(value)`, `joinOrigin(origin,
181
+ path)` — the pieces the class is built from, for a caller who wants the
182
+ serialiser without the builder.
203
183
 
204
184
 
package/llms.txt CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  > Package: `@warlock.js/sitemap`
4
4
 
5
- > Runtime sitemap.xml generation for Warlock.js — walks the page registry, honours exclusion rules, and reports dynamic routes it cannot enumerate.
5
+ > A framework-blind sitemap builder: entries in, a valid sitemaps.org document out. Zero runtime dependencies.
6
6
 
7
7
  ## Skills
8
8
 
9
- - [sitemap-overview](@warlock.js/sitemap/sitemap-overview/SKILL.md): Front-door orientation for `@warlock.js/sitemap` — three usage modes (standalone any Node app, Warlock API-only via `sitemapConnector({ entries })`, Warlock web via the page registry), the `sitemapConnector()` + `src/config/sitemap.ts` (`SitemapConfig`: `enabled`, `path`, `defaults.changefreq`/`defaults.priority`) wiring, exclusion rules (not-found, error page, `metadata.robots: noindex`, `sitemap: false`), the page-level `sitemap` export for dynamic routes, the app-supplied/page-derived entries merge-and-dedupe rule, `NoPageRegistryError`, and the dev-mode diagnostic for a dynamic route with no `sitemap` export. TRIGGER when: code imports anything from `@warlock.js/sitemap`; user asks "what does @warlock.js/sitemap do", "how do I add a page to the sitemap", "why is my dynamic route missing from sitemap.xml", "sitemap changefreq/priority", "sitemapConnector", "SitemapConfig", "sitemap without @warlock.js/web", "NoPageRegistryError"; package.json adds `@warlock.js/sitemap`; user is scaffolding `warlock add sitemap`; user is building an API-only Warlock app or a plain Node app and wants a sitemap. Skip: user wants to PARSE or fetch a remote sitemap — this package only generates one, in this release; the `app.publicUrl`/`PUBLIC_APP_URL` config key itself lives in `@warlock.js/core/configure-app/SKILL.md`.
9
+ - [sitemap-overview](@warlock.js/sitemap/sitemap-overview/SKILL.md): Front-door orientation for `@warlock.js/sitemap` — the framework-blind `Sitemap` builder class (`new Sitemap({ baseUrl, changefreq, priority, lastmod })`, `add`/`addMany`/`declareRoute`, `size`/`entries()`/`routes()`/`duplicates()`, sync `toXML()`, `saveTo()`), the constructor-time `baseUrl` validation and `InvalidBaseUrlError`, entry validation and `InvalidSitemapEntryError`, the keyed-by-path silent last-write-wins rule and how `duplicates()` makes it visible, the `count: 0` route diagnostic that `declareRoute()` exists to make possible, `hreflang` alternates via `xhtml:link`, and the 50,000-URL / 50MB protocol ceiling that separates this bounded builder from the streaming writer. TRIGGER when: code imports anything from `@warlock.js/sitemap`; user asks "what does @warlock.js/sitemap do", "how do I generate a sitemap", "how do I add a URL to the sitemap", "why is my dynamic route missing from sitemap.xml", "sitemap changefreq/priority", "sitemap hreflang", "sitemap baseUrl", "InvalidBaseUrlError", "sitemap without Warlock", "sitemap in Express"; package.json adds `@warlock.js/sitemap`; user is scaffolding `warlock add sitemap`. Skip: user wants to PARSE or fetch a remote sitemap — this package only generates one; the Warlock page-registry discovery, locale expansion, the `/sitemap.xml` route and `robots.txt` live in `@warlock.js/web`, not here.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warlock.js/sitemap",
3
- "description": "Runtime sitemap.xml generation for Warlock.js — walks the page registry, honours exclusion rules, and reports dynamic routes it cannot enumerate.",
3
+ "description": "A framework-blind sitemap builder: entries in, a valid sitemaps.org document out. Zero runtime dependencies.",
4
4
  "warlock": {
5
5
  "environment": "server"
6
6
  },
@@ -8,19 +8,7 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/warlockjs/sitemap"
10
10
  },
11
- "peerDependencies": {
12
- "@warlock.js/core": "5.15.0",
13
- "@warlock.js/web": "5.15.0"
14
- },
15
- "peerDependenciesMeta": {
16
- "@warlock.js/core": {
17
- "optional": true
18
- },
19
- "@warlock.js/web": {
20
- "optional": true
21
- }
22
- },
23
- "version": "5.15.0",
11
+ "version": "5.17.0",
24
12
  "main": "./cjs/index.cjs",
25
13
  "module": "./esm/index.mjs",
26
14
  "types": "./esm/index.d.mts",