@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/llms-txt.js
ADDED
|
@@ -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 `'` — 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 `'` and a page with `'`, 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"}
|
package/dist/metadata.js
ADDED
|
@@ -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
|
+
'&': '&',
|
|
162
|
+
'<': '<',
|
|
163
|
+
'>': '>',
|
|
164
|
+
'"': '"',
|
|
165
|
+
"'": ''',
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Escaping for an HTML attribute value.
|
|
169
|
+
*
|
|
170
|
+
* Separate from the XML escaper because HTML has no `'` — 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 `'` and a page with `'`, 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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"}
|
package/dist/robots.d.ts
ADDED
|
@@ -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"}
|