@warlock.js/sitemap 5.15.0 → 5.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +179 -137
- package/cjs/index.cjs +677 -201
- package/cjs/index.cjs.map +1 -1
- package/esm/atomic-publish.mjs +85 -0
- package/esm/atomic-publish.mjs.map +1 -0
- package/esm/atomic-write-file.mjs +50 -0
- package/esm/atomic-write-file.mjs.map +1 -0
- package/esm/duplicate-path-tracker.mjs +36 -0
- package/esm/duplicate-path-tracker.mjs.map +1 -0
- package/esm/errors.d.mts +39 -0
- package/esm/errors.mjs +52 -0
- package/esm/errors.mjs.map +1 -0
- package/esm/index.d.mts +8 -8
- package/esm/index.mjs +6 -6
- package/esm/lastmod.mjs +23 -0
- package/esm/lastmod.mjs.map +1 -0
- package/esm/normalize-entry.mjs +59 -0
- package/esm/normalize-entry.mjs.map +1 -0
- package/esm/route-counter.mjs +19 -0
- package/esm/route-counter.mjs.map +1 -0
- package/esm/shard-name.mjs +28 -0
- package/esm/shard-name.mjs.map +1 -0
- package/esm/sitemap-index-options.mjs +31 -0
- package/esm/sitemap-index-options.mjs.map +1 -0
- package/esm/sitemap-index-types.d.mts +39 -0
- package/esm/sitemap-index-xml.mjs +20 -0
- package/esm/sitemap-index-xml.mjs.map +1 -0
- package/esm/sitemap-index.d.mts +31 -0
- package/esm/sitemap-index.mjs +96 -0
- package/esm/sitemap-index.mjs.map +1 -0
- package/esm/sitemap-shard-writer.mjs +89 -0
- package/esm/sitemap-shard-writer.mjs.map +1 -0
- package/esm/sitemap.d.mts +63 -0
- package/esm/sitemap.mjs +129 -0
- package/esm/sitemap.mjs.map +1 -0
- package/esm/types.d.mts +51 -20
- package/esm/url.d.mts +1 -20
- package/esm/url.mjs +26 -19
- package/esm/url.mjs.map +1 -1
- package/esm/xml.d.mts +15 -4
- package/esm/xml.mjs +30 -8
- package/esm/xml.mjs.map +1 -1
- package/llms-full.txt +139 -159
- package/llms.txt +2 -2
- package/package.json +2 -14
- package/skills/sitemap-overview/SKILL.md +139 -159
- package/esm/collect-entries.d.mts +0 -44
- package/esm/collect-entries.mjs +0 -73
- package/esm/collect-entries.mjs.map +0 -1
- package/esm/diagnostic.d.mts +0 -13
- package/esm/diagnostic.mjs +0 -19
- package/esm/diagnostic.mjs.map +0 -1
- package/esm/routable-page.d.mts +0 -27
- package/esm/sitemap-connector.d.mts +0 -60
- package/esm/sitemap-connector.mjs +0 -117
- 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` —
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
##
|
|
12
|
+
## It does not import Warlock
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
package
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
entries" below.
|
|
25
|
+
```ts
|
|
26
|
+
import { Sitemap } from "@warlock.js/sitemap";
|
|
37
27
|
|
|
38
|
-
|
|
28
|
+
const sitemap = new Sitemap({
|
|
29
|
+
baseUrl: "https://example.com",
|
|
30
|
+
changefreq: "weekly",
|
|
31
|
+
priority: 0.5,
|
|
32
|
+
});
|
|
39
33
|
|
|
40
|
-
|
|
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
|
-
|
|
36
|
+
declare const posts: { slug: string; updatedAt: Date }[];
|
|
46
37
|
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
await sitemap.saveTo("public/sitemap.xml");
|
|
56
51
|
```
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
// src/config/sitemap.ts
|
|
60
|
-
import type { SitemapConfig } from "@warlock.js/sitemap";
|
|
53
|
+
## `baseUrl` is validated in the CONSTRUCTOR
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
`
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
101
|
+
sitemap.routes();
|
|
102
|
+
// [ { route: "/posts/:id", count: 400 }, { route: "/products/:slug", count: 0 } ]
|
|
156
103
|
```
|
|
157
104
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
package/esm/collect-entries.mjs
DELETED
|
@@ -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"}
|
package/esm/diagnostic.d.mts
DELETED
|
@@ -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
|
package/esm/diagnostic.mjs
DELETED
|
@@ -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
|
package/esm/diagnostic.mjs.map
DELETED
|
@@ -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"}
|
package/esm/routable-page.d.mts
DELETED
|
@@ -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
|