@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.
- 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
package/esm/types.d.mts
CHANGED
|
@@ -2,32 +2,63 @@
|
|
|
2
2
|
/** The `<changefreq>` values the sitemap protocol defines. */
|
|
3
3
|
type ChangeFreq = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
4
4
|
/**
|
|
5
|
-
* One
|
|
6
|
-
*
|
|
5
|
+
* One language version of a page, serialised as
|
|
6
|
+
* `<xhtml:link rel="alternate" hreflang="…" href="…"/>`.
|
|
7
|
+
*
|
|
8
|
+
* `hreflang` is not interpreted: `en`, `en-GB` and `x-default` are all just
|
|
9
|
+
* values. This package understands the PROTOCOL concept of an alternate; it
|
|
10
|
+
* does not understand anybody's locale system, and it never derives a
|
|
11
|
+
* locale-prefixed path for you.
|
|
12
|
+
*/
|
|
13
|
+
type SitemapAlternate = {
|
|
14
|
+
readonly hreflang: string; /** A path resolved against `baseUrl`, or an absolute URL used as given. */
|
|
15
|
+
readonly path: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* One `<url>` block. Only `path` is required; `name` and `route` are carried
|
|
19
|
+
* for the diagnostics in `routes()` and `duplicates()` and never serialised.
|
|
7
20
|
*/
|
|
8
21
|
type SitemapEntry = {
|
|
9
|
-
path: string;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
22
|
+
/** The CONCRETE path: `/posts/123`. A pattern is not a URL. */readonly path: string; /** Optional label for the route this came from: `post-details`. */
|
|
23
|
+
readonly name?: string; /** Optional pattern this came from: `/posts/:id`. */
|
|
24
|
+
readonly route?: string;
|
|
25
|
+
readonly lastmod?: string | Date;
|
|
26
|
+
readonly changefreq?: ChangeFreq;
|
|
27
|
+
readonly priority?: number; /** Language versions of THIS page, conventionally including itself. */
|
|
28
|
+
readonly alternates?: readonly SitemapAlternate[];
|
|
13
29
|
};
|
|
14
30
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
31
|
+
* An entry after normalisation: the path carries its leading slash, the
|
|
32
|
+
* builder's defaults have been folded in, and `lastmod` is always the
|
|
33
|
+
* serialised string — a `Date` is resolved once, at `add()`, so `toXML()`
|
|
34
|
+
* stays pure and repeatable.
|
|
18
35
|
*/
|
|
19
|
-
type
|
|
20
|
-
|
|
21
|
-
type SitemapDefaults = {
|
|
22
|
-
changefreq?: ChangeFreq;
|
|
23
|
-
priority?: number;
|
|
36
|
+
type ResolvedSitemapEntry = Omit<SitemapEntry, "lastmod"> & {
|
|
37
|
+
readonly lastmod?: string;
|
|
24
38
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
type SitemapOptions = {
|
|
40
|
+
/** Absolute origin: `https://example.com` or `https://example.com/base`. */readonly baseUrl: string; /** Applied to every entry that does not set its own. */
|
|
41
|
+
readonly changefreq?: ChangeFreq;
|
|
42
|
+
readonly priority?: number;
|
|
43
|
+
readonly lastmod?: string | Date;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* How many URLs a route contributed. A `count: 0` row is the interesting one:
|
|
47
|
+
* the route was declared and produced nothing, so a whole section is missing.
|
|
48
|
+
*/
|
|
49
|
+
type RouteSummary = {
|
|
50
|
+
readonly route: string;
|
|
51
|
+
readonly count: number;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* A path that was added more than once. The later add wins silently, so this
|
|
55
|
+
* is the only way a caller can see it happened.
|
|
56
|
+
*/
|
|
57
|
+
type DuplicateReport = {
|
|
58
|
+
readonly path: string; /** Always >= 2. */
|
|
59
|
+
readonly count: number; /** The `route` of each add, in order, so colliding sources are nameable. */
|
|
60
|
+
readonly routes: readonly (string | undefined)[];
|
|
30
61
|
};
|
|
31
62
|
//#endregion
|
|
32
|
-
export { ChangeFreq,
|
|
63
|
+
export { ChangeFreq, DuplicateReport, ResolvedSitemapEntry, RouteSummary, SitemapAlternate, SitemapEntry, SitemapOptions };
|
|
33
64
|
//# sourceMappingURL=types.d.mts.map
|
package/esm/url.d.mts
CHANGED
|
@@ -5,25 +5,6 @@
|
|
|
5
5
|
* already carries one.
|
|
6
6
|
*/
|
|
7
7
|
declare function joinOrigin(origin: string, routePath: string): string;
|
|
8
|
-
/**
|
|
9
|
-
* Raised when the sitemap is enabled but no public origin is configured.
|
|
10
|
-
* Refuses to boot rather than falling back to a request-derived origin: a
|
|
11
|
-
* sitemap served with the wrong host is worse than one that refuses to
|
|
12
|
-
* start, because nothing downstream ever tells you it was wrong.
|
|
13
|
-
*/
|
|
14
|
-
declare class MissingPublicUrlError extends Error {
|
|
15
|
-
constructor();
|
|
16
|
-
}
|
|
17
|
-
type ResolveOriginOptions = {
|
|
18
|
-
/** `app.publicUrl` from the app's config, when set. */publicUrl?: string; /** Defaults to `process.env`; overridable for tests. */
|
|
19
|
-
env?: Record<string, string | undefined>;
|
|
20
|
-
};
|
|
21
|
-
/**
|
|
22
|
-
* The origin the sitemap is served from: `app.publicUrl` first, then the
|
|
23
|
-
* `PUBLIC_APP_URL` env fallback. Throws {@link MissingPublicUrlError} when
|
|
24
|
-
* neither is set — this is the boot-time check, called once, not per-request.
|
|
25
|
-
*/
|
|
26
|
-
declare function resolveOrigin(options?: ResolveOriginOptions): string;
|
|
27
8
|
//#endregion
|
|
28
|
-
export {
|
|
9
|
+
export { joinOrigin };
|
|
29
10
|
//# sourceMappingURL=url.d.mts.map
|
package/esm/url.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { InvalidBaseUrlError } from "./errors.mjs";
|
|
2
|
+
|
|
1
3
|
//#region ../sitemap/src/url.ts
|
|
2
4
|
/**
|
|
3
5
|
* Joins a configured origin and an app-relative route path into one absolute
|
|
@@ -8,28 +10,33 @@ function joinOrigin(origin, routePath) {
|
|
|
8
10
|
return `${origin.endsWith("/") ? origin.slice(0, -1) : origin}${routePath.startsWith("/") ? routePath : `/${routePath}`}`;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Validates a `baseUrl` and returns it without its trailing slash.
|
|
14
|
+
*
|
|
15
|
+
* `new URL()` accepts `mailto:` and `file:` happily, so the protocol is
|
|
16
|
+
* checked explicitly — a sitemap `<loc>` that is not http(s) is not a document
|
|
17
|
+
* any crawler will fetch.
|
|
15
18
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
function normalizeBaseUrl(value) {
|
|
20
|
+
if (typeof value !== "string" || value.trim() === "") throw new InvalidBaseUrlError(value, "expected a non-empty string");
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = new URL(value);
|
|
24
|
+
} catch {
|
|
25
|
+
throw new InvalidBaseUrlError(value, "not an absolute URL");
|
|
20
26
|
}
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new InvalidBaseUrlError(value, `unsupported protocol "${parsed.protocol}"`);
|
|
28
|
+
const href = parsed.href;
|
|
29
|
+
return href.endsWith("/") ? href.slice(0, -1) : href;
|
|
30
|
+
}
|
|
31
|
+
/** True for a value already usable as a `<loc>` without joining an origin. */
|
|
32
|
+
function isAbsoluteUrl(value) {
|
|
33
|
+
return /^https?:\/\//i.test(value);
|
|
34
|
+
}
|
|
35
|
+
/** Resolves an entry or alternate path against the base URL, unless it is already absolute. */
|
|
36
|
+
function resolveAgainstBase(baseUrl, path) {
|
|
37
|
+
return isAbsoluteUrl(path) ? path : joinOrigin(baseUrl, path);
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
//#endregion
|
|
34
|
-
export {
|
|
41
|
+
export { joinOrigin, normalizeBaseUrl, resolveAgainstBase };
|
|
35
42
|
//# sourceMappingURL=url.mjs.map
|
package/esm/url.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url.mjs","names":[],"sources":["../../../../../../sitemap/src/url.ts"],"sourcesContent":["/**\n * Joins a configured origin and an app-relative route path into one absolute\n * URL, with exactly one slash at the seam regardless of whether either side\n * already carries one.\n */\nexport function joinOrigin(origin: string, routePath: string): string {\n const trimmedOrigin = origin.endsWith(\"/\") ? origin.slice(0, -1) : origin;\n const normalizedPath = routePath.startsWith(\"/\") ? routePath : `/${routePath}`;\n\n return `${trimmedOrigin}${normalizedPath}`;\n}\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"url.mjs","names":[],"sources":["../../../../../../sitemap/src/url.ts"],"sourcesContent":["import { InvalidBaseUrlError } from \"./errors\";\n\n/**\n * Joins a configured origin and an app-relative route path into one absolute\n * URL, with exactly one slash at the seam regardless of whether either side\n * already carries one.\n */\nexport function joinOrigin(origin: string, routePath: string): string {\n const trimmedOrigin = origin.endsWith(\"/\") ? origin.slice(0, -1) : origin;\n const normalizedPath = routePath.startsWith(\"/\") ? routePath : `/${routePath}`;\n\n return `${trimmedOrigin}${normalizedPath}`;\n}\n\n/**\n * Validates a `baseUrl` and returns it without its trailing slash.\n *\n * `new URL()` accepts `mailto:` and `file:` happily, so the protocol is\n * checked explicitly — a sitemap `<loc>` that is not http(s) is not a document\n * any crawler will fetch.\n */\nexport function normalizeBaseUrl(value: string): string {\n if (typeof value !== \"string\" || value.trim() === \"\") {\n throw new InvalidBaseUrlError(value, \"expected a non-empty string\");\n }\n\n let parsed: URL;\n\n try {\n parsed = new URL(value);\n } catch {\n throw new InvalidBaseUrlError(value, \"not an absolute URL\");\n }\n\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new InvalidBaseUrlError(value, `unsupported protocol \"${parsed.protocol}\"`);\n }\n\n const href = parsed.href;\n\n return href.endsWith(\"/\") ? href.slice(0, -1) : href;\n}\n\n/** True for a value already usable as a `<loc>` without joining an origin. */\nexport function isAbsoluteUrl(value: string): boolean {\n return /^https?:\\/\\//i.test(value);\n}\n\n/** Resolves an entry or alternate path against the base URL, unless it is already absolute. */\nexport function resolveAgainstBase(baseUrl: string, path: string): string {\n return isAbsoluteUrl(path) ? path : joinOrigin(baseUrl, path);\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,WAAW,QAAgB,WAA2B;CAIpE,OAAO,GAHe,OAAO,SAAS,GAAG,IAAI,OAAO,MAAM,GAAG,EAAE,IAAI,SAC5C,UAAU,WAAW,GAAG,IAAI,YAAY,IAAI;AAGrE;;;;;;;;AASA,SAAgB,iBAAiB,OAAuB;CACtD,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,IAChD,MAAM,IAAI,oBAAoB,OAAO,6BAA6B;CAGpE,IAAI;CAEJ,IAAI;EACF,SAAS,IAAI,IAAI,KAAK;CACxB,QAAQ;EACN,MAAM,IAAI,oBAAoB,OAAO,qBAAqB;CAC5D;CAEA,IAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UACrD,MAAM,IAAI,oBAAoB,OAAO,yBAAyB,OAAO,SAAS,EAAE;CAGlF,MAAM,OAAO,OAAO;CAEpB,OAAO,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI;AAClD;;AAGA,SAAgB,cAAc,OAAwB;CACpD,OAAO,gBAAgB,KAAK,KAAK;AACnC;;AAGA,SAAgB,mBAAmB,SAAiB,MAAsB;CACxE,OAAO,cAAc,IAAI,IAAI,OAAO,WAAW,SAAS,IAAI;AAC9D"}
|
package/esm/xml.d.mts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ResolvedSitemapEntry } from "./types.mjs";
|
|
2
2
|
|
|
3
3
|
//#region ../sitemap/src/xml.d.ts
|
|
4
4
|
/** Escapes the five XML-significant characters. A URL's query string routinely contains `&`. */
|
|
5
5
|
declare function escapeXml(value: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Renders one `<url>` block. Exported so the streaming writer can measure the
|
|
8
|
+
* exact bytes it is about to append before deciding whether the byte ceiling
|
|
9
|
+
* forces a new shard — a separate approximation could disagree with what is
|
|
10
|
+
* actually written.
|
|
11
|
+
*/
|
|
12
|
+
declare function renderUrlBlock(entry: ResolvedSitemapEntry, baseUrl: string): string;
|
|
6
13
|
/**
|
|
7
14
|
* Serialises entries into a `urlset` sitemap document — the sitemaps.org
|
|
8
15
|
* namespace, `<url>` per entry, element order `loc` / `lastmod` / `changefreq`
|
|
9
|
-
* / `priority` (schema order; a validator that checks order rejects any other)
|
|
16
|
+
* / `priority` (schema order; a validator that checks order rejects any other),
|
|
17
|
+
* then any `xhtml:link` alternates.
|
|
18
|
+
*
|
|
19
|
+
* The xhtml namespace is declared only when some entry actually carries an
|
|
20
|
+
* alternate — an unused namespace on every single-language sitemap is noise.
|
|
10
21
|
*/
|
|
11
|
-
declare function buildSitemapXml(entries: readonly
|
|
22
|
+
declare function buildSitemapXml(entries: readonly ResolvedSitemapEntry[], baseUrl: string): string;
|
|
12
23
|
//#endregion
|
|
13
|
-
export { buildSitemapXml, escapeXml };
|
|
24
|
+
export { buildSitemapXml, escapeXml, renderUrlBlock };
|
|
14
25
|
//# sourceMappingURL=xml.d.mts.map
|
package/esm/xml.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveAgainstBase } from "./url.mjs";
|
|
2
2
|
|
|
3
3
|
//#region ../sitemap/src/xml.ts
|
|
4
4
|
const XML_ESCAPES = {
|
|
@@ -12,24 +12,46 @@ const XML_ESCAPES = {
|
|
|
12
12
|
function escapeXml(value) {
|
|
13
13
|
return value.replace(/[&<>"']/g, (char) => XML_ESCAPES[char] ?? char);
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
16
|
+
/**
|
|
17
|
+
* The `xmlns:xhtml` attribute exactly as it appears on `<urlset>`, including
|
|
18
|
+
* its leading space. Exported so the shard writer can charge its byte length
|
|
19
|
+
* against the ceiling without duplicating the string it measures.
|
|
20
|
+
*/
|
|
21
|
+
const XHTML_NAMESPACE_ATTR = ` xmlns:xhtml="${XHTML_NAMESPACE}"`;
|
|
22
|
+
/**
|
|
23
|
+
* Renders one `<url>` block. Exported so the streaming writer can measure the
|
|
24
|
+
* exact bytes it is about to append before deciding whether the byte ceiling
|
|
25
|
+
* forces a new shard — a separate approximation could disagree with what is
|
|
26
|
+
* actually written.
|
|
27
|
+
*/
|
|
28
|
+
function renderUrlBlock(entry, baseUrl) {
|
|
29
|
+
const lines = [` <url>`, ` <loc>${escapeXml(resolveAgainstBase(baseUrl, entry.path))}</loc>`];
|
|
17
30
|
if (entry.lastmod !== void 0) lines.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);
|
|
18
31
|
if (entry.changefreq !== void 0) lines.push(` <changefreq>${entry.changefreq}</changefreq>`);
|
|
19
32
|
if (entry.priority !== void 0) lines.push(` <priority>${entry.priority}</priority>`);
|
|
33
|
+
for (const alternate of entry.alternates ?? []) {
|
|
34
|
+
const href = escapeXml(resolveAgainstBase(baseUrl, alternate.path));
|
|
35
|
+
lines.push(` <xhtml:link rel="alternate" hreflang="${escapeXml(alternate.hreflang)}" href="${href}"/>`);
|
|
36
|
+
}
|
|
20
37
|
lines.push(` </url>`);
|
|
21
38
|
return lines.join("\n");
|
|
22
39
|
}
|
|
23
40
|
/**
|
|
24
41
|
* Serialises entries into a `urlset` sitemap document — the sitemaps.org
|
|
25
42
|
* namespace, `<url>` per entry, element order `loc` / `lastmod` / `changefreq`
|
|
26
|
-
* / `priority` (schema order; a validator that checks order rejects any other)
|
|
43
|
+
* / `priority` (schema order; a validator that checks order rejects any other),
|
|
44
|
+
* then any `xhtml:link` alternates.
|
|
45
|
+
*
|
|
46
|
+
* The xhtml namespace is declared only when some entry actually carries an
|
|
47
|
+
* alternate — an unused namespace on every single-language sitemap is noise.
|
|
27
48
|
*/
|
|
28
|
-
function buildSitemapXml(entries,
|
|
29
|
-
const
|
|
30
|
-
|
|
49
|
+
function buildSitemapXml(entries, baseUrl) {
|
|
50
|
+
const namespaces = entries.some((entry) => (entry.alternates?.length ?? 0) > 0) ? XHTML_NAMESPACE_ATTR : "";
|
|
51
|
+
const body = entries.map((entry) => renderUrlBlock(entry, baseUrl)).join("\n");
|
|
52
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"${namespaces}>\n` + (body.length > 0 ? `${body}\n` : "") + `</urlset>\n`;
|
|
31
53
|
}
|
|
32
54
|
|
|
33
55
|
//#endregion
|
|
34
|
-
export { buildSitemapXml, escapeXml };
|
|
56
|
+
export { XHTML_NAMESPACE_ATTR, buildSitemapXml, escapeXml, renderUrlBlock };
|
|
35
57
|
//# sourceMappingURL=xml.mjs.map
|
package/esm/xml.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xml.mjs","names":[],"sources":["../../../../../../sitemap/src/xml.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"file":"xml.mjs","names":[],"sources":["../../../../../../sitemap/src/xml.ts"],"sourcesContent":["import type { ResolvedSitemapEntry } from \"./types\";\nimport { resolveAgainstBase } from \"./url\";\n\nconst XML_ESCAPES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\n/** Escapes the five XML-significant characters. A URL's query string routinely contains `&`. */\nexport function escapeXml(value: string): string {\n // The character class and the table are written together, so the lookup can\n // only miss if one is edited without the other; falling back to the original\n // character keeps that editing mistake from silently emitting `undefined`\n // into a URL.\n return value.replace(/[&<>\"']/g, (char) => XML_ESCAPES[char] ?? char);\n}\n\nconst XHTML_NAMESPACE = \"http://www.w3.org/1999/xhtml\";\n\n/**\n * The `xmlns:xhtml` attribute exactly as it appears on `<urlset>`, including\n * its leading space. Exported so the shard writer can charge its byte length\n * against the ceiling without duplicating the string it measures.\n */\nexport const XHTML_NAMESPACE_ATTR = ` xmlns:xhtml=\"${XHTML_NAMESPACE}\"`;\n\n/**\n * Renders one `<url>` block. Exported so the streaming writer can measure the\n * exact bytes it is about to append before deciding whether the byte ceiling\n * forces a new shard — a separate approximation could disagree with what is\n * actually written.\n */\nexport function renderUrlBlock(entry: ResolvedSitemapEntry, baseUrl: string): string {\n const lines = [` <url>`, ` <loc>${escapeXml(resolveAgainstBase(baseUrl, entry.path))}</loc>`];\n\n if (entry.lastmod !== undefined) {\n lines.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);\n }\n\n if (entry.changefreq !== undefined) {\n lines.push(` <changefreq>${entry.changefreq}</changefreq>`);\n }\n\n if (entry.priority !== undefined) {\n lines.push(` <priority>${entry.priority}</priority>`);\n }\n\n for (const alternate of entry.alternates ?? []) {\n const href = escapeXml(resolveAgainstBase(baseUrl, alternate.path));\n\n lines.push(\n ` <xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(alternate.hreflang)}\" href=\"${href}\"/>`,\n );\n }\n\n lines.push(` </url>`);\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Serialises entries into a `urlset` sitemap document — the sitemaps.org\n * namespace, `<url>` per entry, element order `loc` / `lastmod` / `changefreq`\n * / `priority` (schema order; a validator that checks order rejects any other),\n * then any `xhtml:link` alternates.\n *\n * The xhtml namespace is declared only when some entry actually carries an\n * alternate — an unused namespace on every single-language sitemap is noise.\n */\nexport function buildSitemapXml(entries: readonly ResolvedSitemapEntry[], baseUrl: string): string {\n const hasAlternates = entries.some((entry) => (entry.alternates?.length ?? 0) > 0);\n const namespaces = hasAlternates ? XHTML_NAMESPACE_ATTR : \"\";\n const body = entries.map((entry) => renderUrlBlock(entry, baseUrl)).join(\"\\n\");\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"${namespaces}>\\n` +\n (body.length > 0 ? `${body}\\n` : \"\") +\n `</urlset>\\n`\n );\n}\n"],"mappings":";;;AAGA,MAAM,cAAsC;CAC1C,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAK;CACL,KAAK;AACP;;AAGA,SAAgB,UAAU,OAAuB;CAK/C,OAAO,MAAM,QAAQ,aAAa,SAAS,YAAY,SAAS,IAAI;AACtE;AAEA,MAAM,kBAAkB;;;;;;AAOxB,MAAa,uBAAuB,iBAAiB,gBAAgB;;;;;;;AAQrE,SAAgB,eAAe,OAA6B,SAAyB;CACnF,MAAM,QAAQ,CAAC,WAAW,YAAY,UAAU,mBAAmB,SAAS,MAAM,IAAI,CAAC,EAAE,OAAO;CAEhG,IAAI,MAAM,YAAY,QACpB,MAAM,KAAK,gBAAgB,UAAU,MAAM,OAAO,EAAE,WAAW;CAGjE,IAAI,MAAM,eAAe,QACvB,MAAM,KAAK,mBAAmB,MAAM,WAAW,cAAc;CAG/D,IAAI,MAAM,aAAa,QACrB,MAAM,KAAK,iBAAiB,MAAM,SAAS,YAAY;CAGzD,KAAK,MAAM,aAAa,MAAM,cAAc,CAAC,GAAG;EAC9C,MAAM,OAAO,UAAU,mBAAmB,SAAS,UAAU,IAAI,CAAC;EAElE,MAAM,KACJ,6CAA6C,UAAU,UAAU,QAAQ,EAAE,UAAU,KAAK,IAC5F;CACF;CAEA,MAAM,KAAK,UAAU;CAErB,OAAO,MAAM,KAAK,IAAI;AACxB;;;;;;;;;;AAWA,SAAgB,gBAAgB,SAA0C,SAAyB;CAEjG,MAAM,aADgB,QAAQ,MAAM,WAAW,MAAM,YAAY,UAAU,KAAK,CACjD,IAAI,uBAAuB;CAC1D,MAAM,OAAO,QAAQ,KAAK,UAAU,eAAe,OAAO,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI;CAE7E,OACE,sGAC8D,WAAW,QACxE,KAAK,SAAS,IAAI,GAAG,KAAK,MAAM,MACjC;AAEJ"}
|