@warlock.js/sitemap 5.15.0 → 5.16.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
@@ -1,194 +1,174 @@
1
1
  ---
2
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`.'
3
+ 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.'
4
4
  ---
5
5
 
6
6
  # `@warlock.js/sitemap` — overview
7
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.
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
11
 
12
- ## `@warlock.js/core` and `@warlock.js/web` are optional peers
12
+ ## It does not import Warlock
13
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.
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.
19
18
 
20
- ## Three ways to use it
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.
21
22
 
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.
23
+ ## The whole common case
33
24
 
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.
25
+ ```ts
26
+ import { Sitemap } from "@warlock.js/sitemap";
37
27
 
38
- ## Server-only package
28
+ const sitemap = new Sitemap({
29
+ baseUrl: "https://example.com",
30
+ changefreq: "weekly",
31
+ priority: 0.5,
32
+ });
39
33
 
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.
34
+ sitemap.add({ path: "/" });
44
35
 
45
- ## Wiring it up `sitemapConnector()` and `src/config/sitemap.ts` (Modes 2 and 3)
36
+ declare const posts: { slug: string; updatedAt: Date }[];
46
37
 
47
- `warlock add sitemap` (`requires: ["web"]`) writes both halves for Mode 3.
48
- Doing it by hand is the same two pieces:
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
+ }
49
47
 
50
- ```ts
51
- // warlock.config.ts
52
- import { defineConfig } from "@warlock.js/core";
53
- import { sitemapConnector } from "@warlock.js/sitemap";
48
+ const xml = sitemap.toXML();
54
49
 
55
- export default defineConfig({ connectors: [sitemapConnector()] });
50
+ await sitemap.saveTo("public/sitemap.xml");
56
51
  ```
57
52
 
58
- ```ts
59
- // src/config/sitemap.ts
60
- import type { SitemapConfig } from "@warlock.js/sitemap";
53
+ ## `baseUrl` is validated in the CONSTRUCTOR
61
54
 
62
- const sitemapConfig: SitemapConfig = {
63
- enabled: true,
64
- path: "/sitemap.xml",
65
- defaults: { changefreq: "weekly", priority: 0.5 },
66
- };
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.
67
60
 
68
- export default sitemapConfig;
69
- ```
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.
70
63
 
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 |
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 |
107
70
  | --- | --- |
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** |
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
+ | `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.
115
81
 
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.
82
+ ## Duplicates: silent, but not hidden
121
83
 
122
- ## Standalone usage (Mode 1) no `sitemapConnector()` at all
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.
123
87
 
124
- Call the building blocks directly against a hand-built `RoutablePage[]`:
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:
125
90
 
126
91
  ```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");
92
+ sitemap.duplicates();
93
+ // [ { path: "/posts/1", count: 2, routes: ["/posts/:id", "/:slug"] } ]
140
94
  ```
141
95
 
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
96
+ ## `routes()` and why `declareRoute()` exists
147
97
 
148
98
  ```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
- ];
99
+ sitemap.declareRoute("/products/:slug");
153
100
 
154
- // or, to keep a page out of the sitemap deliberately
155
- export const sitemap = false;
101
+ sitemap.routes();
102
+ // [ { route: "/posts/:id", count: 400 }, { route: "/products/:slug", count: 0 } ]
156
103
  ```
157
104
 
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/`.
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
118
+
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.
134
+
135
+ ## `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
+
148
+ `maxBytesPerFile` is enforced at shard boundaries, not within an entry: a
149
+ single entry too large for the ceiling is written alone in its own shard
150
+ rather than split — an entry can't be split.
151
+
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
+
172
+ `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.
@@ -1,44 +0,0 @@
1
- import { SitemapDefaults, SitemapEntry } from "./types.mjs";
2
- import { RoutablePage } from "./routable-page.mjs";
3
-
4
- //#region ../sitemap/src/collect-entries.d.ts
5
- /** A route path carries a dynamic segment (`[id]` -> `:id`) it cannot enumerate on its own. */
6
- declare function isDynamicRoutePath(routePath: string): boolean;
7
- /** Applies the config's `defaults` to any entry that omits `changefreq`/`priority` — shared by page-derived and app-supplied entries alike. */
8
- declare function withDefaults(entry: SitemapEntry, defaults: SitemapDefaults | undefined): SitemapEntry;
9
- /**
10
- * Combines page-derived entries with app-supplied ones (`SitemapConnectorOptions.entries`),
11
- * deduplicating by `path`. App-supplied entries are ADDED, not substituted — an
12
- * app with both a page graph and extra URLs (e.g. rows the page graph can't
13
- * see) wants both — but where the same path appears in both, the app-supplied
14
- * entry wins, since it was written for that exact path on purpose.
15
- */
16
- declare function mergeSitemapEntries(pageEntries: readonly SitemapEntry[], appEntries: readonly SitemapEntry[]): SitemapEntry[];
17
- type CollectSitemapEntriesOptions = {
18
- defaults?: SitemapDefaults;
19
- };
20
- type CollectSitemapEntriesResult = {
21
- entries: SitemapEntry[]; /** Route names of dynamic routes with no `sitemap` export — feed to `describeUnresolvedDynamicRoutes`. */
22
- unresolvedDynamicRoutes: string[];
23
- };
24
- /**
25
- * Walks the routable pages and produces the entries + the unresolved-dynamic
26
- * diagnostic input, applying every exclusion rule:
27
- *
28
- * - `metadata.robots` says `noindex` -> excluded.
29
- * - `sitemap: false` -> excluded.
30
- * - a `sitemap` export (any route) -> its returned entries, in place of the
31
- * page's own route path.
32
- * - a dynamic route with no `sitemap` export -> omitted, name collected.
33
- * - everything else (static routes) -> one entry at the page's own route path.
34
- *
35
- * Not-found and error pages are excluded by construction: the caller is
36
- * expected to hand this only `DiscoveredRoutablePage`-derived entries, and
37
- * the not-found route is never one of those (`@warlock.js/web`'s discovery
38
- * reports it as a routable page for the client matcher, but the runtime
39
- * wiring filters it out before calling here — see the sitemap README).
40
- */
41
- declare function collectSitemapEntries(pages: readonly RoutablePage[], options?: CollectSitemapEntriesOptions): Promise<CollectSitemapEntriesResult>;
42
- //#endregion
43
- export { CollectSitemapEntriesOptions, CollectSitemapEntriesResult, collectSitemapEntries, isDynamicRoutePath, mergeSitemapEntries, withDefaults };
44
- //# sourceMappingURL=collect-entries.d.mts.map
@@ -1,73 +0,0 @@
1
- //#region ../sitemap/src/collect-entries.ts
2
- const DYNAMIC_SEGMENT = /(^|\/):[A-Za-z_][A-Za-z0-9_]*/;
3
- /** A route path carries a dynamic segment (`[id]` -> `:id`) it cannot enumerate on its own. */
4
- function isDynamicRoutePath(routePath) {
5
- return DYNAMIC_SEGMENT.test(routePath);
6
- }
7
- function isNoindex(robots) {
8
- return robots !== void 0 && /noindex/i.test(robots);
9
- }
10
- /** Applies the config's `defaults` to any entry that omits `changefreq`/`priority` — shared by page-derived and app-supplied entries alike. */
11
- function withDefaults(entry, defaults) {
12
- return {
13
- ...entry,
14
- changefreq: entry.changefreq ?? defaults?.changefreq,
15
- priority: entry.priority ?? defaults?.priority
16
- };
17
- }
18
- /**
19
- * Combines page-derived entries with app-supplied ones (`SitemapConnectorOptions.entries`),
20
- * deduplicating by `path`. App-supplied entries are ADDED, not substituted — an
21
- * app with both a page graph and extra URLs (e.g. rows the page graph can't
22
- * see) wants both — but where the same path appears in both, the app-supplied
23
- * entry wins, since it was written for that exact path on purpose.
24
- */
25
- function mergeSitemapEntries(pageEntries, appEntries) {
26
- const byPath = /* @__PURE__ */ new Map();
27
- for (const entry of pageEntries) byPath.set(entry.path, entry);
28
- for (const entry of appEntries) byPath.set(entry.path, entry);
29
- return Array.from(byPath.values());
30
- }
31
- /**
32
- * Walks the routable pages and produces the entries + the unresolved-dynamic
33
- * diagnostic input, applying every exclusion rule:
34
- *
35
- * - `metadata.robots` says `noindex` -> excluded.
36
- * - `sitemap: false` -> excluded.
37
- * - a `sitemap` export (any route) -> its returned entries, in place of the
38
- * page's own route path.
39
- * - a dynamic route with no `sitemap` export -> omitted, name collected.
40
- * - everything else (static routes) -> one entry at the page's own route path.
41
- *
42
- * Not-found and error pages are excluded by construction: the caller is
43
- * expected to hand this only `DiscoveredRoutablePage`-derived entries, and
44
- * the not-found route is never one of those (`@warlock.js/web`'s discovery
45
- * reports it as a routable page for the client matcher, but the runtime
46
- * wiring filters it out before calling here — see the sitemap README).
47
- */
48
- async function collectSitemapEntries(pages, options = {}) {
49
- const entries = [];
50
- const unresolvedDynamicRoutes = [];
51
- for (const page of pages) {
52
- if (isNoindex(page.robots)) continue;
53
- if (page.sitemap === false) continue;
54
- if (typeof page.sitemap === "function") {
55
- const produced = await page.sitemap();
56
- for (const entry of produced) entries.push(withDefaults(entry, options.defaults));
57
- continue;
58
- }
59
- if (isDynamicRoutePath(page.routePath)) {
60
- unresolvedDynamicRoutes.push(page.routeName);
61
- continue;
62
- }
63
- entries.push(withDefaults({ path: page.routePath }, options.defaults));
64
- }
65
- return {
66
- entries,
67
- unresolvedDynamicRoutes
68
- };
69
- }
70
-
71
- //#endregion
72
- export { collectSitemapEntries, isDynamicRoutePath, mergeSitemapEntries, withDefaults };
73
- //# sourceMappingURL=collect-entries.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"collect-entries.mjs","names":[],"sources":["../../../../../../sitemap/src/collect-entries.ts"],"sourcesContent":["import type { RoutablePage } from \"./routable-page\";\r\nimport type { SitemapDefaults, SitemapEntry } from \"./types\";\r\n\r\nconst DYNAMIC_SEGMENT = /(^|\\/):[A-Za-z_][A-Za-z0-9_]*/;\r\n\r\n/** A route path carries a dynamic segment (`[id]` -> `:id`) it cannot enumerate on its own. */\r\nexport function isDynamicRoutePath(routePath: string): boolean {\r\n return DYNAMIC_SEGMENT.test(routePath);\r\n}\r\n\r\nfunction isNoindex(robots: string | undefined): boolean {\r\n return robots !== undefined && /noindex/i.test(robots);\r\n}\r\n\r\n/** Applies the config's `defaults` to any entry that omits `changefreq`/`priority` — shared by page-derived and app-supplied entries alike. */\r\nexport function withDefaults(entry: SitemapEntry, defaults: SitemapDefaults | undefined): SitemapEntry {\r\n return {\r\n ...entry,\r\n changefreq: entry.changefreq ?? defaults?.changefreq,\r\n priority: entry.priority ?? defaults?.priority,\r\n };\r\n}\r\n\r\n/**\r\n * Combines page-derived entries with app-supplied ones (`SitemapConnectorOptions.entries`),\r\n * deduplicating by `path`. App-supplied entries are ADDED, not substituted — an\r\n * app with both a page graph and extra URLs (e.g. rows the page graph can't\r\n * see) wants both — but where the same path appears in both, the app-supplied\r\n * entry wins, since it was written for that exact path on purpose.\r\n */\r\nexport function mergeSitemapEntries(\r\n pageEntries: readonly SitemapEntry[],\r\n appEntries: readonly SitemapEntry[],\r\n): SitemapEntry[] {\r\n const byPath = new Map<string, SitemapEntry>();\r\n\r\n for (const entry of pageEntries) byPath.set(entry.path, entry);\r\n for (const entry of appEntries) byPath.set(entry.path, entry);\r\n\r\n return Array.from(byPath.values());\r\n}\r\n\r\nexport type CollectSitemapEntriesOptions = {\r\n defaults?: SitemapDefaults;\r\n};\r\n\r\nexport type CollectSitemapEntriesResult = {\r\n entries: SitemapEntry[];\r\n /** Route names of dynamic routes with no `sitemap` export — feed to `describeUnresolvedDynamicRoutes`. */\r\n unresolvedDynamicRoutes: string[];\r\n};\r\n\r\n/**\r\n * Walks the routable pages and produces the entries + the unresolved-dynamic\r\n * diagnostic input, applying every exclusion rule:\r\n *\r\n * - `metadata.robots` says `noindex` -> excluded.\r\n * - `sitemap: false` -> excluded.\r\n * - a `sitemap` export (any route) -> its returned entries, in place of the\r\n * page's own route path.\r\n * - a dynamic route with no `sitemap` export -> omitted, name collected.\r\n * - everything else (static routes) -> one entry at the page's own route path.\r\n *\r\n * Not-found and error pages are excluded by construction: the caller is\r\n * expected to hand this only `DiscoveredRoutablePage`-derived entries, and\r\n * the not-found route is never one of those (`@warlock.js/web`'s discovery\r\n * reports it as a routable page for the client matcher, but the runtime\r\n * wiring filters it out before calling here — see the sitemap README).\r\n */\r\nexport async function collectSitemapEntries(\r\n pages: readonly RoutablePage[],\r\n options: CollectSitemapEntriesOptions = {},\r\n): Promise<CollectSitemapEntriesResult> {\r\n const entries: SitemapEntry[] = [];\r\n const unresolvedDynamicRoutes: string[] = [];\r\n\r\n for (const page of pages) {\r\n if (isNoindex(page.robots)) continue;\r\n if (page.sitemap === false) continue;\r\n\r\n if (typeof page.sitemap === \"function\") {\r\n const produced = await page.sitemap();\r\n\r\n for (const entry of produced) entries.push(withDefaults(entry, options.defaults));\r\n\r\n continue;\r\n }\r\n\r\n if (isDynamicRoutePath(page.routePath)) {\r\n unresolvedDynamicRoutes.push(page.routeName);\r\n continue;\r\n }\r\n\r\n entries.push(withDefaults({ path: page.routePath }, options.defaults));\r\n }\r\n\r\n return { entries, unresolvedDynamicRoutes };\r\n}\r\n"],"mappings":";AAGA,MAAM,kBAAkB;;AAGxB,SAAgB,mBAAmB,WAA4B;CAC7D,OAAO,gBAAgB,KAAK,SAAS;AACvC;AAEA,SAAS,UAAU,QAAqC;CACtD,OAAO,WAAW,UAAa,WAAW,KAAK,MAAM;AACvD;;AAGA,SAAgB,aAAa,OAAqB,UAAqD;CACrG,OAAO;EACL,GAAG;EACH,YAAY,MAAM,cAAc,UAAU;EAC1C,UAAU,MAAM,YAAY,UAAU;CACxC;AACF;;;;;;;;AASA,SAAgB,oBACd,aACA,YACgB;CAChB,MAAM,yBAAS,IAAI,IAA0B;CAE7C,KAAK,MAAM,SAAS,aAAa,OAAO,IAAI,MAAM,MAAM,KAAK;CAC7D,KAAK,MAAM,SAAS,YAAY,OAAO,IAAI,MAAM,MAAM,KAAK;CAE5D,OAAO,MAAM,KAAK,OAAO,OAAO,CAAC;AACnC;;;;;;;;;;;;;;;;;;AA6BA,eAAsB,sBACpB,OACA,UAAwC,CAAC,GACH;CACtC,MAAM,UAA0B,CAAC;CACjC,MAAM,0BAAoC,CAAC;CAE3C,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,UAAU,KAAK,MAAM,GAAG;EAC5B,IAAI,KAAK,YAAY,OAAO;EAE5B,IAAI,OAAO,KAAK,YAAY,YAAY;GACtC,MAAM,WAAW,MAAM,KAAK,QAAQ;GAEpC,KAAK,MAAM,SAAS,UAAU,QAAQ,KAAK,aAAa,OAAO,QAAQ,QAAQ,CAAC;GAEhF;EACF;EAEA,IAAI,mBAAmB,KAAK,SAAS,GAAG;GACtC,wBAAwB,KAAK,KAAK,SAAS;GAC3C;EACF;EAEA,QAAQ,KAAK,aAAa,EAAE,MAAM,KAAK,UAAU,GAAG,QAAQ,QAAQ,CAAC;CACvE;CAEA,OAAO;EAAE;EAAS;CAAwB;AAC5C"}
@@ -1,13 +0,0 @@
1
- //#region ../sitemap/src/diagnostic.d.ts
2
- /**
3
- * The dev-mode diagnostic for dynamic routes {@link collectSitemapEntries}
4
- * (`collect-entries.ts`) could not enumerate — the whole reason this package
5
- * is written carefully. A dynamic route cannot be enumerated without
6
- * application data; what the framework controls is whether the developer
7
- * finds out. Returns `undefined` when there is nothing to report, so a caller
8
- * can `if (message) console.warn(message)` without an extra length check.
9
- */
10
- declare function describeUnresolvedDynamicRoutes(routeNames: readonly string[]): string | undefined;
11
- //#endregion
12
- export { describeUnresolvedDynamicRoutes };
13
- //# sourceMappingURL=diagnostic.d.mts.map
@@ -1,19 +0,0 @@
1
- //#region ../sitemap/src/diagnostic.ts
2
- /**
3
- * The dev-mode diagnostic for dynamic routes {@link collectSitemapEntries}
4
- * (`collect-entries.ts`) could not enumerate — the whole reason this package
5
- * is written carefully. A dynamic route cannot be enumerated without
6
- * application data; what the framework controls is whether the developer
7
- * finds out. Returns `undefined` when there is nothing to report, so a caller
8
- * can `if (message) console.warn(message)` without an extra length check.
9
- */
10
- function describeUnresolvedDynamicRoutes(routeNames) {
11
- if (routeNames.length === 0) return void 0;
12
- const plural = routeNames.length === 1 ? "" : "s";
13
- const named = routeNames.map((name) => ` - ${name}`).join("\n");
14
- return `[warlock:sitemap] ${routeNames.length} dynamic route${plural} ${routeNames.length === 1 ? "has" : "have"} no \`sitemap\` export and ${routeNames.length === 1 ? "is" : "are"} OMITTED from sitemap.xml:\n${named}\nA dynamic route cannot be enumerated without application data. Add \`export const sitemap: SitemapEntries = async () => [...]\` to each page above, or \`export const sitemap = false\` to keep it out of the sitemap deliberately.`;
15
- }
16
-
17
- //#endregion
18
- export { describeUnresolvedDynamicRoutes };
19
- //# sourceMappingURL=diagnostic.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"diagnostic.mjs","names":[],"sources":["../../../../../../sitemap/src/diagnostic.ts"],"sourcesContent":["/**\n * The dev-mode diagnostic for dynamic routes {@link collectSitemapEntries}\n * (`collect-entries.ts`) could not enumerate — the whole reason this package\n * is written carefully. A dynamic route cannot be enumerated without\n * application data; what the framework controls is whether the developer\n * finds out. Returns `undefined` when there is nothing to report, so a caller\n * can `if (message) console.warn(message)` without an extra length check.\n */\nexport function describeUnresolvedDynamicRoutes(routeNames: readonly string[]): string | undefined {\n if (routeNames.length === 0) return undefined;\n\n const plural = routeNames.length === 1 ? \"\" : \"s\";\n const named = routeNames.map((name) => ` - ${name}`).join(\"\\n\");\n\n return (\n `[warlock:sitemap] ${routeNames.length} dynamic route${plural} ` +\n `${routeNames.length === 1 ? \"has\" : \"have\"} no \\`sitemap\\` export and ` +\n `${routeNames.length === 1 ? \"is\" : \"are\"} OMITTED from sitemap.xml:\\n${named}\\n` +\n \"A dynamic route cannot be enumerated without application data. Add \" +\n \"`export const sitemap: SitemapEntries = async () => [...]` to each page above, \" +\n \"or `export const sitemap = false` to keep it out of the sitemap deliberately.\"\n );\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,gCAAgC,YAAmD;CACjG,IAAI,WAAW,WAAW,GAAG,OAAO;CAEpC,MAAM,SAAS,WAAW,WAAW,IAAI,KAAK;CAC9C,MAAM,QAAQ,WAAW,KAAK,SAAS,OAAO,MAAM,CAAC,CAAC,KAAK,IAAI;CAE/D,OACE,qBAAqB,WAAW,OAAO,gBAAgB,OAAO,GAC3D,WAAW,WAAW,IAAI,QAAQ,OAAO,6BACzC,WAAW,WAAW,IAAI,OAAO,MAAM,8BAA8B,MAAM;AAKlF"}
@@ -1,27 +0,0 @@
1
- import { SitemapEntries } from "./types.mjs";
2
-
3
- //#region ../sitemap/src/routable-page.d.ts
4
- /**
5
- * A page's `sitemap` export: the entries function, an explicit opt-out
6
- * (`export const sitemap = false`), or absent entirely.
7
- */
8
- type SitemapPageExport = SitemapEntries | false | undefined;
9
- /**
10
- * The minimal facts {@link collectSitemapEntries} (`collect-entries.ts`) needs
11
- * about one routable page.
12
- *
13
- * Deliberately NOT `DiscoveredRoutablePage` from `@warlock.js/web` — that type
14
- * carries filesystem paths, layout chains and middleware wiring this package
15
- * has no business reading. The runtime wiring (a separate package, adapting
16
- * `discoverPages()` and each page module's exports) builds this shape; this
17
- * package only ever consumes it.
18
- */
19
- type RoutablePage = {
20
- /** Unique route identity — named in the dynamic-route diagnostic when unresolved. */routeName: string; /** The effective route path, `:param` for a dynamic segment (e.g. `/posts/:id`). */
21
- routePath: string; /** `metadata.robots`, when the page's `metadata` export is a static object. */
22
- robots?: string;
23
- sitemap?: SitemapPageExport;
24
- };
25
- //#endregion
26
- export { RoutablePage, SitemapPageExport };
27
- //# sourceMappingURL=routable-page.d.mts.map
@@ -1,60 +0,0 @@
1
- import { SitemapConfig, SitemapEntries } from "./types.mjs";
2
- import { Connector } from "@warlock.js/core";
3
-
4
- //#region ../sitemap/src/sitemap-connector.d.ts
5
- /** Default path when `src/config/sitemap.ts` does not set one. */
6
- declare const DEFAULT_SITEMAP_PATH = "/sitemap.xml";
7
- /**
8
- * Boots after the HTTP connector (`ConnectorPriority.HTTP` is `5`) and after
9
- * web (`5.5`, `web-connector-factory.ts`) — the route it registers has to
10
- * land on the same router web's pages already share, and `listRoutablePages`
11
- * only has a page graph to read once web has scanned it.
12
- */
13
- declare const SITEMAP_CONNECTOR_PRIORITY = 5.6;
14
- type SitemapConnectorOptions = {
15
- /** Supply the configuration directly instead of reading the `sitemap` config key (`src/config/sitemap.ts`). */config?: SitemapConfig;
16
- /**
17
- * App-supplied entries — the only source of entries in a Warlock **API-only**
18
- * project, which has no `@warlock.js/web` page registry for `listRoutablePages()`
19
- * to read. When `@warlock.js/web` IS installed, these are ADDED to the
20
- * page-derived entries (see {@link mergeSitemapEntries}), not substituted, so an
21
- * app with both pages and extra URLs (e.g. rows a database holds) gets both.
22
- */
23
- entries?: SitemapEntries;
24
- };
25
- /**
26
- * Raised at `boot()` when the sitemap is enabled but has no way to produce
27
- * entries: `@warlock.js/web` is not installed, so there is no page registry
28
- * for `listRoutablePages()` to read, AND no `entries` option was supplied.
29
- * Refuses to boot rather than registering a route that would silently serve
30
- * an empty `<urlset>` — the same reasoning as {@link MissingPublicUrlError}:
31
- * a sitemap that looks complete while producing nothing is worse than one
32
- * that never started.
33
- */
34
- declare class NoPageRegistryError extends Error {
35
- constructor();
36
- }
37
- /**
38
- * Construct the sitemap connector.
39
- *
40
- * At `boot()`: reads the `sitemap` config (a no-op when `enabled` is not
41
- * `true`), resolves the public origin ONCE — failing loud via
42
- * {@link resolveOrigin}'s {@link MissingPublicUrlError} rather than falling
43
- * back to a request-derived host — and registers `GET <config.path>`.
44
- *
45
- * The route itself re-reads the page graph on every request via
46
- * `listRoutablePages()`, not once at boot: the registry can change under
47
- * `warlock dev`, and a sitemap that only reflects the app's shape at the
48
- * moment it booted is stale in exactly the way that made `2ede40cf`-class
49
- * defects expensive.
50
- *
51
- * @example
52
- * // warlock.config.ts
53
- * import { sitemapConnector } from "@warlock.js/sitemap";
54
- *
55
- * export default defineConfig({ connectors: [sitemapConnector()] });
56
- */
57
- declare function sitemapConnector(options?: SitemapConnectorOptions): Connector;
58
- //#endregion
59
- export { DEFAULT_SITEMAP_PATH, NoPageRegistryError, SITEMAP_CONNECTOR_PRIORITY, SitemapConnectorOptions, sitemapConnector };
60
- //# sourceMappingURL=sitemap-connector.d.mts.map