@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/feeds.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type IndexableOptions } from './indexable.js';
|
|
2
|
+
import type { SeoResource, SeoSite } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* RSS 2.0 and Atom 1.0, from the same item list.
|
|
5
|
+
*
|
|
6
|
+
* Both are produced because both are still consumed: readers and podcast
|
|
7
|
+
* clients speak RSS, while Atom is the one with a specified date format and a
|
|
8
|
+
* real notion of entry identity. Generating one from the other at runtime would
|
|
9
|
+
* be cheaper and is exactly how feeds acquire subtly different content.
|
|
10
|
+
*
|
|
11
|
+
* Content is escaped, never wrapped in `CDATA`. `CDATA` looks like it removes
|
|
12
|
+
* the escaping problem and instead moves it: the sequence `]]>` inside an
|
|
13
|
+
* article — which appears in any post about XML — terminates the section early
|
|
14
|
+
* and corrupts the feed, with no visible symptom until a reader silently stops
|
|
15
|
+
* updating.
|
|
16
|
+
*/
|
|
17
|
+
export interface FeedItem {
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly link: string;
|
|
20
|
+
/** Globally unique and permanent. The canonical URL, unless the entry moves. */
|
|
21
|
+
readonly id: string;
|
|
22
|
+
/** RFC 3339. Converted to RFC 822 for RSS. */
|
|
23
|
+
readonly updated: string;
|
|
24
|
+
readonly published?: string;
|
|
25
|
+
readonly summary?: string;
|
|
26
|
+
readonly authorName?: string;
|
|
27
|
+
readonly categories?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
export interface FeedInput {
|
|
30
|
+
readonly site: SeoSite;
|
|
31
|
+
readonly title?: string;
|
|
32
|
+
readonly description?: string;
|
|
33
|
+
/** Site-relative path this feed is served at, for the `self` link. */
|
|
34
|
+
readonly selfPath: string;
|
|
35
|
+
readonly language?: string;
|
|
36
|
+
readonly items: readonly FeedItem[];
|
|
37
|
+
/** Injected so a feed is byte-stable in tests. */
|
|
38
|
+
readonly updated?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* RFC 822 date, as RSS 2.0 requires.
|
|
42
|
+
*
|
|
43
|
+
* Hand-rolled rather than taken from `toUTCString`: that method emits `GMT`,
|
|
44
|
+
* and while RFC 822 allows the name, several validators and at least one large
|
|
45
|
+
* reader expect the `+0000` numeric offset. Building it also keeps the day and
|
|
46
|
+
* month names in English regardless of the server locale, which `toLocaleString`
|
|
47
|
+
* would not.
|
|
48
|
+
*/
|
|
49
|
+
export declare function toRfc822(value: string): string;
|
|
50
|
+
export declare function renderRssFeed(input: FeedInput): string;
|
|
51
|
+
export declare function renderAtomFeed(input: FeedInput): string;
|
|
52
|
+
export interface FeedItemsOptions extends IndexableOptions {
|
|
53
|
+
/** Feeds are a recency list, not an archive. Defaults to 50. */
|
|
54
|
+
readonly limit?: number;
|
|
55
|
+
readonly summaryLength?: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Feed items for a set of resources, newest first.
|
|
59
|
+
*
|
|
60
|
+
* Drafts are removed by `indexableResources` before anything else happens. A
|
|
61
|
+
* feed is the one output that cannot be retracted: readers cache, and a digest
|
|
62
|
+
* mail forwards. So the filter is not an option here and there is no parameter
|
|
63
|
+
* to turn it off.
|
|
64
|
+
*/
|
|
65
|
+
export declare function feedItemsFor(site: SeoSite, resources: readonly SeoResource[], options?: FeedItemsOptions): readonly FeedItem[];
|
|
66
|
+
//# sourceMappingURL=feeds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,gBAAgB,EAAsB,MAAM,gBAAgB,CAAA;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAItD;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACxC;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAA;IACnC,kDAAkD;IAClD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS9C;AAUD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAmDtD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAiDvD;AAED,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;IACxD,gEAAgE;IAChE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAChC;AA2BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,SAAS,WAAW,EAAE,EACjC,OAAO,GAAE,gBAAqB,GAC7B,SAAS,QAAQ,EAAE,CAuBrB"}
|
package/dist/feeds.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { condense, extractRichText } from '@cogenta/schema';
|
|
2
|
+
import { indexableResources } from './indexable.js';
|
|
3
|
+
import { absoluteUrl, canonicalUrl } from './url.js';
|
|
4
|
+
import { renderXmlDocument } from './xml.js';
|
|
5
|
+
const RSS_DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
6
|
+
const RSS_MONTHS = [
|
|
7
|
+
'Jan',
|
|
8
|
+
'Feb',
|
|
9
|
+
'Mar',
|
|
10
|
+
'Apr',
|
|
11
|
+
'May',
|
|
12
|
+
'Jun',
|
|
13
|
+
'Jul',
|
|
14
|
+
'Aug',
|
|
15
|
+
'Sep',
|
|
16
|
+
'Oct',
|
|
17
|
+
'Nov',
|
|
18
|
+
'Dec',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* RFC 822 date, as RSS 2.0 requires.
|
|
22
|
+
*
|
|
23
|
+
* Hand-rolled rather than taken from `toUTCString`: that method emits `GMT`,
|
|
24
|
+
* and while RFC 822 allows the name, several validators and at least one large
|
|
25
|
+
* reader expect the `+0000` numeric offset. Building it also keeps the day and
|
|
26
|
+
* month names in English regardless of the server locale, which `toLocaleString`
|
|
27
|
+
* would not.
|
|
28
|
+
*/
|
|
29
|
+
export function toRfc822(value) {
|
|
30
|
+
const date = new Date(value);
|
|
31
|
+
if (Number.isNaN(date.getTime()))
|
|
32
|
+
return value;
|
|
33
|
+
const day = RSS_DAYS[date.getUTCDay()] ?? 'Sun';
|
|
34
|
+
const month = RSS_MONTHS[date.getUTCMonth()] ?? 'Jan';
|
|
35
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
36
|
+
return `${day}, ${pad(date.getUTCDate())} ${month} ${date.getUTCFullYear()} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} +0000`;
|
|
37
|
+
}
|
|
38
|
+
function newestUpdate(items, fallback) {
|
|
39
|
+
let newest = fallback;
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
if (newest === undefined || item.updated > newest)
|
|
42
|
+
newest = item.updated;
|
|
43
|
+
}
|
|
44
|
+
return newest ?? new Date(0).toISOString();
|
|
45
|
+
}
|
|
46
|
+
export function renderRssFeed(input) {
|
|
47
|
+
const { site, items } = input;
|
|
48
|
+
const selfUrl = absoluteUrl(site, input.selfPath);
|
|
49
|
+
const channel = [
|
|
50
|
+
{ name: 'title', text: input.title ?? site.name },
|
|
51
|
+
{ name: 'link', text: absoluteUrl(site, '/') },
|
|
52
|
+
{ name: 'description', text: input.description ?? site.description ?? site.name },
|
|
53
|
+
{ name: 'language', text: input.language ?? site.defaultLocale },
|
|
54
|
+
{ name: 'lastBuildDate', text: toRfc822(newestUpdate(items, input.updated)) },
|
|
55
|
+
// The self link is an Atom element inside RSS. It is not decoration: it is
|
|
56
|
+
// how a reader that has been redirected knows the feed's real address, and
|
|
57
|
+
// how the feed validators identify duplicates.
|
|
58
|
+
{
|
|
59
|
+
name: 'atom:link',
|
|
60
|
+
attributes: { href: selfUrl, rel: 'self', type: 'application/rss+xml' },
|
|
61
|
+
},
|
|
62
|
+
{ name: 'generator', text: 'Cogenta' },
|
|
63
|
+
];
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const children = [
|
|
66
|
+
{ name: 'title', text: item.title },
|
|
67
|
+
{ name: 'link', text: item.link },
|
|
68
|
+
// `isPermaLink="true"` would tell a reader the guid is a fetchable URL.
|
|
69
|
+
// It usually is, but a moved entry keeps its id and stops being one, so
|
|
70
|
+
// the safe declaration is the one that stays true.
|
|
71
|
+
{ name: 'guid', attributes: { isPermaLink: 'false' }, text: item.id },
|
|
72
|
+
{ name: 'pubDate', text: toRfc822(item.published ?? item.updated) },
|
|
73
|
+
];
|
|
74
|
+
if (item.summary !== undefined)
|
|
75
|
+
children.push({ name: 'description', text: item.summary });
|
|
76
|
+
if (item.authorName !== undefined) {
|
|
77
|
+
// RSS `author` is specified as an email address; a bare name there fails
|
|
78
|
+
// validation. `dc:creator` is the element every reader actually displays.
|
|
79
|
+
children.push({ name: 'dc:creator', text: item.authorName });
|
|
80
|
+
}
|
|
81
|
+
for (const category of item.categories ?? []) {
|
|
82
|
+
children.push({ name: 'category', text: category });
|
|
83
|
+
}
|
|
84
|
+
channel.push({ name: 'item', children });
|
|
85
|
+
}
|
|
86
|
+
return renderXmlDocument({
|
|
87
|
+
name: 'rss',
|
|
88
|
+
attributes: {
|
|
89
|
+
version: '2.0',
|
|
90
|
+
'xmlns:atom': 'http://www.w3.org/2005/Atom',
|
|
91
|
+
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
|
|
92
|
+
},
|
|
93
|
+
children: [{ name: 'channel', children: channel }],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function renderAtomFeed(input) {
|
|
97
|
+
const { site, items } = input;
|
|
98
|
+
const selfUrl = absoluteUrl(site, input.selfPath);
|
|
99
|
+
const children = [
|
|
100
|
+
{ name: 'title', text: input.title ?? site.name },
|
|
101
|
+
// The feed's own id must be a permanent IRI, and must not change when the
|
|
102
|
+
// feed moves — hence the site root rather than the feed URL.
|
|
103
|
+
{ name: 'id', text: absoluteUrl(site, '/') },
|
|
104
|
+
{ name: 'updated', text: newestUpdate(items, input.updated) },
|
|
105
|
+
{ name: 'link', attributes: { rel: 'self', type: 'application/atom+xml', href: selfUrl } },
|
|
106
|
+
{
|
|
107
|
+
name: 'link',
|
|
108
|
+
attributes: { rel: 'alternate', type: 'text/html', href: absoluteUrl(site, '/') },
|
|
109
|
+
},
|
|
110
|
+
{ name: 'generator', text: 'Cogenta' },
|
|
111
|
+
];
|
|
112
|
+
if (input.description !== undefined || site.description !== undefined) {
|
|
113
|
+
children.push({ name: 'subtitle', text: input.description ?? site.description ?? '' });
|
|
114
|
+
}
|
|
115
|
+
for (const item of items) {
|
|
116
|
+
const entry = [
|
|
117
|
+
{ name: 'title', text: item.title },
|
|
118
|
+
{ name: 'id', text: item.id },
|
|
119
|
+
{ name: 'updated', text: item.updated },
|
|
120
|
+
{ name: 'link', attributes: { rel: 'alternate', type: 'text/html', href: item.link } },
|
|
121
|
+
];
|
|
122
|
+
if (item.published !== undefined)
|
|
123
|
+
entry.push({ name: 'published', text: item.published });
|
|
124
|
+
if (item.summary !== undefined) {
|
|
125
|
+
entry.push({ name: 'summary', attributes: { type: 'text' }, text: item.summary });
|
|
126
|
+
}
|
|
127
|
+
if (item.authorName !== undefined) {
|
|
128
|
+
entry.push({ name: 'author', children: [{ name: 'name', text: item.authorName }] });
|
|
129
|
+
}
|
|
130
|
+
for (const category of item.categories ?? []) {
|
|
131
|
+
entry.push({ name: 'category', attributes: { term: category } });
|
|
132
|
+
}
|
|
133
|
+
children.push({ name: 'entry', children: entry });
|
|
134
|
+
}
|
|
135
|
+
return renderXmlDocument({
|
|
136
|
+
name: 'feed',
|
|
137
|
+
attributes: {
|
|
138
|
+
xmlns: 'http://www.w3.org/2005/Atom',
|
|
139
|
+
'xml:lang': input.language ?? site.defaultLocale,
|
|
140
|
+
},
|
|
141
|
+
children,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const DEFAULT_FEED_LIMIT = 50;
|
|
145
|
+
const DEFAULT_SUMMARY_LENGTH = 400;
|
|
146
|
+
const SUMMARY_FIELDS = ['excerpt', 'description', 'summary', 'subtitle', 'teaser'];
|
|
147
|
+
const TITLE_FIELDS = ['title', 'name', 'label', 'heading'];
|
|
148
|
+
function pick(resource, candidates) {
|
|
149
|
+
for (const candidate of candidates) {
|
|
150
|
+
if (resource.collection.fields[candidate] === undefined)
|
|
151
|
+
continue;
|
|
152
|
+
const value = resource.entry.values[candidate];
|
|
153
|
+
if (typeof value === 'string' && value.trim().length > 0)
|
|
154
|
+
return condense(value);
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
function bodySummary(resource, limit) {
|
|
159
|
+
for (const [name, field] of Object.entries(resource.collection.fields)) {
|
|
160
|
+
if (field.kind !== 'richText')
|
|
161
|
+
continue;
|
|
162
|
+
const text = extractRichText(resource.entry.values[name]);
|
|
163
|
+
if (text.length === 0)
|
|
164
|
+
continue;
|
|
165
|
+
return text.length > limit ? `${text.slice(0, limit).trimEnd()}…` : text;
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Feed items for a set of resources, newest first.
|
|
171
|
+
*
|
|
172
|
+
* Drafts are removed by `indexableResources` before anything else happens. A
|
|
173
|
+
* feed is the one output that cannot be retracted: readers cache, and a digest
|
|
174
|
+
* mail forwards. So the filter is not an option here and there is no parameter
|
|
175
|
+
* to turn it off.
|
|
176
|
+
*/
|
|
177
|
+
export function feedItemsFor(site, resources, options = {}) {
|
|
178
|
+
const summaryLength = options.summaryLength ?? DEFAULT_SUMMARY_LENGTH;
|
|
179
|
+
const published = indexableResources(site, resources, options);
|
|
180
|
+
const items = [];
|
|
181
|
+
for (const resource of published) {
|
|
182
|
+
const link = canonicalUrl(site, resource);
|
|
183
|
+
if (link === null)
|
|
184
|
+
continue;
|
|
185
|
+
const summary = pick(resource, SUMMARY_FIELDS) ?? bodySummary(resource, summaryLength);
|
|
186
|
+
items.push({
|
|
187
|
+
title: pick(resource, TITLE_FIELDS) ?? site.name,
|
|
188
|
+
link,
|
|
189
|
+
id: link,
|
|
190
|
+
updated: resource.entry.updatedAt,
|
|
191
|
+
...(resource.entry.publishedAt === null ? {} : { published: resource.entry.publishedAt }),
|
|
192
|
+
...(summary === undefined ? {} : { summary }),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return items
|
|
196
|
+
.sort((a, b) => (b.published ?? b.updated).localeCompare(a.published ?? a.updated))
|
|
197
|
+
.slice(0, options.limit ?? DEFAULT_FEED_LIMIT);
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=feeds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feeds.js","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type IndexableOptions } from './indexable.js';
|
|
2
|
+
import type { SeoResource, SeoSite } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* `hreflang`, derived from the translation family (ADR-0014).
|
|
5
|
+
*
|
|
6
|
+
* The model is one entry per language, translations pointing at their source
|
|
7
|
+
* through `translationOf`. The set of entries sharing a source is the family,
|
|
8
|
+
* and the family *is* the annotation: contract A says so explicitly, so nothing
|
|
9
|
+
* here reads a per-field translation or a locale suffix.
|
|
10
|
+
*
|
|
11
|
+
* ## Why reciprocity is a shape, not a check
|
|
12
|
+
*
|
|
13
|
+
* Google discards a whole `hreflang` cluster the moment one page fails to point
|
|
14
|
+
* back. The usual way to get this wrong is to compute alternates per page —
|
|
15
|
+
* "what are the other languages of *this* entry" — because then each page owns
|
|
16
|
+
* its own answer and the answers drift the instant one of them is filtered
|
|
17
|
+
* differently.
|
|
18
|
+
*
|
|
19
|
+
* So the alternate list is computed **once per family** and handed unchanged to
|
|
20
|
+
* every member, self included. Reciprocity then cannot fail: A and B are not
|
|
21
|
+
* two lists that happen to agree, they are the same list. The test suite still
|
|
22
|
+
* asserts it, because the property is the whole point of the module.
|
|
23
|
+
*/
|
|
24
|
+
export interface HreflangAlternate {
|
|
25
|
+
/** A BCP 47 tag, or `x-default`. */
|
|
26
|
+
readonly hreflang: string;
|
|
27
|
+
readonly href: string;
|
|
28
|
+
}
|
|
29
|
+
export interface TranslationFamily {
|
|
30
|
+
/** The id of the source entry, which is the family's identity. */
|
|
31
|
+
readonly sourceId: string;
|
|
32
|
+
/** Indexable members only, sorted by locale so output is stable. */
|
|
33
|
+
readonly members: readonly SeoResource[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Groups resources into translation families, dropping everything unpublished
|
|
37
|
+
* first.
|
|
38
|
+
*
|
|
39
|
+
* The filter runs **before** grouping on purpose. A draft German translation
|
|
40
|
+
* must not appear as an alternate of the published French one: the tag would
|
|
41
|
+
* advertise a URL that 404s, and a crawler that follows it distrusts the rest
|
|
42
|
+
* of the cluster.
|
|
43
|
+
*/
|
|
44
|
+
export declare function groupTranslationFamilies(site: SeoSite, resources: readonly SeoResource[], options?: IndexableOptions): readonly TranslationFamily[];
|
|
45
|
+
/**
|
|
46
|
+
* The alternate set of a family: one entry per language, plus `x-default`.
|
|
47
|
+
*
|
|
48
|
+
* `x-default` names the page a crawler should serve to a user whose language
|
|
49
|
+
* the site does not cover, and the only defensible answer is the **source**
|
|
50
|
+
* entry — the one with no `translationOf`. When the source is not published,
|
|
51
|
+
* no `x-default` is emitted at all: picking a translation instead would be a
|
|
52
|
+
* guess, and a wrong `x-default` sends every unmatched visitor to a language
|
|
53
|
+
* chosen at random by insertion order.
|
|
54
|
+
*/
|
|
55
|
+
export declare function alternatesFor(site: SeoSite, family: TranslationFamily): readonly HreflangAlternate[];
|
|
56
|
+
/**
|
|
57
|
+
* Alternates keyed by entry id, for a whole page set.
|
|
58
|
+
*
|
|
59
|
+
* Every member of a family maps to the *same array instance*. That is what
|
|
60
|
+
* makes reciprocity structural rather than emergent.
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildHreflangMap(site: SeoSite, resources: readonly SeoResource[], options?: IndexableOptions): ReadonlyMap<string, readonly HreflangAlternate[]>;
|
|
63
|
+
//# sourceMappingURL=hreflang.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hreflang.d.ts","sourceRoot":"","sources":["../src/hreflang.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAe,MAAM,gBAAgB,CAAA;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAGtD;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAA;CACzC;AAOD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,SAAS,WAAW,EAAE,EACjC,OAAO,GAAE,gBAAqB,GAC7B,SAAS,iBAAiB,EAAE,CAiB9B;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,iBAAiB,GACxB,SAAS,iBAAiB,EAAE,CAqB9B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,SAAS,WAAW,EAAE,EACjC,OAAO,GAAE,gBAAqB,GAC7B,WAAW,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC,CAanD"}
|
package/dist/hreflang.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { isIndexable } from './indexable.js';
|
|
2
|
+
import { canonicalUrl } from './url.js';
|
|
3
|
+
/** A translation points at its source; a source points at nothing (ADR-0014). */
|
|
4
|
+
function familyIdOf(resource) {
|
|
5
|
+
return resource.entry.translationOf ?? resource.entry.id;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Groups resources into translation families, dropping everything unpublished
|
|
9
|
+
* first.
|
|
10
|
+
*
|
|
11
|
+
* The filter runs **before** grouping on purpose. A draft German translation
|
|
12
|
+
* must not appear as an alternate of the published French one: the tag would
|
|
13
|
+
* advertise a URL that 404s, and a crawler that follows it distrusts the rest
|
|
14
|
+
* of the cluster.
|
|
15
|
+
*/
|
|
16
|
+
export function groupTranslationFamilies(site, resources, options = {}) {
|
|
17
|
+
const families = new Map();
|
|
18
|
+
for (const resource of resources) {
|
|
19
|
+
if (!isIndexable(site, resource, options))
|
|
20
|
+
continue;
|
|
21
|
+
const id = familyIdOf(resource);
|
|
22
|
+
const existing = families.get(id);
|
|
23
|
+
if (existing === undefined)
|
|
24
|
+
families.set(id, [resource]);
|
|
25
|
+
else
|
|
26
|
+
existing.push(resource);
|
|
27
|
+
}
|
|
28
|
+
return [...families.entries()]
|
|
29
|
+
.map(([sourceId, members]) => ({
|
|
30
|
+
sourceId,
|
|
31
|
+
members: [...members].sort((a, b) => a.entry.locale.localeCompare(b.entry.locale)),
|
|
32
|
+
}))
|
|
33
|
+
.sort((a, b) => a.sourceId.localeCompare(b.sourceId));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The alternate set of a family: one entry per language, plus `x-default`.
|
|
37
|
+
*
|
|
38
|
+
* `x-default` names the page a crawler should serve to a user whose language
|
|
39
|
+
* the site does not cover, and the only defensible answer is the **source**
|
|
40
|
+
* entry — the one with no `translationOf`. When the source is not published,
|
|
41
|
+
* no `x-default` is emitted at all: picking a translation instead would be a
|
|
42
|
+
* guess, and a wrong `x-default` sends every unmatched visitor to a language
|
|
43
|
+
* chosen at random by insertion order.
|
|
44
|
+
*/
|
|
45
|
+
export function alternatesFor(site, family) {
|
|
46
|
+
const alternates = [];
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
for (const member of family.members) {
|
|
49
|
+
const href = canonicalUrl(site, member);
|
|
50
|
+
// `isIndexable` already established the URL exists; the null branch is a
|
|
51
|
+
// type narrowing, not a case.
|
|
52
|
+
if (href === null)
|
|
53
|
+
continue;
|
|
54
|
+
if (seen.has(member.entry.locale))
|
|
55
|
+
continue;
|
|
56
|
+
seen.add(member.entry.locale);
|
|
57
|
+
alternates.push({ hreflang: member.entry.locale, href });
|
|
58
|
+
}
|
|
59
|
+
const source = family.members.find((member) => member.entry.translationOf === null);
|
|
60
|
+
if (source !== undefined) {
|
|
61
|
+
const href = canonicalUrl(site, source);
|
|
62
|
+
if (href !== null)
|
|
63
|
+
alternates.push({ hreflang: 'x-default', href });
|
|
64
|
+
}
|
|
65
|
+
return alternates;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Alternates keyed by entry id, for a whole page set.
|
|
69
|
+
*
|
|
70
|
+
* Every member of a family maps to the *same array instance*. That is what
|
|
71
|
+
* makes reciprocity structural rather than emergent.
|
|
72
|
+
*/
|
|
73
|
+
export function buildHreflangMap(site, resources, options = {}) {
|
|
74
|
+
const map = new Map();
|
|
75
|
+
for (const family of groupTranslationFamilies(site, resources, options)) {
|
|
76
|
+
// A family of one gets no alternates: a self-referencing `hreflang` on a
|
|
77
|
+
// page with no translation is noise, and Google says as much.
|
|
78
|
+
if (family.members.length < 2)
|
|
79
|
+
continue;
|
|
80
|
+
const alternates = alternatesFor(site, family);
|
|
81
|
+
for (const member of family.members)
|
|
82
|
+
map.set(member.entry.id, alternates);
|
|
83
|
+
}
|
|
84
|
+
return map;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=hreflang.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hreflang.js","sourceRoot":"","sources":["../src/hreflang.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cogenta/seo` — the SEO floor every Cogenta site stands on (L3, task 14).
|
|
3
|
+
*
|
|
4
|
+
* Sitemaps, `robots.txt`, JSON-LD, Open Graph and Twitter Card, RSS and Atom,
|
|
5
|
+
* `hreflang`, canonicals, `llms.txt` and IndexNow. Two properties hold across
|
|
6
|
+
* all of them, and they are the reason the package exists as one unit rather
|
|
7
|
+
* than as helpers scattered through the theme:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Nothing unpublished ever leaves.** One gate (`isIndexable`), asked by
|
|
10
|
+
* every generator, so no output can forget.
|
|
11
|
+
* 2. **Every document is escaped by construction.** No caller ever concatenates
|
|
12
|
+
* XML, so no title can invalidate a feed.
|
|
13
|
+
*
|
|
14
|
+
* The package holds no state, opens no socket except the explicit IndexNow
|
|
15
|
+
* ping, and never touches the database — it is safe inside the render sandbox
|
|
16
|
+
* (rule R5).
|
|
17
|
+
*/
|
|
18
|
+
export type { FeedInput, FeedItem, FeedItemsOptions } from './feeds.js';
|
|
19
|
+
export { feedItemsFor, renderAtomFeed, renderRssFeed, toRfc822 } from './feeds.js';
|
|
20
|
+
export type { HreflangAlternate, TranslationFamily } from './hreflang.js';
|
|
21
|
+
export { alternatesFor, buildHreflangMap, groupTranslationFamilies } from './hreflang.js';
|
|
22
|
+
export type { IndexableOptions } from './indexable.js';
|
|
23
|
+
export { indexableResources, isIndexable, isPublished } from './indexable.js';
|
|
24
|
+
export type { IndexNowFetch, IndexNowOptions, IndexNowResult } from './indexnow.js';
|
|
25
|
+
export { INDEXNOW_MAX_URLS, indexNowKeyFile, pingIndexNow } from './indexnow.js';
|
|
26
|
+
export type { JsonLdObject, JsonLdOptions, JsonLdValue } from './json-ld.js';
|
|
27
|
+
export { buildJsonLd, renderJsonLdScript, schemaTypeFor } from './json-ld.js';
|
|
28
|
+
export type { LlmsTxtLink, LlmsTxtOptions, LlmsTxtSection, LlmsTxtSectionsOptions, } from './llms-txt.js';
|
|
29
|
+
export { llmsTxtSectionsFor, renderLlmsTxt } from './llms-txt.js';
|
|
30
|
+
export type { MetadataOptions, MetaTag } from './metadata.js';
|
|
31
|
+
export { buildMetaTags, escapeHtmlAttribute, escapeHtmlText, renderMetaTags, } from './metadata.js';
|
|
32
|
+
export type { RobotsGroup, RobotsOptions } from './robots.js';
|
|
33
|
+
export { renderRobotsTxt } from './robots.js';
|
|
34
|
+
export type { ChangeFrequency, SitemapFile, SitemapOptions, SitemapUrl } from './sitemap.js';
|
|
35
|
+
export { buildSitemap, SITEMAP_MAX_BYTES, SITEMAP_MAX_URLS, sitemapUrlsFor, } from './sitemap.js';
|
|
36
|
+
export type { SeoImage, SeoReference, SeoResolvers, SeoResource, SeoSite } from './types.js';
|
|
37
|
+
export { absoluteUrl, canonicalUrl, hasRoute, normaliseBaseUrl, routeParams } from './url.js';
|
|
38
|
+
export type { XmlAttributes, XmlElement } from './xml.js';
|
|
39
|
+
export { escapeXmlAttribute, escapeXmlText, renderXmlDocument, renderXmlElement, stripIllegalXmlChars, } from './xml.js';
|
|
40
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAClF,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACzE,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAA;AACzF,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7E,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACnF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAChF,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC5E,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC7E,YAAY,EACV,WAAW,EACX,cAAc,EACd,cAAc,EACd,sBAAsB,GACvB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AACjE,YAAY,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAC7D,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,cAAc,EACd,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC5F,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAC5F,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAC7F,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,UAAU,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cogenta/seo` — the SEO floor every Cogenta site stands on (L3, task 14).
|
|
3
|
+
*
|
|
4
|
+
* Sitemaps, `robots.txt`, JSON-LD, Open Graph and Twitter Card, RSS and Atom,
|
|
5
|
+
* `hreflang`, canonicals, `llms.txt` and IndexNow. Two properties hold across
|
|
6
|
+
* all of them, and they are the reason the package exists as one unit rather
|
|
7
|
+
* than as helpers scattered through the theme:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Nothing unpublished ever leaves.** One gate (`isIndexable`), asked by
|
|
10
|
+
* every generator, so no output can forget.
|
|
11
|
+
* 2. **Every document is escaped by construction.** No caller ever concatenates
|
|
12
|
+
* XML, so no title can invalidate a feed.
|
|
13
|
+
*
|
|
14
|
+
* The package holds no state, opens no socket except the explicit IndexNow
|
|
15
|
+
* ping, and never touches the database — it is safe inside the render sandbox
|
|
16
|
+
* (rule R5).
|
|
17
|
+
*/
|
|
18
|
+
export { feedItemsFor, renderAtomFeed, renderRssFeed, toRfc822 } from './feeds.js';
|
|
19
|
+
export { alternatesFor, buildHreflangMap, groupTranslationFamilies } from './hreflang.js';
|
|
20
|
+
export { indexableResources, isIndexable, isPublished } from './indexable.js';
|
|
21
|
+
export { INDEXNOW_MAX_URLS, indexNowKeyFile, pingIndexNow } from './indexnow.js';
|
|
22
|
+
export { buildJsonLd, renderJsonLdScript, schemaTypeFor } from './json-ld.js';
|
|
23
|
+
export { llmsTxtSectionsFor, renderLlmsTxt } from './llms-txt.js';
|
|
24
|
+
export { buildMetaTags, escapeHtmlAttribute, escapeHtmlText, renderMetaTags, } from './metadata.js';
|
|
25
|
+
export { renderRobotsTxt } from './robots.js';
|
|
26
|
+
export { buildSitemap, SITEMAP_MAX_BYTES, SITEMAP_MAX_URLS, sitemapUrlsFor, } from './sitemap.js';
|
|
27
|
+
export { absoluteUrl, canonicalUrl, hasRoute, normaliseBaseUrl, routeParams } from './url.js';
|
|
28
|
+
export { escapeXmlAttribute, escapeXmlText, renderXmlDocument, renderXmlElement, stripIllegalXmlChars, } from './xml.js';
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAElF,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAA;AAEzF,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE7E,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAEhF,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAO7E,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAEjE,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,cAAc,EACd,cAAc,GACf,MAAM,eAAe,CAAA;AAEtB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,GACf,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE7F,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,UAAU,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ContentEntry } from '@cogenta/schema';
|
|
2
|
+
import type { SeoResource, SeoSite } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The single gate every public artefact passes through.
|
|
5
|
+
*
|
|
6
|
+
* There is one function rather than a filter per output because the failure
|
|
7
|
+
* mode is asymmetric: a draft missing from the sitemap is a delay, a draft
|
|
8
|
+
* present in the RSS feed is a publication. Feed readers cache, and mail
|
|
9
|
+
* digests forward — an unpublished article that reaches a feed cannot be
|
|
10
|
+
* recalled. So sitemap, feeds, `hreflang`, `llms.txt` and IndexNow all ask the
|
|
11
|
+
* same question here, and no caller is trusted to have filtered beforehand.
|
|
12
|
+
*/
|
|
13
|
+
export interface IndexableOptions {
|
|
14
|
+
/** Injected so a scheduled entry can be tested without waiting for the clock. */
|
|
15
|
+
readonly now?: Date;
|
|
16
|
+
}
|
|
17
|
+
export declare function isPublished(entry: ContentEntry, options?: IndexableOptions): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Published **and** reachable at a URL.
|
|
20
|
+
*
|
|
21
|
+
* A published entry in a collection with no `routing` has nothing to put in a
|
|
22
|
+
* sitemap, and an entry whose route parameters are incomplete — a slug that was
|
|
23
|
+
* never filled — would produce a URL that 404s.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isIndexable(site: SeoSite, resource: SeoResource, options?: IndexableOptions): boolean;
|
|
26
|
+
/** The indexable subset of a list, in the order given. */
|
|
27
|
+
export declare function indexableResources(site: SeoSite, resources: readonly SeoResource[], options?: IndexableOptions): readonly SeoResource[];
|
|
28
|
+
//# sourceMappingURL=indexable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexable.d.ts","sourceRoot":"","sources":["../src/indexable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAGtD;;;;;;;;;GASG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAA;CACpB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAmBxF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,WAAW,EACrB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAGT;AAED,0DAA0D;AAC1D,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,SAAS,WAAW,EAAE,EACjC,OAAO,GAAE,gBAAqB,GAC7B,SAAS,WAAW,EAAE,CAExB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { canonicalUrl } from './url.js';
|
|
2
|
+
export function isPublished(entry, options = {}) {
|
|
3
|
+
// `scheduled` and `archived` are both "not public right now", and `draft`
|
|
4
|
+
// never was. Only one of the four statuses means published (contract A).
|
|
5
|
+
if (entry.status !== 'published')
|
|
6
|
+
return false;
|
|
7
|
+
// The working face carries edits nobody has published. Rendering a feed from
|
|
8
|
+
// it ships the unreviewed paragraph an editor typed a minute ago, which is
|
|
9
|
+
// the same leak as publishing a draft — just harder to notice, because the
|
|
10
|
+
// entry legitimately appears in the list.
|
|
11
|
+
if (entry.state !== 'published')
|
|
12
|
+
return false;
|
|
13
|
+
// A published status with a future `publishedAt` is a scheduled entry whose
|
|
14
|
+
// job has not run yet, or one that a scheduler pre-flipped. Either way it is
|
|
15
|
+
// not public.
|
|
16
|
+
if (entry.publishedAt === null)
|
|
17
|
+
return false;
|
|
18
|
+
const at = Date.parse(entry.publishedAt);
|
|
19
|
+
if (Number.isNaN(at))
|
|
20
|
+
return false;
|
|
21
|
+
return at <= (options.now ?? new Date()).getTime();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Published **and** reachable at a URL.
|
|
25
|
+
*
|
|
26
|
+
* A published entry in a collection with no `routing` has nothing to put in a
|
|
27
|
+
* sitemap, and an entry whose route parameters are incomplete — a slug that was
|
|
28
|
+
* never filled — would produce a URL that 404s.
|
|
29
|
+
*/
|
|
30
|
+
export function isIndexable(site, resource, options = {}) {
|
|
31
|
+
if (!isPublished(resource.entry, options))
|
|
32
|
+
return false;
|
|
33
|
+
return canonicalUrl(site, resource) !== null;
|
|
34
|
+
}
|
|
35
|
+
/** The indexable subset of a list, in the order given. */
|
|
36
|
+
export function indexableResources(site, resources, options = {}) {
|
|
37
|
+
return resources.filter((resource) => isIndexable(site, resource, options));
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=indexable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexable.js","sourceRoot":"","sources":["../src/indexable.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAkBvC,MAAM,UAAU,WAAW,CAAC,KAAmB,EAAE,OAAO,GAAqB,EAAE;IAC7E,0EAA0E;IAC1E,yEAAyE;IACzE,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW;QAAE,OAAO,KAAK,CAAA;IAE9C,6EAA6E;IAC7E,2EAA2E;IAC3E,2EAA2E;IAC3E,0CAA0C;IAC1C,IAAI,KAAK,CAAC,KAAK,KAAK,WAAW;QAAE,OAAO,KAAK,CAAA;IAE7C,4EAA4E;IAC5E,6EAA6E;IAC7E,cAAc;IACd,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IACxC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAAE,OAAO,KAAK,CAAA;IAElC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAA;AACpD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,IAAa,EACb,QAAqB,EACrB,OAAO,GAAqB,EAAE;IAE9B,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QAAE,OAAO,KAAK,CAAA;IACvD,OAAO,YAAY,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,IAAI,CAAA;AAC9C,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,kBAAkB,CAChC,IAAa,EACb,SAAiC,EACjC,OAAO,GAAqB,EAAE;IAE9B,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAA;AAC7E,CAAC"}
|