@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,47 @@
1
+ /** IndexNow accepts at most 10 000 URLs in one submission. */
2
+ export declare const INDEXNOW_MAX_URLS = 10000;
3
+ export type IndexNowFetch = (input: string, init: {
4
+ readonly method: string;
5
+ readonly headers: Readonly<Record<string, string>>;
6
+ readonly body: string;
7
+ readonly signal: AbortSignal;
8
+ }) => Promise<{
9
+ readonly ok: boolean;
10
+ readonly status: number;
11
+ }>;
12
+ export interface IndexNowOptions {
13
+ /** The host the URLs belong to: `example.com`. */
14
+ readonly host: string;
15
+ readonly key: string;
16
+ /**
17
+ * Where the key file is served, when it is not `/<key>.txt`.
18
+ *
19
+ * Needed by sites that cannot serve a file at the root — the endpoint fetches
20
+ * it to prove the submitter controls the host.
21
+ */
22
+ readonly keyLocation?: string;
23
+ readonly urls: readonly string[];
24
+ readonly endpoint?: string;
25
+ readonly timeoutMs?: number;
26
+ readonly fetch?: IndexNowFetch;
27
+ }
28
+ export type IndexNowResult = {
29
+ readonly outcome: 'submitted';
30
+ readonly status: number;
31
+ readonly urlCount: number;
32
+ } | {
33
+ readonly outcome: 'skipped';
34
+ readonly reason: 'no-urls' | 'no-fetch';
35
+ } | {
36
+ readonly outcome: 'failed';
37
+ readonly reason: 'http' | 'network' | 'timeout';
38
+ readonly status?: number;
39
+ readonly message: string;
40
+ };
41
+ /** The file the site must serve so the endpoint can verify ownership. */
42
+ export declare function indexNowKeyFile(key: string): {
43
+ readonly path: string;
44
+ readonly contents: string;
45
+ };
46
+ export declare function pingIndexNow(options: IndexNowOptions): Promise<IndexNowResult>;
47
+ //# sourceMappingURL=indexnow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexnow.d.ts","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"AAwBA,8DAA8D;AAC9D,eAAO,MAAM,iBAAiB,QAAS,CAAA;AAKvC,MAAM,MAAM,aAAa,GAAG,CAC1B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;IACJ,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAC7B,KACE,OAAO,CAAC;IAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAE/D,MAAM,WAAW,eAAe;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAA;CAC/B;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACrF;IAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAAA;CAAE,GACxE;IACE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAA;IAC/C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAA;AAEL,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAGjG;AAmDD,wBAAsB,YAAY,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAkDpF"}
@@ -0,0 +1,136 @@
1
+ import { CogentaError } from '@cogenta/core';
2
+ /**
3
+ * IndexNow — telling Bing, Yandex and Seznam that a URL changed.
4
+ *
5
+ * One POST, no authentication beyond a key file the site serves itself. It is
6
+ * the cheapest indexing win there is, and it is also the one piece of this
7
+ * package that talks to the network, which shapes the design:
8
+ *
9
+ * **A failed ping is not an exception.** Publishing an article must not fail
10
+ * because a third-party endpoint is down, rate-limiting, or slow. So a network
11
+ * error, a timeout and an HTTP 4xx all come back as a result object the caller
12
+ * can log and move on from. `CogentaError` is reserved for what is genuinely
13
+ * the caller's fault and would never succeed on a retry: a malformed key, a URL
14
+ * on the wrong host, an oversized batch.
15
+ *
16
+ * `fetch` is injected so the tests exercise every branch — including the
17
+ * timeout — without a socket. Rule R1 in miniature: the feature degrades to a
18
+ * no-op when the network is absent.
19
+ */
20
+ /** IndexNow: a key is 8 to 128 hexadecimal characters. */
21
+ const KEY_PATTERN = /^[a-fA-F0-9]{8,128}$/;
22
+ /** IndexNow accepts at most 10 000 URLs in one submission. */
23
+ export const INDEXNOW_MAX_URLS = 10_000;
24
+ const DEFAULT_ENDPOINT = 'https://api.indexnow.org/indexnow';
25
+ const DEFAULT_TIMEOUT_MS = 10_000;
26
+ /** The file the site must serve so the endpoint can verify ownership. */
27
+ export function indexNowKeyFile(key) {
28
+ assertKey(key);
29
+ return { path: `/${key}.txt`, contents: `${key}\n` };
30
+ }
31
+ function assertKey(key) {
32
+ if (KEY_PATTERN.test(key))
33
+ return;
34
+ throw new CogentaError({
35
+ code: 'CONFIG_INVALID',
36
+ message: 'The IndexNow key is not 8 to 128 hexadecimal characters.',
37
+ hint: 'Generate one with crypto.randomUUID().replaceAll("-", ""), then serve it at /<key>.txt.',
38
+ details: { keyLength: key.length },
39
+ });
40
+ }
41
+ /**
42
+ * Every URL must be on the submitted host — the endpoint rejects the whole
43
+ * batch otherwise, so catching it here turns a silent 422 into an error naming
44
+ * the offending URL.
45
+ */
46
+ function assertUrls(host, urls) {
47
+ if (urls.length > INDEXNOW_MAX_URLS) {
48
+ throw new CogentaError({
49
+ code: 'CONFIG_INVALID',
50
+ message: `IndexNow accepts ${INDEXNOW_MAX_URLS} URLs per submission, not ${urls.length}.`,
51
+ hint: 'Submit in batches. There is no benefit to submitting a whole site at once.',
52
+ details: { urlCount: urls.length, limit: INDEXNOW_MAX_URLS },
53
+ });
54
+ }
55
+ for (const url of urls) {
56
+ let parsed;
57
+ try {
58
+ parsed = new URL(url);
59
+ }
60
+ catch {
61
+ throw new CogentaError({
62
+ code: 'CONFIG_INVALID',
63
+ message: `"${url}" is not an absolute URL.`,
64
+ hint: 'IndexNow takes absolute URLs only, protocol included.',
65
+ details: { url },
66
+ });
67
+ }
68
+ if (parsed.host !== host) {
69
+ throw new CogentaError({
70
+ code: 'CONFIG_INVALID',
71
+ message: `"${url}" is not on the submitted host "${host}".`,
72
+ hint: 'One submission covers one host. Group the URLs by host and ping once per group.',
73
+ details: { url, host, urlHost: parsed.host },
74
+ });
75
+ }
76
+ }
77
+ }
78
+ export async function pingIndexNow(options) {
79
+ assertKey(options.key);
80
+ assertUrls(options.host, options.urls);
81
+ if (options.urls.length === 0)
82
+ return { outcome: 'skipped', reason: 'no-urls' };
83
+ const send = options.fetch ?? globalFetch();
84
+ // A build target without `fetch` is a supported target, not an error: the
85
+ // site simply does not ping (rule R1).
86
+ if (send === null)
87
+ return { outcome: 'skipped', reason: 'no-fetch' };
88
+ const body = JSON.stringify({
89
+ host: options.host,
90
+ key: options.key,
91
+ ...(options.keyLocation === undefined ? {} : { keyLocation: options.keyLocation }),
92
+ urlList: options.urls,
93
+ });
94
+ const controller = new AbortController();
95
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
96
+ try {
97
+ const response = await send(options.endpoint ?? DEFAULT_ENDPOINT, {
98
+ method: 'POST',
99
+ headers: { 'content-type': 'application/json; charset=utf-8' },
100
+ body,
101
+ signal: controller.signal,
102
+ });
103
+ if (!response.ok) {
104
+ return {
105
+ outcome: 'failed',
106
+ reason: 'http',
107
+ status: response.status,
108
+ message: `The IndexNow endpoint answered ${response.status}.`,
109
+ };
110
+ }
111
+ return { outcome: 'submitted', status: response.status, urlCount: options.urls.length };
112
+ }
113
+ catch (cause) {
114
+ const aborted = controller.signal.aborted;
115
+ return {
116
+ outcome: 'failed',
117
+ reason: aborted ? 'timeout' : 'network',
118
+ message: aborted
119
+ ? `The IndexNow endpoint did not answer within ${options.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms.`
120
+ : messageOf(cause),
121
+ };
122
+ }
123
+ finally {
124
+ clearTimeout(timeout);
125
+ }
126
+ }
127
+ function messageOf(cause) {
128
+ return cause instanceof Error ? cause.message : 'The IndexNow request failed.';
129
+ }
130
+ function globalFetch() {
131
+ const candidate = globalThis.fetch;
132
+ if (typeof candidate !== 'function')
133
+ return null;
134
+ return (input, init) => candidate(input, init);
135
+ }
136
+ //# sourceMappingURL=indexnow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexnow.js","sourceRoot":"","sources":["../src/indexnow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ import type { CollectionDefinition } from '@cogenta/schema';
2
+ import type { SeoReference, SeoResolvers, SeoResource, SeoSite } from './types.js';
3
+ /**
4
+ * JSON-LD **derived from the schema**, never typed by hand (L3 § Socle SEO).
5
+ *
6
+ * The spec is explicit: an `article` collection produces a schema.org `Article`
7
+ * with no intervention. That constraint is what makes structured data survive
8
+ * — a hand-written JSON-LD block is a second copy of the content that stops
9
+ * matching the first one within a week, and Google penalises exactly that
10
+ * divergence.
11
+ *
12
+ * So the mapping has two halves, and both are declarative:
13
+ *
14
+ * 1. **Collection → `@type`**, from the collection's own name and, failing
15
+ * that, from the shape of its fields.
16
+ * 2. **Field → property**, from the field's `kind` and name.
17
+ *
18
+ * Anything the mapping cannot justify is left out. Incomplete structured data
19
+ * is ignored by a crawler; wrong structured data earns a manual action.
20
+ */
21
+ /** A JSON-LD value. Closed on purpose: no `any` may enter the graph. */
22
+ export type JsonLdValue = string | number | boolean | null | readonly JsonLdValue[] | {
23
+ readonly [key: string]: JsonLdValue | undefined;
24
+ };
25
+ export type JsonLdObject = {
26
+ readonly [key: string]: JsonLdValue | undefined;
27
+ };
28
+ export interface JsonLdOptions {
29
+ readonly resolvers?: SeoResolvers;
30
+ /**
31
+ * Force the schema.org type of a collection: `{ article: 'BlogPosting' }`.
32
+ *
33
+ * The escape hatch exists because "is a `recipe` a `Recipe` or a
34
+ * `HowTo`" is an editorial decision no amount of field inspection settles.
35
+ */
36
+ readonly types?: Readonly<Record<string, string>>;
37
+ /** Emitted as `publisher`. Usually the site's organisation. */
38
+ readonly publisher?: SeoReference;
39
+ /** Cuts `articleBody`, which is otherwise the whole article in the page head. */
40
+ readonly maxBodyLength?: number;
41
+ }
42
+ /**
43
+ * The schema.org type of a collection.
44
+ *
45
+ * Order matters: an explicit override, then the name, then the shape. The
46
+ * shape rule is deliberately blunt — long text plus a publication date is an
47
+ * article, and everything else is a `WebPage` — because a subtler heuristic
48
+ * would be a heuristic nobody can predict from reading the collection.
49
+ */
50
+ export declare function schemaTypeFor(collection: CollectionDefinition, options?: JsonLdOptions): string;
51
+ /**
52
+ * The JSON-LD object for one entry.
53
+ *
54
+ * `null` when the entry has no URL: structured data without a stable `@id` is
55
+ * an orphan node no crawler can attach to a page.
56
+ */
57
+ export declare function buildJsonLd(site: SeoSite, resource: SeoResource, options?: JsonLdOptions): JsonLdObject | null;
58
+ /**
59
+ * JSON-LD ready to sit inside a `<script type="application/ld+json">`.
60
+ *
61
+ * `<`, `>` and `&` are written as `\u00XX` escapes. This is not decoration: a
62
+ * value containing the literal text `</script>` closes the tag early, and the
63
+ * remainder of the JSON becomes markup in the document. Inside a script
64
+ * element, HTML entity escaping does *not* apply — the only escape the parser
65
+ * respects is the JSON one, so it has to happen here. U+2028 and U+2029 are
66
+ * escaped for the same reason on the JavaScript side.
67
+ */
68
+ export declare function renderJsonLdScript(graph: JsonLdObject | readonly JsonLdObject[]): string;
69
+ //# sourceMappingURL=json-ld.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-ld.d.ts","sourceRoot":"","sources":["../src/json-ld.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAmB,MAAM,iBAAiB,CAAA;AAE5E,OAAO,KAAK,EAAY,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAG5F;;;;;;;;;;;;;;;;;GAiBG;AAEH,wEAAwE;AACxE,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,WAAW,EAAE,GACtB;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAA;CAAE,CAAA;AAEvD,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAA;CAAE,CAAA;AAE9E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,CAAA;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACjD,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,CAAA;IACjC,iFAAiF;IACjF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAChC;AAmDD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE,aAAkB,GAC1B,MAAM,CAiBR;AAqGD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,WAAW,EACrB,OAAO,GAAE,aAAkB,GAC1B,YAAY,GAAG,IAAI,CAuGrB;AA0BD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,YAAY,EAAE,GAAG,MAAM,CAOxF"}
@@ -0,0 +1,319 @@
1
+ import { condense, extractRichText } from '@cogenta/schema';
2
+ import { canonicalUrl } from './url.js';
3
+ /**
4
+ * Collection name → schema.org type.
5
+ *
6
+ * Names are matched after lower-casing and stripping non-letters, so `blogPost`,
7
+ * `blog_post` and `BlogPost` all land on the same row. The list is short and
8
+ * covers what a CMS actually models; everything else falls through to the
9
+ * shape-based rules below, which is the honest answer for a bespoke collection.
10
+ */
11
+ const TYPE_BY_NAME = {
12
+ article: 'Article',
13
+ post: 'BlogPosting',
14
+ blogpost: 'BlogPosting',
15
+ news: 'NewsArticle',
16
+ newsarticle: 'NewsArticle',
17
+ page: 'WebPage',
18
+ author: 'Person',
19
+ person: 'Person',
20
+ product: 'Product',
21
+ event: 'Event',
22
+ recipe: 'Recipe',
23
+ faq: 'FAQPage',
24
+ organization: 'Organization',
25
+ organisation: 'Organization',
26
+ company: 'Organization',
27
+ job: 'JobPosting',
28
+ course: 'Course',
29
+ book: 'Book',
30
+ video: 'VideoObject',
31
+ tag: 'Thing',
32
+ category: 'Thing',
33
+ };
34
+ /** Types whose main text property is `articleBody` and that carry a `headline`. */
35
+ const ARTICLE_TYPES = new Set([
36
+ 'Article',
37
+ 'BlogPosting',
38
+ 'NewsArticle',
39
+ 'TechArticle',
40
+ 'Report',
41
+ ]);
42
+ function normaliseName(name) {
43
+ return name.toLowerCase().replace(/[^a-z]/gu, '');
44
+ }
45
+ function fieldsOf(collection) {
46
+ return Object.entries(collection.fields);
47
+ }
48
+ /**
49
+ * The schema.org type of a collection.
50
+ *
51
+ * Order matters: an explicit override, then the name, then the shape. The
52
+ * shape rule is deliberately blunt — long text plus a publication date is an
53
+ * article, and everything else is a `WebPage` — because a subtler heuristic
54
+ * would be a heuristic nobody can predict from reading the collection.
55
+ */
56
+ export function schemaTypeFor(collection, options = {}) {
57
+ const override = options.types?.[collection.name];
58
+ if (override !== undefined)
59
+ return override;
60
+ const byName = TYPE_BY_NAME[normaliseName(collection.name)];
61
+ if (byName !== undefined)
62
+ return byName;
63
+ const fields = fieldsOf(collection);
64
+ const hasProse = fields.some(([, field]) => field.kind === 'richText');
65
+ const hasDate = fields.some(([name, field]) => (field.kind === 'datetime' || field.kind === 'date') && /publish|date/iu.test(name));
66
+ if (hasProse && hasDate)
67
+ return 'Article';
68
+ if (collection.routing !== undefined)
69
+ return 'WebPage';
70
+ return 'Thing';
71
+ }
72
+ /** Field names that mean "the human-readable label of this thing". */
73
+ const NAME_FIELDS = ['title', 'name', 'label', 'heading'];
74
+ /** Field names that mean "the short summary". */
75
+ const DESCRIPTION_FIELDS = ['excerpt', 'description', 'summary', 'subtitle', 'teaser', 'abstract'];
76
+ function firstStringValue(resource, candidates, kinds) {
77
+ for (const candidate of candidates) {
78
+ const field = resource.collection.fields[candidate];
79
+ if (field === undefined || !kinds.includes(field.kind))
80
+ continue;
81
+ const value = resource.entry.values[candidate];
82
+ if (typeof value === 'string' && value.trim().length > 0)
83
+ return condense(value);
84
+ }
85
+ return undefined;
86
+ }
87
+ function imageValue(image) {
88
+ return {
89
+ '@type': 'ImageObject',
90
+ url: image.url,
91
+ width: image.width,
92
+ height: image.height,
93
+ caption: image.alt,
94
+ };
95
+ }
96
+ function referenceValue(reference) {
97
+ return {
98
+ '@type': reference.type ?? 'Thing',
99
+ name: reference.name,
100
+ url: reference.url,
101
+ };
102
+ }
103
+ function resolveMedia(value, resolvers) {
104
+ const ids = (Array.isArray(value) ? value : [value]).filter((id) => typeof id === 'string' && id.length > 0);
105
+ const resolve = resolvers?.media;
106
+ if (resolve === undefined)
107
+ return [];
108
+ const images = [];
109
+ for (const id of ids) {
110
+ const image = resolve(id);
111
+ if (image !== null)
112
+ images.push(image);
113
+ }
114
+ return images;
115
+ }
116
+ function resolveReferences(value, target, resolvers) {
117
+ const ids = (Array.isArray(value) ? value : [value]).filter((id) => typeof id === 'string' && id.length > 0);
118
+ const resolve = resolvers?.reference;
119
+ if (resolve === undefined)
120
+ return [];
121
+ const references = [];
122
+ for (const id of ids) {
123
+ const reference = resolve(target, id);
124
+ if (reference !== null)
125
+ references.push(reference);
126
+ }
127
+ return references;
128
+ }
129
+ /**
130
+ * The property a relation field maps to.
131
+ *
132
+ * Only the three relations a CMS reliably has are mapped. A relation to
133
+ * `relatedProducts` has no defensible schema.org property, and inventing one
134
+ * produces a graph that says something the site does not mean.
135
+ */
136
+ function relationProperty(fieldName, target) {
137
+ const name = normaliseName(fieldName);
138
+ const to = normaliseName(target);
139
+ if (name === 'author' || name === 'authors' || to === 'author' || to === 'person')
140
+ return 'author';
141
+ if (name === 'tags' || name === 'keywords' || to === 'tag')
142
+ return 'keywords';
143
+ if (name === 'category' || name === 'categories' || to === 'category')
144
+ return 'articleSection';
145
+ return null;
146
+ }
147
+ function dateProperty(fieldName) {
148
+ const name = normaliseName(fieldName);
149
+ if (name === 'publishedat' || name === 'publishdate' || name === 'date')
150
+ return 'datePublished';
151
+ if (name === 'updatedat' || name === 'modifiedat')
152
+ return 'dateModified';
153
+ if (name === 'expiresat' || name === 'expirydate')
154
+ return 'expires';
155
+ if (name === 'startsat' || name === 'startdate')
156
+ return 'startDate';
157
+ if (name === 'endsat' || name === 'enddate')
158
+ return 'endDate';
159
+ return null;
160
+ }
161
+ const DEFAULT_MAX_BODY = 5_000;
162
+ /**
163
+ * The JSON-LD object for one entry.
164
+ *
165
+ * `null` when the entry has no URL: structured data without a stable `@id` is
166
+ * an orphan node no crawler can attach to a page.
167
+ */
168
+ export function buildJsonLd(site, resource, options = {}) {
169
+ const url = canonicalUrl(site, resource);
170
+ if (url === null)
171
+ return null;
172
+ const { collection, entry } = resource;
173
+ const type = schemaTypeFor(collection, options);
174
+ const resolvers = options.resolvers;
175
+ const graph = {
176
+ '@context': 'https://schema.org',
177
+ '@type': type,
178
+ '@id': url,
179
+ url,
180
+ inLanguage: entry.locale,
181
+ };
182
+ const name = firstStringValue(resource, NAME_FIELDS, ['text', 'slug']);
183
+ if (name !== undefined) {
184
+ // `headline` is the article-family property; everything else uses `name`.
185
+ // Google truncates `headline` past 110 characters, so both are emitted for
186
+ // an article: the short one for the rich result, the full one for the graph.
187
+ if (ARTICLE_TYPES.has(type)) {
188
+ graph.headline = name.length > 110 ? `${name.slice(0, 107).trimEnd()}…` : name;
189
+ graph.name = name;
190
+ }
191
+ else {
192
+ graph.name = name;
193
+ }
194
+ }
195
+ const description = firstStringValue(resource, DESCRIPTION_FIELDS, ['text', 'richText']);
196
+ if (description !== undefined)
197
+ graph.description = description;
198
+ const images = [];
199
+ const authors = [];
200
+ const keywords = [];
201
+ const sections = [];
202
+ for (const [fieldName, field] of fieldsOf(collection)) {
203
+ const value = entry.values[fieldName];
204
+ if (value === undefined || value === null)
205
+ continue;
206
+ switch (field.kind) {
207
+ case 'media': {
208
+ images.push(...resolveMedia(value, resolvers));
209
+ break;
210
+ }
211
+ case 'relation': {
212
+ const target = typeof field.options.to === 'string' ? field.options.to : fieldName;
213
+ const property = relationProperty(fieldName, target);
214
+ if (property === null)
215
+ break;
216
+ const references = resolveReferences(value, target, resolvers);
217
+ if (property === 'author')
218
+ authors.push(...references);
219
+ if (property === 'keywords')
220
+ keywords.push(...references.map((one) => one.name));
221
+ if (property === 'articleSection')
222
+ sections.push(...references.map((one) => one.name));
223
+ break;
224
+ }
225
+ case 'date':
226
+ case 'datetime': {
227
+ const property = dateProperty(fieldName);
228
+ if (property !== null && typeof value === 'string')
229
+ graph[property] = value;
230
+ break;
231
+ }
232
+ case 'richText': {
233
+ if (!ARTICLE_TYPES.has(type))
234
+ break;
235
+ const body = extractRichText(value);
236
+ if (body.length === 0)
237
+ break;
238
+ const limit = options.maxBodyLength ?? DEFAULT_MAX_BODY;
239
+ graph.articleBody = body.length > limit ? body.slice(0, limit).trimEnd() : body;
240
+ break;
241
+ }
242
+ case 'geo': {
243
+ if (!isGeoPoint(value))
244
+ break;
245
+ graph.geo = { '@type': 'GeoCoordinates', latitude: value.lat, longitude: value.lng };
246
+ break;
247
+ }
248
+ default:
249
+ break;
250
+ }
251
+ }
252
+ if (images.length > 0) {
253
+ graph.image = images.length === 1 ? imageValue(images[0]) : images.map(imageValue);
254
+ }
255
+ if (authors.length > 0) {
256
+ graph.author =
257
+ authors.length === 1
258
+ ? referenceValue({ type: 'Person', ...authors[0] })
259
+ : authors.map((one) => referenceValue({ type: 'Person', ...one }));
260
+ }
261
+ if (keywords.length > 0)
262
+ graph.keywords = keywords.join(', ');
263
+ if (sections.length > 0)
264
+ graph.articleSection = sections[0];
265
+ // The system fields are authoritative for dates: a collection may have no
266
+ // `publishedAt` of its own, and every entry has these two (contract A).
267
+ if (graph.datePublished === undefined && entry.publishedAt !== null) {
268
+ graph.datePublished = entry.publishedAt;
269
+ }
270
+ if (graph.dateModified === undefined)
271
+ graph.dateModified = entry.updatedAt;
272
+ if (options.publisher !== undefined) {
273
+ graph.publisher = referenceValue({ type: 'Organization', ...options.publisher });
274
+ }
275
+ return stripUndefined(graph);
276
+ }
277
+ function isGeoPoint(value) {
278
+ if (typeof value !== 'object' || value === null)
279
+ return false;
280
+ const point = value;
281
+ return typeof point.lat === 'number' && typeof point.lng === 'number';
282
+ }
283
+ /** `undefined` is not JSON. Dropping keys here keeps every call site free of guards. */
284
+ function stripUndefined(value) {
285
+ const cleaned = {};
286
+ for (const [key, entry] of Object.entries(value)) {
287
+ if (entry === undefined)
288
+ continue;
289
+ cleaned[key] = typeof entry === 'object' && entry !== null ? stripDeep(entry) : entry;
290
+ }
291
+ return cleaned;
292
+ }
293
+ function stripDeep(value) {
294
+ if (Array.isArray(value))
295
+ return value.map(stripDeep);
296
+ if (typeof value === 'object' && value !== null) {
297
+ return stripUndefined(value);
298
+ }
299
+ return value;
300
+ }
301
+ /**
302
+ * JSON-LD ready to sit inside a `<script type="application/ld+json">`.
303
+ *
304
+ * `<`, `>` and `&` are written as `\u00XX` escapes. This is not decoration: a
305
+ * value containing the literal text `</script>` closes the tag early, and the
306
+ * remainder of the JSON becomes markup in the document. Inside a script
307
+ * element, HTML entity escaping does *not* apply — the only escape the parser
308
+ * respects is the JSON one, so it has to happen here. U+2028 and U+2029 are
309
+ * escaped for the same reason on the JavaScript side.
310
+ */
311
+ export function renderJsonLdScript(graph) {
312
+ return JSON.stringify(graph)
313
+ .replace(/</gu, '\\u003c')
314
+ .replace(/>/gu, '\\u003e')
315
+ .replace(/&/gu, '\\u0026')
316
+ .replace(/\u2028/gu, '\\u2028')
317
+ .replace(/\u2029/gu, '\\u2029');
318
+ }
319
+ //# sourceMappingURL=json-ld.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-ld.js","sourceRoot":"","sources":["../src/json-ld.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { type IndexableOptions } from './indexable.js';
2
+ import type { SeoResource, SeoSite } from './types.js';
3
+ /**
4
+ * `llms.txt` — the site, in Markdown, for a model that has one request to spend.
5
+ *
6
+ * The format (llmstxt.org) is a single `H1`, an optional `>` blockquote
7
+ * summary, then `H2` sections of links with a short note each. It is Markdown
8
+ * rather than XML because the consumer is a language model, and a sitemap is
9
+ * exactly the wrong shape for one: 50 000 bare URLs with no titles say nothing
10
+ * about what the site is.
11
+ *
12
+ * The same publication gate applies as everywhere else. A draft listed here is
13
+ * worse than a draft in a sitemap: a model does not queue the URL for later, it
14
+ * repeats the title in an answer immediately.
15
+ */
16
+ export interface LlmsTxtLink {
17
+ readonly title: string;
18
+ readonly url: string;
19
+ readonly note?: string;
20
+ }
21
+ export interface LlmsTxtSection {
22
+ readonly title: string;
23
+ readonly links: readonly LlmsTxtLink[];
24
+ }
25
+ export interface LlmsTxtOptions {
26
+ readonly site: SeoSite;
27
+ /** Defaults to the site name. */
28
+ readonly title?: string;
29
+ /** The blockquote line. Defaults to the site description. */
30
+ readonly summary?: string;
31
+ /** Free paragraphs between the summary and the first section. */
32
+ readonly details?: readonly string[];
33
+ readonly sections: readonly LlmsTxtSection[];
34
+ }
35
+ export declare function renderLlmsTxt(options: LlmsTxtOptions): string;
36
+ export interface LlmsTxtSectionsOptions extends IndexableOptions {
37
+ /** Section heading for a collection. Defaults to its plural label. */
38
+ readonly sectionTitle?: (collectionName: string) => string;
39
+ /** Entries listed per collection. Defaults to 100. */
40
+ readonly limitPerSection?: number;
41
+ }
42
+ /**
43
+ * One section per collection, entries newest first.
44
+ *
45
+ * Grouping by collection rather than by URL depth is what makes the file useful:
46
+ * "Articles", "Guides", "Authors" tells a model what kind of thing each link is
47
+ * before it fetches any of them.
48
+ */
49
+ export declare function llmsTxtSectionsFor(site: SeoSite, resources: readonly SeoResource[], options?: LlmsTxtSectionsOptions): readonly LlmsTxtSection[];
50
+ //# sourceMappingURL=llms-txt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llms-txt.d.ts","sourceRoot":"","sources":["../src/llms-txt.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,gBAAgB,EAAsB,MAAM,gBAAgB,CAAA;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAGtD;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;CACvC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,iCAAiC;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,iEAAiE;IACjE,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAA;CAC7C;AAkBD,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAsB7D;AAcD,MAAM,WAAW,sBAAuB,SAAQ,gBAAgB;IAC9D,sEAAsE;IACtE,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,MAAM,CAAA;IAC1D,sDAAsD;IACtD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAClC;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,SAAS,WAAW,EAAE,EACjC,OAAO,GAAE,sBAA2B,GACnC,SAAS,cAAc,EAAE,CA8B3B"}