@cogenta/seo 0.1.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/dist/feeds.d.ts +66 -0
- package/dist/feeds.d.ts.map +1 -0
- package/dist/feeds.js +199 -0
- package/dist/feeds.js.map +1 -0
- package/dist/hreflang.d.ts +63 -0
- package/dist/hreflang.d.ts.map +1 -0
- package/dist/hreflang.js +86 -0
- package/dist/hreflang.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/indexable.d.ts +28 -0
- package/dist/indexable.d.ts.map +1 -0
- package/dist/indexable.js +39 -0
- package/dist/indexable.js.map +1 -0
- package/dist/indexnow.d.ts +47 -0
- package/dist/indexnow.d.ts.map +1 -0
- package/dist/indexnow.js +136 -0
- package/dist/indexnow.js.map +1 -0
- package/dist/json-ld.d.ts +69 -0
- package/dist/json-ld.d.ts.map +1 -0
- package/dist/json-ld.js +319 -0
- package/dist/json-ld.js.map +1 -0
- package/dist/llms-txt.d.ts +50 -0
- package/dist/llms-txt.d.ts.map +1 -0
- package/dist/llms-txt.js +85 -0
- package/dist/llms-txt.js.map +1 -0
- package/dist/metadata.d.ts +61 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +200 -0
- package/dist/metadata.js.map +1 -0
- package/dist/robots.d.ts +35 -0
- package/dist/robots.d.ts.map +1 -0
- package/dist/robots.js +50 -0
- package/dist/robots.js.map +1 -0
- package/dist/sitemap.d.ts +65 -0
- package/dist/sitemap.d.ts.map +1 -0
- package/dist/sitemap.js +197 -0
- package/dist/sitemap.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/url.d.ts +41 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +110 -0
- package/dist/url.js.map +1 -0
- package/dist/xml.d.ts +32 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +134 -0
- package/dist/xml.js.map +1 -0
- package/package.json +43 -0
package/dist/sitemap.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { CogentaError } from '@cogenta/core';
|
|
2
|
+
import { buildHreflangMap } from './hreflang.js';
|
|
3
|
+
import { indexableResources } from './indexable.js';
|
|
4
|
+
import { absoluteUrl, canonicalUrl } from './url.js';
|
|
5
|
+
import { renderXmlDocument, xmlElementByteLength } from './xml.js';
|
|
6
|
+
/**
|
|
7
|
+
* `sitemap.xml`, split and indexed when it has to be.
|
|
8
|
+
*
|
|
9
|
+
* The two limits are the protocol's, not a preference: sitemaps.org caps a
|
|
10
|
+
* single file at **50 000 URLs and 50 MB uncompressed**, and a file that
|
|
11
|
+
* exceeds either is rejected whole. Both are enforced, because a site can hit
|
|
12
|
+
* the byte limit well before the URL limit once `xhtml:link` alternates are
|
|
13
|
+
* included — twelve languages multiply the size of every entry by roughly
|
|
14
|
+
* thirteen while the URL count stays put.
|
|
15
|
+
*/
|
|
16
|
+
/** sitemaps.org: 50 000 URLs per file, and the same cap on entries in an index. */
|
|
17
|
+
export const SITEMAP_MAX_URLS = 50_000;
|
|
18
|
+
/** sitemaps.org: 50 MB uncompressed, counted as 50 × 2²⁰ bytes. */
|
|
19
|
+
export const SITEMAP_MAX_BYTES = 52_428_800;
|
|
20
|
+
const SITEMAP_NS = 'http://www.sitemaps.org/schemas/sitemap/0.9';
|
|
21
|
+
const XHTML_NS = 'http://www.w3.org/1999/xhtml';
|
|
22
|
+
const DEFAULT_INDEX_PATH = '/sitemap.xml';
|
|
23
|
+
const defaultChunkPath = (index) => `/sitemap-${index}.xml`;
|
|
24
|
+
function urlElement(url) {
|
|
25
|
+
const children = [{ name: 'loc', text: url.loc }];
|
|
26
|
+
if (url.lastmod !== undefined)
|
|
27
|
+
children.push({ name: 'lastmod', text: url.lastmod });
|
|
28
|
+
if (url.changefreq !== undefined)
|
|
29
|
+
children.push({ name: 'changefreq', text: url.changefreq });
|
|
30
|
+
if (url.priority !== undefined) {
|
|
31
|
+
children.push({ name: 'priority', text: clampPriority(url.priority) });
|
|
32
|
+
}
|
|
33
|
+
for (const alternate of url.alternates ?? []) {
|
|
34
|
+
children.push({
|
|
35
|
+
name: 'xhtml:link',
|
|
36
|
+
attributes: { rel: 'alternate', hreflang: alternate.hreflang, href: alternate.href },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return { name: 'url', children };
|
|
40
|
+
}
|
|
41
|
+
function clampPriority(priority) {
|
|
42
|
+
const bounded = Math.min(1, Math.max(0, priority));
|
|
43
|
+
return bounded.toFixed(1);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Bytes a chunk costs before any URL: declaration, root tag with both
|
|
47
|
+
* namespaces, closing tag and newlines. Measured rather than estimated so the
|
|
48
|
+
* budget stays right if the header ever changes.
|
|
49
|
+
*/
|
|
50
|
+
function chunkOverhead() {
|
|
51
|
+
return Buffer.byteLength(renderXmlDocument(urlsetElement([])), 'utf8');
|
|
52
|
+
}
|
|
53
|
+
function urlsetElement(urls) {
|
|
54
|
+
return {
|
|
55
|
+
name: 'urlset',
|
|
56
|
+
attributes: { xmlns: SITEMAP_NS, 'xmlns:xhtml': XHTML_NS },
|
|
57
|
+
children: urls.map(urlElement),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function splitIntoChunks(urls, maxUrls, maxBytes) {
|
|
61
|
+
const overhead = chunkOverhead();
|
|
62
|
+
const chunks = [];
|
|
63
|
+
let current = [];
|
|
64
|
+
let size = overhead;
|
|
65
|
+
for (const url of urls) {
|
|
66
|
+
// +1 for the newline the renderer puts between siblings.
|
|
67
|
+
const cost = xmlElementByteLength(urlElement(url), 1) + 1;
|
|
68
|
+
if (overhead + cost > maxBytes) {
|
|
69
|
+
throw new CogentaError({
|
|
70
|
+
code: 'CONTENT_INVALID',
|
|
71
|
+
message: `A single sitemap entry for "${url.loc}" exceeds the ${maxBytes}-byte file limit.`,
|
|
72
|
+
hint: 'It almost certainly carries far too many hreflang alternates. A sitemap entry cannot be split across files.',
|
|
73
|
+
details: { loc: url.loc, bytes: cost, maxBytes },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (current.length >= maxUrls || size + cost > maxBytes) {
|
|
77
|
+
chunks.push(current);
|
|
78
|
+
current = [];
|
|
79
|
+
size = overhead;
|
|
80
|
+
}
|
|
81
|
+
current.push(url);
|
|
82
|
+
size += cost;
|
|
83
|
+
}
|
|
84
|
+
if (current.length > 0 || chunks.length === 0)
|
|
85
|
+
chunks.push(current);
|
|
86
|
+
return chunks;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The files to write for a set of URLs.
|
|
90
|
+
*
|
|
91
|
+
* One file below the limits, an index plus N chunks above them. The caller
|
|
92
|
+
* writes what it gets and never has to know which case it is in — a shape that
|
|
93
|
+
* makes the split untestable in production is how sites discover at 60 000 URLs
|
|
94
|
+
* that nobody ever exercised the second branch.
|
|
95
|
+
*/
|
|
96
|
+
export function buildSitemap(site, urls, options = {}) {
|
|
97
|
+
const maxUrls = options.maxUrls ?? SITEMAP_MAX_URLS;
|
|
98
|
+
const maxBytes = options.maxBytes ?? SITEMAP_MAX_BYTES;
|
|
99
|
+
if (maxUrls < 1 || maxBytes < 1) {
|
|
100
|
+
throw new CogentaError({
|
|
101
|
+
code: 'CONFIG_INVALID',
|
|
102
|
+
message: 'A sitemap file must allow at least one URL and one byte.',
|
|
103
|
+
hint: `Leave maxUrls and maxBytes unset to use the protocol limits (${SITEMAP_MAX_URLS} URLs, ${SITEMAP_MAX_BYTES} bytes).`,
|
|
104
|
+
details: { maxUrls, maxBytes },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const indexPath = options.indexPath ?? DEFAULT_INDEX_PATH;
|
|
108
|
+
const chunkPath = options.chunkPath ?? defaultChunkPath;
|
|
109
|
+
const chunks = splitIntoChunks(urls, maxUrls, maxBytes);
|
|
110
|
+
if (chunks.length === 1) {
|
|
111
|
+
const only = chunks[0] ?? [];
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
path: indexPath,
|
|
115
|
+
contents: renderXmlDocument(urlsetElement(only)),
|
|
116
|
+
isIndex: false,
|
|
117
|
+
urlCount: only.length,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
// The index cap is the protocol's own, deliberately *not* `maxUrls`. The two
|
|
122
|
+
// limits happen to share a number, and conflating them means that lowering
|
|
123
|
+
// `maxUrls` — which a caller does to force a split, and a test does to
|
|
124
|
+
// exercise one — also lowers how many files an index may list, so the split
|
|
125
|
+
// it just asked for is rejected.
|
|
126
|
+
const maxIndexEntries = options.maxIndexEntries ?? SITEMAP_MAX_URLS;
|
|
127
|
+
if (chunks.length > maxIndexEntries) {
|
|
128
|
+
throw new CogentaError({
|
|
129
|
+
code: 'CONTENT_INVALID',
|
|
130
|
+
message: `The site needs ${chunks.length} sitemap files, more than the ${maxIndexEntries} an index may list.`,
|
|
131
|
+
hint: 'A sitemap index cannot itself be indexed. Split the site across several hosts, or raise maxUrls so each file holds more.',
|
|
132
|
+
details: { files: chunks.length, maxIndexEntries },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const files = chunks.map((chunk, position) => ({
|
|
136
|
+
path: chunkPath(position + 1),
|
|
137
|
+
contents: renderXmlDocument(urlsetElement(chunk)),
|
|
138
|
+
isIndex: false,
|
|
139
|
+
urlCount: chunk.length,
|
|
140
|
+
}));
|
|
141
|
+
const lastmod = options.lastmod ?? newestLastmod(urls);
|
|
142
|
+
const index = {
|
|
143
|
+
name: 'sitemapindex',
|
|
144
|
+
attributes: { xmlns: SITEMAP_NS },
|
|
145
|
+
children: files.map((file) => ({
|
|
146
|
+
name: 'sitemap',
|
|
147
|
+
children: [
|
|
148
|
+
{ name: 'loc', text: absoluteUrl(site, file.path) },
|
|
149
|
+
lastmod === undefined ? null : { name: 'lastmod', text: lastmod },
|
|
150
|
+
],
|
|
151
|
+
})),
|
|
152
|
+
};
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
path: indexPath,
|
|
156
|
+
contents: renderXmlDocument(index),
|
|
157
|
+
isIndex: true,
|
|
158
|
+
urlCount: files.length,
|
|
159
|
+
},
|
|
160
|
+
...files,
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
function newestLastmod(urls) {
|
|
164
|
+
let newest;
|
|
165
|
+
for (const url of urls) {
|
|
166
|
+
if (url.lastmod === undefined)
|
|
167
|
+
continue;
|
|
168
|
+
if (newest === undefined || url.lastmod > newest)
|
|
169
|
+
newest = url.lastmod;
|
|
170
|
+
}
|
|
171
|
+
return newest;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Sitemap URLs for a set of resources: published only, alternates attached.
|
|
175
|
+
*
|
|
176
|
+
* `lastmod` is `updatedAt` rather than `publishedAt`: the field answers "has
|
|
177
|
+
* this changed since you last fetched it", which is what makes a crawler come
|
|
178
|
+
* back for a corrected article.
|
|
179
|
+
*/
|
|
180
|
+
export function sitemapUrlsFor(site, resources, options = {}) {
|
|
181
|
+
const published = indexableResources(site, resources, options);
|
|
182
|
+
const hreflang = buildHreflangMap(site, resources, options);
|
|
183
|
+
const urls = [];
|
|
184
|
+
for (const resource of published) {
|
|
185
|
+
const loc = canonicalUrl(site, resource);
|
|
186
|
+
if (loc === null)
|
|
187
|
+
continue;
|
|
188
|
+
const alternates = hreflang.get(resource.entry.id);
|
|
189
|
+
urls.push({
|
|
190
|
+
loc,
|
|
191
|
+
lastmod: resource.entry.updatedAt,
|
|
192
|
+
...(alternates === undefined ? {} : { alternates }),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return urls;
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=sitemap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { CollectionDefinition, ContentEntry } from '@cogenta/schema';
|
|
2
|
+
/**
|
|
3
|
+
* What the SEO layer needs to know about the site.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately not the whole site configuration: this package is consumed by
|
|
6
|
+
* the render process, which owns neither secrets nor a database connection
|
|
7
|
+
* (rule R5). Everything here is public information that already appears in the
|
|
8
|
+
* page source.
|
|
9
|
+
*/
|
|
10
|
+
export interface SeoSite {
|
|
11
|
+
/** Absolute origin, with or without a path prefix. `https://example.com`. */
|
|
12
|
+
readonly baseUrl: string;
|
|
13
|
+
/** The site name, used in `og:site_name` and as the feed title. */
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly description?: string;
|
|
16
|
+
/** The language a URL carries when it has no locale prefix. */
|
|
17
|
+
readonly defaultLocale: string;
|
|
18
|
+
/** Every language the site serves. Used to validate what a family claims. */
|
|
19
|
+
readonly locales?: readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* Whether the default locale is served without its prefix — `/blog/hello`
|
|
22
|
+
* next to `/fr/blog/bonjour`.
|
|
23
|
+
*
|
|
24
|
+
* It has to be stated rather than guessed: `buildPath` always prefixes a
|
|
25
|
+
* localised route, while `matchPath` accepts both forms, so only the site
|
|
26
|
+
* knows which of the two a crawler will find. Guessing produces canonical
|
|
27
|
+
* URLs that 301 to themselves.
|
|
28
|
+
*/
|
|
29
|
+
readonly unprefixedDefaultLocale?: boolean;
|
|
30
|
+
/** `@handle` for `twitter:site`. */
|
|
31
|
+
readonly twitterSite?: string;
|
|
32
|
+
}
|
|
33
|
+
/** An entry together with the collection that describes it. */
|
|
34
|
+
export interface SeoResource<TEntry extends ContentEntry = ContentEntry> {
|
|
35
|
+
readonly collection: CollectionDefinition;
|
|
36
|
+
readonly entry: TEntry;
|
|
37
|
+
}
|
|
38
|
+
export interface SeoImage {
|
|
39
|
+
readonly url: string;
|
|
40
|
+
readonly width?: number;
|
|
41
|
+
readonly height?: number;
|
|
42
|
+
readonly alt?: string;
|
|
43
|
+
readonly mimeType?: string;
|
|
44
|
+
}
|
|
45
|
+
/** A related entity — an author, a tag — reduced to what a crawler can use. */
|
|
46
|
+
export interface SeoReference {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly url?: string;
|
|
49
|
+
/** schema.org type, when the caller knows it. `Person` for an author. */
|
|
50
|
+
readonly type?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* How to turn an identifier into something publishable.
|
|
54
|
+
*
|
|
55
|
+
* A `media` field stores a media id and a `relation` field stores an entry id;
|
|
56
|
+
* neither is meaningful to a crawler. Resolution needs the media pipeline and
|
|
57
|
+
* the content store, which this package must not reach, so it is injected.
|
|
58
|
+
*
|
|
59
|
+
* Both are optional, and an unresolved id is **omitted** rather than emitted
|
|
60
|
+
* raw: `"image": "0192f3a1-…"` is worse than no image at all, because it makes
|
|
61
|
+
* structured data invalid rather than incomplete.
|
|
62
|
+
*/
|
|
63
|
+
export interface SeoResolvers {
|
|
64
|
+
readonly media?: (id: string) => SeoImage | null;
|
|
65
|
+
readonly reference?: (collection: string, id: string) => SeoReference | null;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAEzE;;;;;;;GAOG;AACH,MAAM,WAAW,OAAO;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,+DAA+D;IAC/D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAA;IAC1C,oCAAoC;IACpC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,+DAA+D;AAC/D,MAAM,WAAW,WAAW,CAAC,MAAM,SAAS,YAAY,GAAG,YAAY;IACrE,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAA;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,yEAAyE;IACzE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,QAAQ,GAAG,IAAI,CAAA;IAChD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,YAAY,GAAG,IAAI,CAAA;CAC7E"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/url.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type CollectionDefinition, type ContentEntry } from '@cogenta/schema';
|
|
2
|
+
import type { SeoResource, SeoSite } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Every URL this package emits is absolute, and every one of them is built
|
|
5
|
+
* here.
|
|
6
|
+
*
|
|
7
|
+
* Sitemaps, feeds and `hreflang` all reject relative URLs, and a canonical that
|
|
8
|
+
* disagrees with the sitemap by a trailing slash is treated as a different page
|
|
9
|
+
* — the single most common cause of "Google indexed the wrong URL". One
|
|
10
|
+
* function, one form.
|
|
11
|
+
*/
|
|
12
|
+
/** `https://example.com/blog/` and `https://example.com/blog` are the same origin. */
|
|
13
|
+
export declare function normaliseBaseUrl(baseUrl: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* An absolute URL for a site-relative path.
|
|
16
|
+
*
|
|
17
|
+
* The path is expected to be already encoded — `buildPath` percent-encodes each
|
|
18
|
+
* segment — so it is concatenated rather than passed through `new URL`, which
|
|
19
|
+
* would double-encode a `%` that is already an escape.
|
|
20
|
+
*/
|
|
21
|
+
export declare function absoluteUrl(site: SeoSite, path: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* The route parameters an entry supplies.
|
|
24
|
+
*
|
|
25
|
+
* A pattern segment `:slug` reads `values.slug`; `:id` reads the system field,
|
|
26
|
+
* since it is the only parameter an entry always has and the natural fallback
|
|
27
|
+
* for a collection with no slug.
|
|
28
|
+
*/
|
|
29
|
+
export declare function routeParams(collection: CollectionDefinition, entry: ContentEntry): Record<string, string>;
|
|
30
|
+
/** True when the collection can produce a URL at all. */
|
|
31
|
+
export declare function hasRoute(collection: CollectionDefinition): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The canonical, absolute URL of an entry, or null when it has no route.
|
|
34
|
+
*
|
|
35
|
+
* Null rather than an exception: a collection without `routing` is a perfectly
|
|
36
|
+
* ordinary thing — an author, a tag, a site setting — and asking "what is the
|
|
37
|
+
* canonical URL of every entry" must not blow up on the first one that has
|
|
38
|
+
* none.
|
|
39
|
+
*/
|
|
40
|
+
export declare function canonicalUrl(site: SeoSite, resource: SeoResource): string | null;
|
|
41
|
+
//# sourceMappingURL=url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,oBAAoB,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACzF,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEtD;;;;;;;;GAQG;AAEH,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAwBxD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,oBAAoB,EAChC,KAAK,EAAE,YAAY,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwBxB;AAED,yDAAyD;AACzD,wBAAgB,QAAQ,CAAC,UAAU,EAAE,oBAAoB,GAAG,OAAO,CAElE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAoBhF"}
|
package/dist/url.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { CogentaError } from '@cogenta/core';
|
|
2
|
+
import { buildPath } from '@cogenta/schema';
|
|
3
|
+
/**
|
|
4
|
+
* Every URL this package emits is absolute, and every one of them is built
|
|
5
|
+
* here.
|
|
6
|
+
*
|
|
7
|
+
* Sitemaps, feeds and `hreflang` all reject relative URLs, and a canonical that
|
|
8
|
+
* disagrees with the sitemap by a trailing slash is treated as a different page
|
|
9
|
+
* — the single most common cause of "Google indexed the wrong URL". One
|
|
10
|
+
* function, one form.
|
|
11
|
+
*/
|
|
12
|
+
/** `https://example.com/blog/` and `https://example.com/blog` are the same origin. */
|
|
13
|
+
export function normaliseBaseUrl(baseUrl) {
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = new URL(baseUrl);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new CogentaError({
|
|
20
|
+
code: 'CONFIG_INVALID',
|
|
21
|
+
message: `The site base URL "${baseUrl}" is not a URL.`,
|
|
22
|
+
hint: 'Give an absolute origin, protocol included: https://example.com',
|
|
23
|
+
details: { baseUrl },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
27
|
+
throw new CogentaError({
|
|
28
|
+
code: 'CONFIG_INVALID',
|
|
29
|
+
message: `The site base URL uses "${parsed.protocol}", which no crawler follows.`,
|
|
30
|
+
hint: 'Use http or https. Sitemaps and feeds carry absolute http(s) URLs only.',
|
|
31
|
+
details: { baseUrl },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const path = parsed.pathname.replace(/\/+$/u, '');
|
|
35
|
+
return `${parsed.origin}${path}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* An absolute URL for a site-relative path.
|
|
39
|
+
*
|
|
40
|
+
* The path is expected to be already encoded — `buildPath` percent-encodes each
|
|
41
|
+
* segment — so it is concatenated rather than passed through `new URL`, which
|
|
42
|
+
* would double-encode a `%` that is already an escape.
|
|
43
|
+
*/
|
|
44
|
+
export function absoluteUrl(site, path) {
|
|
45
|
+
const base = normaliseBaseUrl(site.baseUrl);
|
|
46
|
+
if (path.length === 0 || path === '/')
|
|
47
|
+
return `${base}/`;
|
|
48
|
+
return `${base}${path.startsWith('/') ? path : `/${path}`}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The route parameters an entry supplies.
|
|
52
|
+
*
|
|
53
|
+
* A pattern segment `:slug` reads `values.slug`; `:id` reads the system field,
|
|
54
|
+
* since it is the only parameter an entry always has and the natural fallback
|
|
55
|
+
* for a collection with no slug.
|
|
56
|
+
*/
|
|
57
|
+
export function routeParams(collection, entry) {
|
|
58
|
+
const pattern = collection.routing?.pattern ?? '';
|
|
59
|
+
const params = {};
|
|
60
|
+
for (const segment of pattern.split('/')) {
|
|
61
|
+
if (!segment.startsWith(':'))
|
|
62
|
+
continue;
|
|
63
|
+
const name = segment.slice(1);
|
|
64
|
+
if (name === 'id') {
|
|
65
|
+
params[name] = entry.id;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const value = entry.values[name];
|
|
69
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
70
|
+
params[name] = value;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === 'number') {
|
|
74
|
+
params[name] = String(value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return params;
|
|
78
|
+
}
|
|
79
|
+
/** True when the collection can produce a URL at all. */
|
|
80
|
+
export function hasRoute(collection) {
|
|
81
|
+
return collection.routing !== undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The canonical, absolute URL of an entry, or null when it has no route.
|
|
85
|
+
*
|
|
86
|
+
* Null rather than an exception: a collection without `routing` is a perfectly
|
|
87
|
+
* ordinary thing — an author, a tag, a site setting — and asking "what is the
|
|
88
|
+
* canonical URL of every entry" must not blow up on the first one that has
|
|
89
|
+
* none.
|
|
90
|
+
*/
|
|
91
|
+
export function canonicalUrl(site, resource) {
|
|
92
|
+
const { collection, entry } = resource;
|
|
93
|
+
const routing = collection.routing;
|
|
94
|
+
if (routing === undefined)
|
|
95
|
+
return null;
|
|
96
|
+
const params = routeParams(collection, entry);
|
|
97
|
+
for (const segment of routing.pattern.split('/')) {
|
|
98
|
+
if (segment.startsWith(':') && params[segment.slice(1)] === undefined)
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const localised = routing.locale === true;
|
|
102
|
+
const path = buildPath(collection, params, localised ? entry.locale : undefined);
|
|
103
|
+
if (localised && site.unprefixedDefaultLocale === true && entry.locale === site.defaultLocale) {
|
|
104
|
+
const prefix = `/${encodeURIComponent(entry.locale)}`;
|
|
105
|
+
const stripped = path.slice(prefix.length);
|
|
106
|
+
return absoluteUrl(site, stripped.length === 0 ? '/' : stripped);
|
|
107
|
+
}
|
|
108
|
+
return absoluteUrl(site, path);
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=url.js.map
|
package/dist/url.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.js","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"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"}
|
package/dist/xml.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare function stripIllegalXmlChars(value: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Text content.
|
|
4
|
+
*
|
|
5
|
+
* `>` is escaped although it is only required inside a `]]>` run. Escaping it
|
|
6
|
+
* unconditionally costs three bytes and removes the need for anyone to reason
|
|
7
|
+
* about where the exception applies.
|
|
8
|
+
*/
|
|
9
|
+
export declare function escapeXmlText(value: string): string;
|
|
10
|
+
export declare function escapeXmlAttribute(value: string): string;
|
|
11
|
+
export type XmlAttributes = Readonly<Record<string, string | number | undefined>>;
|
|
12
|
+
export interface XmlElement {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly attributes?: XmlAttributes;
|
|
15
|
+
/** Text content. Ignored when `children` holds anything. */
|
|
16
|
+
readonly text?: string;
|
|
17
|
+
/** `null` entries are dropped, so a conditional child needs no array surgery. */
|
|
18
|
+
readonly children?: readonly (XmlElement | null | undefined)[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A whole document, declaration included.
|
|
22
|
+
*
|
|
23
|
+
* UTF-8 is stated rather than assumed: a sitemap served without a charset
|
|
24
|
+
* header and without a declaration is read as US-ASCII by a conforming parser,
|
|
25
|
+
* which mangles every non-Latin slug on the site.
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderXmlDocument(root: XmlElement): string;
|
|
28
|
+
/** The serialised size of one element, used to keep a sitemap file under its byte budget. */
|
|
29
|
+
export declare function xmlElementByteLength(element: XmlElement, depth: number): number;
|
|
30
|
+
/** Exposed so a caller measuring a document can measure exactly what will be written. */
|
|
31
|
+
export declare function renderXmlElement(element: XmlElement, depth?: number): string;
|
|
32
|
+
//# sourceMappingURL=xml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml.d.ts","sourceRoot":"","sources":["../src/xml.ts"],"names":[],"mappings":"AA8CA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAQD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD;AAiBD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKxD;AAED,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;AAEjF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,UAAU,CAAC,EAAE,aAAa,CAAA;IACnC,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAA;CAC/D;AAsDD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAE1D;AAED,6FAA6F;AAC7F,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,SAAI,GAAG,MAAM,CAEvE"}
|
package/dist/xml.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { CogentaError } from '@cogenta/core';
|
|
2
|
+
/**
|
|
3
|
+
* XML serialisation, written by hand and on purpose (rule R9).
|
|
4
|
+
*
|
|
5
|
+
* The reason this file exists rather than a dependency: the whole of Cogenta's
|
|
6
|
+
* XML output is four fixed document shapes — sitemap, sitemap index, RSS, Atom
|
|
7
|
+
* — and the only hard part is escaping. A library would bring a parser, a DOM
|
|
8
|
+
* and a stream API to solve a problem that is one substitution table wide.
|
|
9
|
+
*
|
|
10
|
+
* Escaping is where sitemaps and feeds actually break. A crawler does not
|
|
11
|
+
* repair a malformed document: it rejects the file whole, so one article whose
|
|
12
|
+
* title contains `<` silently removes every other URL in the same file from the
|
|
13
|
+
* index. That failure is invisible in production — the file is served with a
|
|
14
|
+
* 200 and looks fine in a browser — which is why it is tested here against a
|
|
15
|
+
* real parse rather than against a snapshot.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Characters XML 1.0 forbids outright, escaped or not.
|
|
19
|
+
*
|
|
20
|
+
* `�` is not a legal escape for a NUL: the character is simply not
|
|
21
|
+
* representable, so the only correct handling is removal. Content reaches us
|
|
22
|
+
* from imports and from agents, both of which produce stray control bytes, and
|
|
23
|
+
* a single one of them makes the document unparsable.
|
|
24
|
+
*/
|
|
25
|
+
// Assembled from code points rather than written as a regex literal. A literal
|
|
26
|
+
// is what a reader would expect, and it is the one form that cannot be used: it
|
|
27
|
+
// puts control characters in the source, where every editor, diff and terminal
|
|
28
|
+
// renders them differently, and the linter rejects it for exactly that reason.
|
|
29
|
+
// Tab (09), newline (0A) and carriage return (0D) are absent from the ranges —
|
|
30
|
+
// XML allows all three, and stripping them would silently reflow content.
|
|
31
|
+
const ILLEGAL_XML_RANGES = [
|
|
32
|
+
[0x00, 0x08],
|
|
33
|
+
[0x0b, 0x0c],
|
|
34
|
+
[0x0e, 0x1f],
|
|
35
|
+
[0xfffe, 0xffff],
|
|
36
|
+
];
|
|
37
|
+
const ILLEGAL_XML_CHARS = new RegExp(`[${ILLEGAL_XML_RANGES.map(([from, to]) => `${String.fromCodePoint(from)}-${String.fromCodePoint(to)}`).join('')}]`, 'gu');
|
|
38
|
+
export function stripIllegalXmlChars(value) {
|
|
39
|
+
return value.replace(ILLEGAL_XML_CHARS, '');
|
|
40
|
+
}
|
|
41
|
+
const TEXT_ESCAPES = {
|
|
42
|
+
'&': '&',
|
|
43
|
+
'<': '<',
|
|
44
|
+
'>': '>',
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Text content.
|
|
48
|
+
*
|
|
49
|
+
* `>` is escaped although it is only required inside a `]]>` run. Escaping it
|
|
50
|
+
* unconditionally costs three bytes and removes the need for anyone to reason
|
|
51
|
+
* about where the exception applies.
|
|
52
|
+
*/
|
|
53
|
+
export function escapeXmlText(value) {
|
|
54
|
+
return stripIllegalXmlChars(value).replace(/[&<>]/gu, (char) => TEXT_ESCAPES[char] ?? char);
|
|
55
|
+
}
|
|
56
|
+
const ATTRIBUTE_ESCAPES = {
|
|
57
|
+
'&': '&',
|
|
58
|
+
'<': '<',
|
|
59
|
+
'>': '>',
|
|
60
|
+
'"': '"',
|
|
61
|
+
"'": ''',
|
|
62
|
+
// Attribute-value normalisation turns a literal tab, newline or carriage
|
|
63
|
+
// return into a space before the application ever sees it. A title that wraps
|
|
64
|
+
// would come back out of the parser silently different, so the whitespace is
|
|
65
|
+
// written as a character reference, which normalisation leaves alone.
|
|
66
|
+
'\t': '	',
|
|
67
|
+
'\n': ' ',
|
|
68
|
+
'\r': ' ',
|
|
69
|
+
};
|
|
70
|
+
export function escapeXmlAttribute(value) {
|
|
71
|
+
return stripIllegalXmlChars(value).replace(/[&<>"'\t\n\r]/gu, (char) => ATTRIBUTE_ESCAPES[char] ?? char);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A conservative subset of the XML `Name` production: ASCII letters, digits,
|
|
75
|
+
* `_`, `-`, `.` and one `:` for a namespace prefix. Every tag this package
|
|
76
|
+
* emits is a literal, so this only ever fires on a programming mistake — but a
|
|
77
|
+
* mistake that would produce a document no crawler can read.
|
|
78
|
+
*/
|
|
79
|
+
const XML_NAME = /^[A-Za-z_][\w.-]*(?::[A-Za-z_][\w.-]*)?$/;
|
|
80
|
+
function assertName(name) {
|
|
81
|
+
if (XML_NAME.test(name))
|
|
82
|
+
return;
|
|
83
|
+
throw new CogentaError({
|
|
84
|
+
code: 'CONTENT_INVALID',
|
|
85
|
+
message: `"${name}" is not a usable XML element or attribute name.`,
|
|
86
|
+
hint: 'Element and attribute names are fixed by the feed format; they are never built from content.',
|
|
87
|
+
details: { name },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function renderAttributes(attributes) {
|
|
91
|
+
if (attributes === undefined)
|
|
92
|
+
return '';
|
|
93
|
+
let rendered = '';
|
|
94
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
95
|
+
if (value === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
assertName(name);
|
|
98
|
+
rendered += ` ${name}="${escapeXmlAttribute(String(value))}"`;
|
|
99
|
+
}
|
|
100
|
+
return rendered;
|
|
101
|
+
}
|
|
102
|
+
function renderElement(element, depth) {
|
|
103
|
+
assertName(element.name);
|
|
104
|
+
const pad = ' '.repeat(depth);
|
|
105
|
+
const open = `${pad}<${element.name}${renderAttributes(element.attributes)}`;
|
|
106
|
+
const children = (element.children ?? []).filter((child) => child !== null && child !== undefined);
|
|
107
|
+
if (children.length > 0) {
|
|
108
|
+
const inner = children.map((child) => renderElement(child, depth + 1)).join('\n');
|
|
109
|
+
return `${open}>\n${inner}\n${pad}</${element.name}>`;
|
|
110
|
+
}
|
|
111
|
+
if (element.text !== undefined) {
|
|
112
|
+
return `${open}>${escapeXmlText(element.text)}</${element.name}>`;
|
|
113
|
+
}
|
|
114
|
+
return `${open} />`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* A whole document, declaration included.
|
|
118
|
+
*
|
|
119
|
+
* UTF-8 is stated rather than assumed: a sitemap served without a charset
|
|
120
|
+
* header and without a declaration is read as US-ASCII by a conforming parser,
|
|
121
|
+
* which mangles every non-Latin slug on the site.
|
|
122
|
+
*/
|
|
123
|
+
export function renderXmlDocument(root) {
|
|
124
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n${renderElement(root, 0)}\n`;
|
|
125
|
+
}
|
|
126
|
+
/** The serialised size of one element, used to keep a sitemap file under its byte budget. */
|
|
127
|
+
export function xmlElementByteLength(element, depth) {
|
|
128
|
+
return Buffer.byteLength(renderElement(element, depth), 'utf8');
|
|
129
|
+
}
|
|
130
|
+
/** Exposed so a caller measuring a document can measure exactly what will be written. */
|
|
131
|
+
export function renderXmlElement(element, depth = 0) {
|
|
132
|
+
return renderElement(element, depth);
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=xml.js.map
|
package/dist/xml.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml.js","sourceRoot":"","sources":["../src/xml.ts"],"names":[],"mappings":"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"}
|