@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.
Files changed (53) hide show
  1. package/dist/feeds.d.ts +66 -0
  2. package/dist/feeds.d.ts.map +1 -0
  3. package/dist/feeds.js +199 -0
  4. package/dist/feeds.js.map +1 -0
  5. package/dist/hreflang.d.ts +63 -0
  6. package/dist/hreflang.d.ts.map +1 -0
  7. package/dist/hreflang.js +86 -0
  8. package/dist/hreflang.js.map +1 -0
  9. package/dist/index.d.ts +40 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +29 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/indexable.d.ts +28 -0
  14. package/dist/indexable.d.ts.map +1 -0
  15. package/dist/indexable.js +39 -0
  16. package/dist/indexable.js.map +1 -0
  17. package/dist/indexnow.d.ts +47 -0
  18. package/dist/indexnow.d.ts.map +1 -0
  19. package/dist/indexnow.js +136 -0
  20. package/dist/indexnow.js.map +1 -0
  21. package/dist/json-ld.d.ts +69 -0
  22. package/dist/json-ld.d.ts.map +1 -0
  23. package/dist/json-ld.js +319 -0
  24. package/dist/json-ld.js.map +1 -0
  25. package/dist/llms-txt.d.ts +50 -0
  26. package/dist/llms-txt.d.ts.map +1 -0
  27. package/dist/llms-txt.js +85 -0
  28. package/dist/llms-txt.js.map +1 -0
  29. package/dist/metadata.d.ts +61 -0
  30. package/dist/metadata.d.ts.map +1 -0
  31. package/dist/metadata.js +200 -0
  32. package/dist/metadata.js.map +1 -0
  33. package/dist/robots.d.ts +35 -0
  34. package/dist/robots.d.ts.map +1 -0
  35. package/dist/robots.js +50 -0
  36. package/dist/robots.js.map +1 -0
  37. package/dist/sitemap.d.ts +65 -0
  38. package/dist/sitemap.d.ts.map +1 -0
  39. package/dist/sitemap.js +197 -0
  40. package/dist/sitemap.js.map +1 -0
  41. package/dist/types.d.ts +67 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +2 -0
  44. package/dist/types.js.map +1 -0
  45. package/dist/url.d.ts +41 -0
  46. package/dist/url.d.ts.map +1 -0
  47. package/dist/url.js +110 -0
  48. package/dist/url.js.map +1 -0
  49. package/dist/xml.d.ts +32 -0
  50. package/dist/xml.d.ts.map +1 -0
  51. package/dist/xml.js +134 -0
  52. package/dist/xml.js.map +1 -0
  53. package/package.json +43 -0
@@ -0,0 +1,85 @@
1
+ import { condense } from '@cogenta/schema';
2
+ import { indexableResources } from './indexable.js';
3
+ import { canonicalUrl } from './url.js';
4
+ /**
5
+ * Markdown link escaping.
6
+ *
7
+ * A title containing `]` closes the link text early and the rest of the line
8
+ * becomes prose; a URL containing a space or `)` breaks the target. Titles are
9
+ * escaped, URLs are wrapped in angle brackets, which is the Markdown-specified
10
+ * way to carry a URL with awkward characters.
11
+ */
12
+ function escapeLinkText(value) {
13
+ return condense(value).replace(/([[\]\\])/gu, '\\$1');
14
+ }
15
+ function renderUrl(url) {
16
+ return /[()\s]/u.test(url) ? `<${url}>` : url;
17
+ }
18
+ export function renderLlmsTxt(options) {
19
+ const lines = [`# ${escapeLinkText(options.title ?? options.site.name)}`];
20
+ const summary = options.summary ?? options.site.description;
21
+ if (summary !== undefined && summary.length > 0) {
22
+ lines.push('', `> ${condense(summary)}`);
23
+ }
24
+ for (const paragraph of options.details ?? []) {
25
+ lines.push('', condense(paragraph));
26
+ }
27
+ for (const section of options.sections) {
28
+ if (section.links.length === 0)
29
+ continue;
30
+ lines.push('', `## ${escapeLinkText(section.title)}`, '');
31
+ for (const link of section.links) {
32
+ const note = link.note === undefined ? '' : `: ${condense(link.note)}`;
33
+ lines.push(`- [${escapeLinkText(link.title)}](${renderUrl(link.url)})${note}`);
34
+ }
35
+ }
36
+ return `${lines.join('\n')}\n`;
37
+ }
38
+ const TITLE_FIELDS = ['title', 'name', 'label', 'heading'];
39
+ const NOTE_FIELDS = ['excerpt', 'description', 'summary', 'subtitle'];
40
+ function pick(resource, candidates) {
41
+ for (const candidate of candidates) {
42
+ if (resource.collection.fields[candidate] === undefined)
43
+ continue;
44
+ const value = resource.entry.values[candidate];
45
+ if (typeof value === 'string' && value.trim().length > 0)
46
+ return condense(value);
47
+ }
48
+ return undefined;
49
+ }
50
+ /**
51
+ * One section per collection, entries newest first.
52
+ *
53
+ * Grouping by collection rather than by URL depth is what makes the file useful:
54
+ * "Articles", "Guides", "Authors" tells a model what kind of thing each link is
55
+ * before it fetches any of them.
56
+ */
57
+ export function llmsTxtSectionsFor(site, resources, options = {}) {
58
+ const limit = options.limitPerSection ?? 100;
59
+ const byCollection = new Map();
60
+ for (const resource of indexableResources(site, resources, options)) {
61
+ const url = canonicalUrl(site, resource);
62
+ if (url === null)
63
+ continue;
64
+ const name = resource.collection.name;
65
+ let section = byCollection.get(name);
66
+ if (section === undefined) {
67
+ section = {
68
+ title: options.sectionTitle?.(name) ?? resource.collection.labels.plural,
69
+ links: [],
70
+ };
71
+ byCollection.set(name, section);
72
+ }
73
+ const note = pick(resource, NOTE_FIELDS);
74
+ section.links.push({
75
+ title: pick(resource, TITLE_FIELDS) ?? url,
76
+ url,
77
+ ...(note === undefined ? {} : { note }),
78
+ });
79
+ }
80
+ return [...byCollection.values()].map((section) => ({
81
+ title: section.title,
82
+ links: section.links.slice(0, limit),
83
+ }));
84
+ }
85
+ //# sourceMappingURL=llms-txt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llms-txt.js","sourceRoot":"","sources":["../src/llms-txt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import type { HreflangAlternate } from './hreflang.js';
2
+ import type { IndexableOptions } from './indexable.js';
3
+ import type { SeoImage, SeoResolvers, SeoResource, SeoSite } from './types.js';
4
+ /**
5
+ * The head of a page: canonical, `hreflang`, Open Graph, Twitter Card, robots.
6
+ *
7
+ * Tags are returned as data and rendered separately. The renderer is one
8
+ * function of eight lines, so returning strings would have been shorter — but a
9
+ * theme needs to *inspect* the tags to merge its own, an Astro integration
10
+ * wants them as component props, and a test wants to assert on a tag rather
11
+ * than on a substring of HTML. A string is the one shape none of the three can
12
+ * use.
13
+ */
14
+ export type MetaTag = {
15
+ readonly kind: 'meta';
16
+ readonly name: string;
17
+ readonly content: string;
18
+ } | {
19
+ readonly kind: 'property';
20
+ readonly property: string;
21
+ readonly content: string;
22
+ } | {
23
+ readonly kind: 'link';
24
+ readonly rel: string;
25
+ readonly href: string;
26
+ readonly hreflang?: string;
27
+ } | {
28
+ readonly kind: 'title';
29
+ readonly text: string;
30
+ };
31
+ export interface MetadataOptions extends IndexableOptions {
32
+ readonly resolvers?: SeoResolvers;
33
+ /** The family's alternates, from `buildHreflangMap`. */
34
+ readonly alternates?: readonly HreflangAlternate[];
35
+ /** Overrides the derived title. */
36
+ readonly title?: string;
37
+ readonly description?: string;
38
+ /** Overrides the derived social image. */
39
+ readonly image?: SeoImage;
40
+ /**
41
+ * Forces `noindex` even on a published entry — a tag archive page, a search
42
+ * result page, a paginated tail.
43
+ */
44
+ readonly noindex?: boolean;
45
+ readonly types?: Readonly<Record<string, string>>;
46
+ }
47
+ export declare function buildMetaTags(site: SeoSite, resource: SeoResource, options?: MetadataOptions): readonly MetaTag[];
48
+ /**
49
+ * Escaping for an HTML attribute value.
50
+ *
51
+ * Separate from the XML escaper because HTML has no `&apos;` — it is an XML
52
+ * entity that HTML 4 parsers do not know — so the numeric reference is used
53
+ * instead. The two look interchangeable and are not; sharing one function here
54
+ * is how a feed ends up with `&#39;` and a page with `&apos;`, one of which is
55
+ * wrong in each context.
56
+ */
57
+ export declare function escapeHtmlAttribute(value: string): string;
58
+ export declare function escapeHtmlText(value: string): string;
59
+ /** The tags as HTML, one per line, ready to drop into `<head>`. */
60
+ export declare function renderMetaTags(tags: readonly MetaTag[]): string;
61
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAGtD,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAG9E;;;;;;;;;GASG;AAEH,MAAM,MAAM,OAAO,GACf;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC1E;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClF;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAErD,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,CAAA;IACjC,wDAAwD;IACxD,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAA;IAClD,mCAAmC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAClD;AAwDD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,WAAW,EACrB,OAAO,GAAE,eAAoB,GAC5B,SAAS,OAAO,EAAE,CA6GpB;AAUD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAkBD,mEAAmE;AACnE,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAE/D"}
@@ -0,0 +1,200 @@
1
+ import { condense } from '@cogenta/schema';
2
+ import { isPublished } from './indexable.js';
3
+ import { schemaTypeFor } from './json-ld.js';
4
+ import { canonicalUrl } from './url.js';
5
+ const TITLE_FIELDS = ['title', 'name', 'label', 'heading'];
6
+ const DESCRIPTION_FIELDS = ['excerpt', 'description', 'summary', 'subtitle', 'teaser'];
7
+ /** Google truncates a description near 160 characters; longer is wasted bytes. */
8
+ const MAX_DESCRIPTION = 160;
9
+ function pickString(resource, candidates) {
10
+ for (const candidate of candidates) {
11
+ if (resource.collection.fields[candidate] === undefined)
12
+ continue;
13
+ const value = resource.entry.values[candidate];
14
+ if (typeof value === 'string' && value.trim().length > 0)
15
+ return condense(value);
16
+ }
17
+ return undefined;
18
+ }
19
+ function truncate(value, limit) {
20
+ if (value.length <= limit)
21
+ return value;
22
+ const cut = value.slice(0, limit - 1);
23
+ const lastSpace = cut.lastIndexOf(' ');
24
+ return `${(lastSpace > limit / 2 ? cut.slice(0, lastSpace) : cut).trimEnd()}…`;
25
+ }
26
+ function firstImage(resource, resolvers) {
27
+ const resolve = resolvers?.media;
28
+ if (resolve === undefined)
29
+ return undefined;
30
+ for (const [name, field] of Object.entries(resource.collection.fields)) {
31
+ if (field.kind !== 'media')
32
+ continue;
33
+ const value = resource.entry.values[name];
34
+ const id = Array.isArray(value) ? value[0] : value;
35
+ if (typeof id !== 'string' || id.length === 0)
36
+ continue;
37
+ const image = resolve(id);
38
+ if (image !== null)
39
+ return image;
40
+ }
41
+ return undefined;
42
+ }
43
+ /**
44
+ * Open Graph's `og:type` vocabulary is not schema.org's, and it is tiny:
45
+ * `article`, `profile`, `website`, `book`, plus the music and video families.
46
+ * Mapping through it rather than passing the schema.org type keeps Facebook and
47
+ * LinkedIn from silently falling back to `website` on every page.
48
+ */
49
+ function openGraphType(schemaType) {
50
+ if (schemaType.endsWith('Article') || schemaType === 'BlogPosting')
51
+ return 'article';
52
+ if (schemaType === 'Person')
53
+ return 'profile';
54
+ if (schemaType === 'Book')
55
+ return 'book';
56
+ if (schemaType === 'VideoObject')
57
+ return 'video.other';
58
+ return 'website';
59
+ }
60
+ export function buildMetaTags(site, resource, options = {}) {
61
+ const tags = [];
62
+ const url = canonicalUrl(site, resource);
63
+ const schemaType = schemaTypeFor(resource.collection, {
64
+ ...(options.types === undefined ? {} : { types: options.types }),
65
+ });
66
+ const title = options.title ?? pickString(resource, TITLE_FIELDS) ?? site.name;
67
+ const rawDescription = options.description ?? pickString(resource, DESCRIPTION_FIELDS) ?? site.description;
68
+ const description = rawDescription === undefined ? undefined : truncate(rawDescription, MAX_DESCRIPTION);
69
+ const image = options.image ?? firstImage(resource, options.resolvers);
70
+ tags.push({ kind: 'title', text: title });
71
+ // The robots tag comes first among the meta tags because it is the one that
72
+ // matters when it is wrong. An unpublished entry rendered through a preview
73
+ // token must carry `noindex`: preview URLs leak — they get pasted into chats,
74
+ // into tickets, and eventually into a crawler's referrer log.
75
+ const indexable = options.noindex !== true && isPublished(resource.entry, options) && url !== null;
76
+ if (!indexable) {
77
+ tags.push({ kind: 'meta', name: 'robots', content: 'noindex, nofollow' });
78
+ }
79
+ if (description !== undefined) {
80
+ tags.push({ kind: 'meta', name: 'description', content: description });
81
+ }
82
+ if (url !== null && indexable) {
83
+ tags.push({ kind: 'link', rel: 'canonical', href: url });
84
+ }
85
+ for (const alternate of options.alternates ?? []) {
86
+ tags.push({
87
+ kind: 'link',
88
+ rel: 'alternate',
89
+ href: alternate.href,
90
+ hreflang: alternate.hreflang,
91
+ });
92
+ }
93
+ // Open Graph.
94
+ tags.push({ kind: 'property', property: 'og:type', content: openGraphType(schemaType) });
95
+ tags.push({ kind: 'property', property: 'og:title', content: title });
96
+ tags.push({ kind: 'property', property: 'og:site_name', content: site.name });
97
+ tags.push({ kind: 'property', property: 'og:locale', content: resource.entry.locale });
98
+ if (url !== null)
99
+ tags.push({ kind: 'property', property: 'og:url', content: url });
100
+ if (description !== undefined) {
101
+ tags.push({ kind: 'property', property: 'og:description', content: description });
102
+ }
103
+ if (image !== undefined) {
104
+ tags.push({ kind: 'property', property: 'og:image', content: image.url });
105
+ if (image.width !== undefined) {
106
+ tags.push({ kind: 'property', property: 'og:image:width', content: String(image.width) });
107
+ }
108
+ if (image.height !== undefined) {
109
+ tags.push({ kind: 'property', property: 'og:image:height', content: String(image.height) });
110
+ }
111
+ if (image.alt !== undefined) {
112
+ tags.push({ kind: 'property', property: 'og:image:alt', content: image.alt });
113
+ }
114
+ }
115
+ // `og:locale:alternate` takes the *other* languages only — listing the page's
116
+ // own locale there makes Facebook drop the whole set.
117
+ for (const alternate of options.alternates ?? []) {
118
+ if (alternate.hreflang === 'x-default')
119
+ continue;
120
+ if (alternate.hreflang === resource.entry.locale)
121
+ continue;
122
+ tags.push({ kind: 'property', property: 'og:locale:alternate', content: alternate.hreflang });
123
+ }
124
+ if (openGraphType(schemaType) === 'article') {
125
+ if (resource.entry.publishedAt !== null) {
126
+ tags.push({
127
+ kind: 'property',
128
+ property: 'article:published_time',
129
+ content: resource.entry.publishedAt,
130
+ });
131
+ }
132
+ tags.push({
133
+ kind: 'property',
134
+ property: 'article:modified_time',
135
+ content: resource.entry.updatedAt,
136
+ });
137
+ }
138
+ // Twitter Card. `summary_large_image` only when there is an image: the card
139
+ // type is a promise, and an unfulfilled one renders as a broken frame.
140
+ tags.push({
141
+ kind: 'meta',
142
+ name: 'twitter:card',
143
+ content: image === undefined ? 'summary' : 'summary_large_image',
144
+ });
145
+ if (site.twitterSite !== undefined) {
146
+ tags.push({ kind: 'meta', name: 'twitter:site', content: site.twitterSite });
147
+ }
148
+ tags.push({ kind: 'meta', name: 'twitter:title', content: title });
149
+ if (description !== undefined) {
150
+ tags.push({ kind: 'meta', name: 'twitter:description', content: description });
151
+ }
152
+ if (image !== undefined) {
153
+ tags.push({ kind: 'meta', name: 'twitter:image', content: image.url });
154
+ if (image.alt !== undefined) {
155
+ tags.push({ kind: 'meta', name: 'twitter:image:alt', content: image.alt });
156
+ }
157
+ }
158
+ return tags;
159
+ }
160
+ const HTML_ATTRIBUTE_ESCAPES = {
161
+ '&': '&amp;',
162
+ '<': '&lt;',
163
+ '>': '&gt;',
164
+ '"': '&quot;',
165
+ "'": '&#39;',
166
+ };
167
+ /**
168
+ * Escaping for an HTML attribute value.
169
+ *
170
+ * Separate from the XML escaper because HTML has no `&apos;` — it is an XML
171
+ * entity that HTML 4 parsers do not know — so the numeric reference is used
172
+ * instead. The two look interchangeable and are not; sharing one function here
173
+ * is how a feed ends up with `&#39;` and a page with `&apos;`, one of which is
174
+ * wrong in each context.
175
+ */
176
+ export function escapeHtmlAttribute(value) {
177
+ return value.replace(/[&<>"']/gu, (char) => HTML_ATTRIBUTE_ESCAPES[char] ?? char);
178
+ }
179
+ export function escapeHtmlText(value) {
180
+ return value.replace(/[&<>]/gu, (char) => HTML_ATTRIBUTE_ESCAPES[char] ?? char);
181
+ }
182
+ function renderMetaTag(tag) {
183
+ switch (tag.kind) {
184
+ case 'title':
185
+ return `<title>${escapeHtmlText(tag.text)}</title>`;
186
+ case 'meta':
187
+ return `<meta name="${escapeHtmlAttribute(tag.name)}" content="${escapeHtmlAttribute(tag.content)}" />`;
188
+ case 'property':
189
+ return `<meta property="${escapeHtmlAttribute(tag.property)}" content="${escapeHtmlAttribute(tag.content)}" />`;
190
+ case 'link': {
191
+ const hreflang = tag.hreflang === undefined ? '' : ` hreflang="${escapeHtmlAttribute(tag.hreflang)}"`;
192
+ return `<link rel="${escapeHtmlAttribute(tag.rel)}"${hreflang} href="${escapeHtmlAttribute(tag.href)}" />`;
193
+ }
194
+ }
195
+ }
196
+ /** The tags as HTML, one per line, ready to drop into `<head>`. */
197
+ export function renderMetaTags(tags) {
198
+ return tags.map(renderMetaTag).join('\n');
199
+ }
200
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { SeoSite } from './types.js';
2
+ /**
3
+ * `robots.txt`.
4
+ *
5
+ * The format has no escaping whatsoever: a directive ends at the newline, full
6
+ * stop. That makes a value containing a newline an *injection* — a path built
7
+ * from a site setting could append `Disallow: /` and de-index the whole site
8
+ * with no error anywhere. There is nothing to escape it with, so the only
9
+ * correct handling is refusal, which is what `assertSingleLine` does.
10
+ */
11
+ export interface RobotsGroup {
12
+ /** One or more user agents this group applies to. */
13
+ readonly userAgent: string | readonly string[];
14
+ readonly allow?: readonly string[];
15
+ readonly disallow?: readonly string[];
16
+ /** Seconds. Ignored by Google, honoured by Bing and Yandex. */
17
+ readonly crawlDelay?: number;
18
+ }
19
+ export interface RobotsOptions {
20
+ readonly site: SeoSite;
21
+ /** Defaults to a single group allowing everything. */
22
+ readonly groups?: readonly RobotsGroup[];
23
+ /** Site-relative sitemap paths. Emitted as absolute URLs, as the spec requires. */
24
+ readonly sitemaps?: readonly string[];
25
+ /**
26
+ * Blocks every crawler, whatever `groups` says.
27
+ *
28
+ * For staging and preview hosts. It is a separate flag rather than a
29
+ * convention so that "is this environment indexable" is one boolean somebody
30
+ * can find, instead of a `Disallow` buried in a config file.
31
+ */
32
+ readonly allowIndexing?: boolean;
33
+ }
34
+ export declare function renderRobotsTxt(options: RobotsOptions): string;
35
+ //# sourceMappingURL=robots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../src/robots.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAGzC;;;;;;;;GAQG;AAEH,MAAM,WAAW,WAAW;IAC1B,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;IAC9C,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,sDAAsD;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,WAAW,EAAE,CAAA;IACxC,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CACjC;AAYD,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAoC9D"}
package/dist/robots.js ADDED
@@ -0,0 +1,50 @@
1
+ import { CogentaError } from '@cogenta/core';
2
+ import { absoluteUrl } from './url.js';
3
+ function assertSingleLine(value, what) {
4
+ if (!/[\n\r]/u.test(value))
5
+ return value;
6
+ throw new CogentaError({
7
+ code: 'CONFIG_INVALID',
8
+ message: `A robots.txt ${what} may not contain a line break.`,
9
+ hint: 'robots.txt has no escaping: a line break would be read as a new directive. Fix the value at its source.',
10
+ details: { what },
11
+ });
12
+ }
13
+ export function renderRobotsTxt(options) {
14
+ const lines = [];
15
+ if (options.allowIndexing === false) {
16
+ lines.push('User-agent: *', 'Disallow: /');
17
+ }
18
+ else {
19
+ const groups = options.groups ?? [{ userAgent: '*', allow: ['/'] }];
20
+ for (const group of groups) {
21
+ const agents = typeof group.userAgent === 'string' ? [group.userAgent] : group.userAgent;
22
+ for (const agent of agents) {
23
+ lines.push(`User-agent: ${assertSingleLine(agent, 'user agent')}`);
24
+ }
25
+ // Allow before Disallow: both Google and Bing resolve a conflict by the
26
+ // most specific rule, but a human reading the file resolves it by order.
27
+ for (const path of group.allow ?? []) {
28
+ lines.push(`Allow: ${assertSingleLine(path, 'path')}`);
29
+ }
30
+ for (const path of group.disallow ?? []) {
31
+ lines.push(`Disallow: ${assertSingleLine(path, 'path')}`);
32
+ }
33
+ if (group.crawlDelay !== undefined)
34
+ lines.push(`Crawl-delay: ${group.crawlDelay}`);
35
+ lines.push('');
36
+ }
37
+ }
38
+ // Sitemap lines are host-level, not group-level, so they go last and outside
39
+ // any group. Put inside one, they are ignored by half the crawlers.
40
+ const sitemaps = options.sitemaps ?? ['/sitemap.xml'];
41
+ if (sitemaps.length > 0) {
42
+ if (lines.at(-1) !== '')
43
+ lines.push('');
44
+ for (const path of sitemaps) {
45
+ lines.push(`Sitemap: ${assertSingleLine(absoluteUrl(options.site, path), 'sitemap URL')}`);
46
+ }
47
+ }
48
+ return `${lines.join('\n').trimEnd()}\n`;
49
+ }
50
+ //# sourceMappingURL=robots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.js","sourceRoot":"","sources":["../src/robots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAqCtC,SAAS,gBAAgB,CAAC,KAAa,EAAE,IAAY;IACnD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACxC,MAAM,IAAI,YAAY,CAAC;QACrB,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,gBAAgB,IAAI,gCAAgC;QAC7D,IAAI,EAAE,yGAAyG;QAC/G,OAAO,EAAE,EAAE,IAAI,EAAE;KAClB,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAsB;IACpD,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,IAAI,OAAO,CAAC,aAAa,KAAK,KAAK,EAAE,CAAC;QACpC,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,aAAa,CAAC,CAAA;IAC5C,CAAC;SAAM,CAAC;QACN,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACnE,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAA;YACxF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,KAAK,CAAC,IAAI,CAAC,eAAe,gBAAgB,CAAC,KAAK,EAAE,YAAY,CAAC,EAAE,CAAC,CAAA;YACpE,CAAC;YACD,wEAAwE;YACxE,yEAAyE;YACzE,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;gBACrC,KAAK,CAAC,IAAI,CAAC,UAAU,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA;YACxD,CAAC;YACD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;gBACxC,KAAK,CAAC,IAAI,CAAC,aAAa,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA;YAC3D,CAAC;YACD,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS;gBAAE,KAAK,CAAC,IAAI,CAAC,gBAAgB,KAAK,CAAC,UAAU,EAAE,CAAC,CAAA;YAClF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAChB,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,oEAAoE;IACpE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,cAAc,CAAC,CAAA;IACrD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,IAAI,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACvC,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC5B,KAAK,CAAC,IAAI,CAAC,YAAY,gBAAgB,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,aAAa,CAAC,EAAE,CAAC,CAAA;QAC5F,CAAC;IACH,CAAC;IAED,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,IAAI,CAAA;AAC1C,CAAC"}
@@ -0,0 +1,65 @@
1
+ import type { HreflangAlternate } from './hreflang.js';
2
+ import { type IndexableOptions } from './indexable.js';
3
+ import type { SeoResource, SeoSite } from './types.js';
4
+ /**
5
+ * `sitemap.xml`, split and indexed when it has to be.
6
+ *
7
+ * The two limits are the protocol's, not a preference: sitemaps.org caps a
8
+ * single file at **50 000 URLs and 50 MB uncompressed**, and a file that
9
+ * exceeds either is rejected whole. Both are enforced, because a site can hit
10
+ * the byte limit well before the URL limit once `xhtml:link` alternates are
11
+ * included — twelve languages multiply the size of every entry by roughly
12
+ * thirteen while the URL count stays put.
13
+ */
14
+ /** sitemaps.org: 50 000 URLs per file, and the same cap on entries in an index. */
15
+ export declare const SITEMAP_MAX_URLS = 50000;
16
+ /** sitemaps.org: 50 MB uncompressed, counted as 50 × 2²⁰ bytes. */
17
+ export declare const SITEMAP_MAX_BYTES = 52428800;
18
+ export type ChangeFrequency = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
19
+ export interface SitemapUrl {
20
+ readonly loc: string;
21
+ /** W3C datetime. An invalid one makes a crawler ignore the field, not the file. */
22
+ readonly lastmod?: string;
23
+ readonly changefreq?: ChangeFrequency;
24
+ /** 0.0 to 1.0. Rounded to one decimal on output. */
25
+ readonly priority?: number;
26
+ readonly alternates?: readonly HreflangAlternate[];
27
+ }
28
+ /** One file to write, with the site-relative path it must be served at. */
29
+ export interface SitemapFile {
30
+ readonly path: string;
31
+ readonly contents: string;
32
+ /** True for the index that lists the others. Exactly one when there is a split. */
33
+ readonly isIndex: boolean;
34
+ readonly urlCount: number;
35
+ }
36
+ export interface SitemapOptions extends IndexableOptions {
37
+ readonly maxUrls?: number;
38
+ readonly maxBytes?: number;
39
+ /** How many files an index may list. The protocol limit unless a crawler says otherwise. */
40
+ readonly maxIndexEntries?: number;
41
+ /** Path of the single file, and of the index once there is a split. */
42
+ readonly indexPath?: string;
43
+ /** Path of chunk `n`, one-based. */
44
+ readonly chunkPath?: (index: number) => string;
45
+ /** `lastmod` of the index itself. Defaults to the newest child `lastmod`. */
46
+ readonly lastmod?: string;
47
+ }
48
+ /**
49
+ * The files to write for a set of URLs.
50
+ *
51
+ * One file below the limits, an index plus N chunks above them. The caller
52
+ * writes what it gets and never has to know which case it is in — a shape that
53
+ * makes the split untestable in production is how sites discover at 60 000 URLs
54
+ * that nobody ever exercised the second branch.
55
+ */
56
+ export declare function buildSitemap(site: SeoSite, urls: readonly SitemapUrl[], options?: SitemapOptions): readonly SitemapFile[];
57
+ /**
58
+ * Sitemap URLs for a set of resources: published only, alternates attached.
59
+ *
60
+ * `lastmod` is `updatedAt` rather than `publishedAt`: the field answers "has
61
+ * this changed since you last fetched it", which is what makes a crawler come
62
+ * back for a corrected article.
63
+ */
64
+ export declare function sitemapUrlsFor(site: SeoSite, resources: readonly SeoResource[], options?: SitemapOptions): readonly SitemapUrl[];
65
+ //# sourceMappingURL=sitemap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../src/sitemap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAEtD,OAAO,EAAE,KAAK,gBAAgB,EAAsB,MAAM,gBAAgB,CAAA;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAItD;;;;;;;;;GASG;AAEH,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,QAAS,CAAA;AAEtC,mEAAmE;AACnE,eAAO,MAAM,iBAAiB,WAAa,CAAA;AAK3C,MAAM,MAAM,eAAe,GACvB,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,OAAO,CAAA;AAEX,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,mFAAmF;IACnF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,CAAA;IACrC,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAA;CACnD;AAED,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,mFAAmF;IACnF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACtD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,4FAA4F;IAC5F,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;IACjC,uEAAuE;IACvE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,oCAAoC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAC9C,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAmFD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3B,OAAO,GAAE,cAAmB,GAC3B,SAAS,WAAW,EAAE,CAyExB;AAWD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,SAAS,WAAW,EAAE,EACjC,OAAO,GAAE,cAAmB,GAC3B,SAAS,UAAU,EAAE,CAiBvB"}