@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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/cjs/index.cjs +279 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/collect-entries.d.mts +44 -0
- package/esm/collect-entries.mjs +73 -0
- package/esm/collect-entries.mjs.map +1 -0
- package/esm/diagnostic.d.mts +13 -0
- package/esm/diagnostic.mjs +19 -0
- package/esm/diagnostic.mjs.map +1 -0
- package/esm/index.d.mts +8 -0
- package/esm/index.mjs +7 -0
- package/esm/routable-page.d.mts +27 -0
- package/esm/sitemap-connector.d.mts +60 -0
- package/esm/sitemap-connector.mjs +117 -0
- package/esm/sitemap-connector.mjs.map +1 -0
- package/esm/types.d.mts +33 -0
- package/esm/url.d.mts +29 -0
- package/esm/url.mjs +35 -0
- package/esm/url.mjs.map +1 -0
- package/esm/xml.d.mts +14 -0
- package/esm/xml.mjs +35 -0
- package/esm/xml.mjs.map +1 -0
- package/llms-full.txt +204 -0
- package/llms.txt +9 -0
- package/package.json +39 -0
- package/skills/sitemap-overview/SKILL.md +194 -0
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/`.
|