@warlock.js/sitemap 5.15.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/llms-full.txt ADDED
@@ -0,0 +1,204 @@
1
+ # Warlock Sitemap — full skills
2
+
3
+ > Package: `@warlock.js/sitemap`
4
+
5
+ > Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/sitemap/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
6
+
7
+ ## sitemap-overview `@warlock.js/sitemap/sitemap-overview/SKILL.md`
8
+
9
+ ---
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`.'
12
+ ---
13
+
14
+ # `@warlock.js/sitemap` — overview
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.
19
+
20
+ ## `@warlock.js/core` and `@warlock.js/web` are optional peers
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.
27
+
28
+ ## Three ways to use it
29
+
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.
41
+
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.
45
+
46
+ ## Server-only package
47
+
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.
52
+
53
+ ## Wiring it up — `sitemapConnector()` and `src/config/sitemap.ts` (Modes 2 and 3)
54
+
55
+ `warlock add sitemap` (`requires: ["web"]`) writes both halves for Mode 3.
56
+ Doing it by hand is the same two pieces:
57
+
58
+ ```ts
59
+ // warlock.config.ts
60
+ import { defineConfig } from "@warlock.js/core";
61
+ import { sitemapConnector } from "@warlock.js/sitemap";
62
+
63
+ export default defineConfig({ connectors: [sitemapConnector()] });
64
+ ```
65
+
66
+ ```ts
67
+ // src/config/sitemap.ts
68
+ import type { SitemapConfig } from "@warlock.js/sitemap";
69
+
70
+ const sitemapConfig: SitemapConfig = {
71
+ enabled: true,
72
+ path: "/sitemap.xml",
73
+ defaults: { changefreq: "weekly", priority: 0.5 },
74
+ };
75
+
76
+ export default sitemapConfig;
77
+ ```
78
+
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 |
115
+ | --- | --- |
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** |
123
+
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.
129
+
130
+ ## Standalone usage (Mode 1) — no `sitemapConnector()` at all
131
+
132
+ Call the building blocks directly against a hand-built `RoutablePage[]`:
133
+
134
+ ```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");
148
+ ```
149
+
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
155
+
156
+ ```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
+ ];
161
+
162
+ // or, to keep a page out of the sitemap deliberately
163
+ export const sitemap = false;
164
+ ```
165
+
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/`.
203
+
204
+
package/llms.txt ADDED
@@ -0,0 +1,9 @@
1
+ # Warlock Sitemap
2
+
3
+ > Package: `@warlock.js/sitemap`
4
+
5
+ > Runtime sitemap.xml generation for Warlock.js — walks the page registry, honours exclusion rules, and reports dynamic routes it cannot enumerate.
6
+
7
+ ## Skills
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`.
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
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.",
4
+ "warlock": {
5
+ "environment": "server"
6
+ },
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/warlockjs/sitemap"
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",
24
+ "main": "./cjs/index.cjs",
25
+ "module": "./esm/index.mjs",
26
+ "types": "./esm/index.d.mts",
27
+ "exports": {
28
+ ".": {
29
+ "import": {
30
+ "types": "./esm/index.d.mts",
31
+ "default": "./esm/index.mjs"
32
+ },
33
+ "require": {
34
+ "types": "./esm/index.d.mts",
35
+ "default": "./cjs/index.cjs"
36
+ }
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,194 @@
1
+ ---
2
+ name: sitemap-overview
3
+ 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`.'
4
+ ---
5
+
6
+ # `@warlock.js/sitemap` — overview
7
+
8
+ Builds `sitemap.xml` at RUNTIME, against whatever source of URLs the app has
9
+ — a page registry, app-supplied entries, or both — a build-time walk can
10
+ read neither. Generation only; no remote sitemap parser in this release.
11
+
12
+ ## `@warlock.js/core` and `@warlock.js/web` are optional peers
13
+
14
+ Both are declared in `peerDependenciesMeta` as optional. Everything in the
15
+ package except `src/sitemap-connector.ts` imports nothing from either, and
16
+ the connector itself only reaches them through a lazy `import()` inside
17
+ `boot()` — never at module load. This is what makes all three modes below
18
+ possible from one package.
19
+
20
+ ## Three ways to use it
21
+
22
+ 1. **Standalone, any Node app** — no Warlock at all. Build a
23
+ `RoutablePage[]` yourself and call `collectSitemapEntries` +
24
+ `buildSitemapXml` directly. Nothing on this path resolves
25
+ `@warlock.js/core` or `@warlock.js/web`.
26
+ 2. **Warlock, API-only** — an API-only app has no page registry for
27
+ `listRoutablePages()` to read. Pass `sitemapConnector({ entries })` with
28
+ an app-supplied entries function instead; `@warlock.js/web` is not
29
+ required on this path.
30
+ 3. **Warlock web** — `warlock add sitemap` wires `sitemapConnector()`
31
+ against the page registry `@warlock.js/web` exposes, plus the page-level
32
+ `sitemap` export for dynamic routes.
33
+
34
+ Modes 2 and 3 aren't exclusive: `entries` and a page registry can both be
35
+ present on the same connector — see "Merging page-derived and app-supplied
36
+ entries" below.
37
+
38
+ ## Server-only package
39
+
40
+ `@warlock.js/sitemap`'s entire runtime surface is server-only — its
41
+ `package.json` declares `"warlock": { "environment": "server" }`. It reads
42
+ the page registry and mounts a route; it has no reason to reach the client
43
+ bundle.
44
+
45
+ ## Wiring it up — `sitemapConnector()` and `src/config/sitemap.ts` (Modes 2 and 3)
46
+
47
+ `warlock add sitemap` (`requires: ["web"]`) writes both halves for Mode 3.
48
+ Doing it by hand is the same two pieces:
49
+
50
+ ```ts
51
+ // warlock.config.ts
52
+ import { defineConfig } from "@warlock.js/core";
53
+ import { sitemapConnector } from "@warlock.js/sitemap";
54
+
55
+ export default defineConfig({ connectors: [sitemapConnector()] });
56
+ ```
57
+
58
+ ```ts
59
+ // src/config/sitemap.ts
60
+ import type { SitemapConfig } from "@warlock.js/sitemap";
61
+
62
+ const sitemapConfig: SitemapConfig = {
63
+ enabled: true,
64
+ path: "/sitemap.xml",
65
+ defaults: { changefreq: "weekly", priority: 0.5 },
66
+ };
67
+
68
+ export default sitemapConfig;
69
+ ```
70
+
71
+ `SitemapConfig` is `{ enabled: boolean; path: string; defaults?: SitemapDefaults }`
72
+ — `defaults` (`changefreq`/`priority`) backs any entry that omits them.
73
+ `sitemapConnector({ config })` accepts the config object directly instead of
74
+ reading the `sitemap` config key, for tests or non-standard wiring.
75
+
76
+ At `boot()` (priority `5.6` — after HTTP's `5` and web's `5.5`, so the route
77
+ it registers lands on the router web already populated) it is a no-op unless
78
+ `enabled` is `true`. When enabled, it resolves `app.publicUrl` /
79
+ `PUBLIC_APP_URL` **once, at boot** via `resolveOrigin()` — throwing
80
+ `MissingPublicUrlError` and refusing to start rather than falling back to a
81
+ request-derived host — then attempts a lazy `import("@warlock.js/web/build")` to
82
+ get `listRoutablePages`. If that import fails **and** no `entries` option
83
+ was supplied, it throws `NoPageRegistryError` — there is nothing to serve.
84
+ Otherwise it registers `GET <path>`. The route itself re-reads the page
85
+ graph on every request via `listRoutablePages()`, not once at boot, so it
86
+ never goes stale under `warlock dev`.
87
+
88
+ ### `NoPageRegistryError`
89
+
90
+ Raised at `boot()`, before the route is registered, when `@warlock.js/web`
91
+ is not installed and no `entries` option was supplied. Refuses to boot
92
+ rather than registering a route that would silently serve an empty
93
+ `<urlset>` — the same reasoning as `MissingPublicUrlError` above. Fix by
94
+ installing `@warlock.js/web`, or by passing `sitemapConnector({ entries })`.
95
+
96
+ ### Merging page-derived and app-supplied entries
97
+
98
+ When both a page registry (Mode 3) and `entries` (Mode 2's option, usable
99
+ alongside Mode 3) are present, `mergeSitemapEntries` (`src/collect-entries.ts`)
100
+ combines them, deduplicated by `path` — not one replacing the other. Where
101
+ the same `path` appears in both, the `entries` version wins, since it was
102
+ written for that exact path on purpose.
103
+
104
+ ## The exclusion rules — read these once and you know the shape
105
+
106
+ | case | behaviour |
107
+ | --- | --- |
108
+ | static route | included |
109
+ | not-found route | excluded |
110
+ | error page | excluded — it isn't a routable page at all |
111
+ | page whose `metadata.robots` says `noindex` | excluded |
112
+ | page exporting `sitemap: false` | excluded |
113
+ | dynamic route (`[id]`, `[...slug]`) **with** a `sitemap` export | the entries that export returns |
114
+ | dynamic route **without** a `sitemap` export | **omitted, and named in a dev-mode diagnostic** |
115
+
116
+ That last row is the whole reason this package exists: a dynamic route cannot
117
+ be enumerated without application data, so silence there would mean a
118
+ sitemap that looks complete while it quietly omits every product page on the
119
+ site. `describeUnresolvedDynamicRoutes` (`src/diagnostic.ts`) is the message a
120
+ developer sees when that happens.
121
+
122
+ ## Standalone usage (Mode 1) — no `sitemapConnector()` at all
123
+
124
+ Call the building blocks directly against a hand-built `RoutablePage[]`:
125
+
126
+ ```ts
127
+ import { buildSitemapXml, collectSitemapEntries, type RoutablePage } from "@warlock.js/sitemap";
128
+
129
+ const pages: RoutablePage[] = [
130
+ { routeName: "home", routePath: "/" },
131
+ {
132
+ routeName: "post-details",
133
+ routePath: "/posts/:id",
134
+ sitemap: async () => (await db.posts.find()).map((post) => ({ path: `/posts/${post.slug}` })),
135
+ },
136
+ ];
137
+
138
+ const { entries } = await collectSitemapEntries(pages, { defaults: { changefreq: "weekly" } });
139
+ const xml = buildSitemapXml(entries, "https://example.com");
140
+ ```
141
+
142
+ This is the only mode where `buildSitemapXml`'s `origin` argument is
143
+ supplied by hand instead of `resolveOrigin()` — there is no `app.publicUrl`
144
+ to read without `@warlock.js/core`.
145
+
146
+ ## The page-level `sitemap` export
147
+
148
+ ```ts
149
+ // any *.page.tsx — only needed for a dynamic route
150
+ export const sitemap: SitemapEntries = async () => [
151
+ { path: "/posts/hello-world", lastmod: "2026-09-17", priority: 0.8 },
152
+ ];
153
+
154
+ // or, to keep a page out of the sitemap deliberately
155
+ export const sitemap = false;
156
+ ```
157
+
158
+ `changefreq` and `priority` are per-entry and optional, falling back to the
159
+ config's `defaults`. They are not part of `PageMetadata` — they mean nothing
160
+ outside a sitemap.
161
+
162
+ ## Building blocks
163
+
164
+ - `sitemapConnector(options?)` (`src/sitemap-connector.ts`) — the connector
165
+ `warlock.config.ts` registers; `options.entries` for Mode 2; see "Wiring it
166
+ up" above. The only module that imports `@warlock.js/core`/`@warlock.js/web`,
167
+ and only lazily.
168
+ - `NoPageRegistryError` (`src/sitemap-connector.ts`) — see above.
169
+ - `collectSitemapEntries(pages, options)` (`src/collect-entries.ts`) — applies
170
+ every exclusion rule above and returns `{ entries, unresolvedDynamicRoutes }`.
171
+ Takes a `RoutablePage[]` — a minimal shape either the runtime wiring adapts
172
+ from `@warlock.js/web`'s page registry (Mode 3), or an app builds by hand
173
+ (Mode 1) — not `@warlock.js/web`'s own discovery type.
174
+ - `mergeSitemapEntries(pageEntries, appEntries)` / `withDefaults(entry, defaults)`
175
+ (`src/collect-entries.ts`) — the dedupe-by-`path` merge (app-supplied wins
176
+ on collision) and the per-entry default-applying helper, shared by
177
+ page-derived and app-supplied entries alike.
178
+ - `buildSitemapXml(entries, origin)` (`src/xml.ts`) — serialises entries into
179
+ the sitemaps.org `urlset` document: correct namespace, element order
180
+ (`loc`/`lastmod`/`changefreq`/`priority`), and XML escaping of `&`, `<`,
181
+ `>`, `"`, `'` in every URL (a URL with a query string contains `&`).
182
+ - `resolveOrigin(options)` / `joinOrigin(origin, path)` (`src/url.ts`) — the
183
+ configured public origin (`app.publicUrl`, env fallback `PUBLIC_APP_URL`),
184
+ and joining it to a route path with exactly one slash regardless of
185
+ trailing slashes on either side. Throws `MissingPublicUrlError` when the
186
+ sitemap is enabled and no origin is configured — a boot-time failure, not a
187
+ request-time fallback.
188
+ - `describeUnresolvedDynamicRoutes(routeNames)` (`src/diagnostic.ts`) — the
189
+ dev-mode message for the row above.
190
+
191
+ ## See also
192
+
193
+ - [`@warlock.js/core/warlock-conventions/SKILL.md`](@warlock.js/core/warlock-conventions/SKILL.md) — the parent framework's conventions.
194
+ - `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/`.