@hagicode/hagilight-starlight 0.2.3-dev.22.1.69e2b54 → 0.2.3-dev.23.1.f56e71c
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/index.mjs +20 -2
- package/package.json +4 -3
- package/rss-renderer.mjs +31 -0
- package/rss-schema.mjs +8 -0
- package/rss-utils.mjs +92 -0
- package/rss.[language].xml.ts +20 -0
- package/rss.xml.ts +9 -19
package/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { resolveRssLocales, resolveRssOptions } from './rss-utils.mjs';
|
|
3
4
|
|
|
4
5
|
const GOOGLE_ID_PATTERN = /^G-[A-Z0-9]+$/u;
|
|
5
6
|
const FIFTY_ONE_LA_ID_PATTERN = /^[A-Za-z0-9_-]+$/u;
|
|
@@ -87,8 +88,16 @@ function getRssFeedUrl(config) {
|
|
|
87
88
|
return rssLink?.attrs.href;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
function createConfiguredIntegration(
|
|
91
|
+
function createConfiguredIntegration(
|
|
92
|
+
instanceId,
|
|
93
|
+
serializedOptions,
|
|
94
|
+
rssConfig,
|
|
95
|
+
componentIds,
|
|
96
|
+
getConfiguredRssFeed,
|
|
97
|
+
generateRss,
|
|
98
|
+
) {
|
|
91
99
|
const optionsId = `virtual:hagilight-starlight/${instanceId}/options`;
|
|
100
|
+
const rssConfigId = 'virtual:hagilight-starlight/rss-config';
|
|
92
101
|
const headerPath = componentIds.header
|
|
93
102
|
? fileURLToPath(new URL('./Header.astro', import.meta.url))
|
|
94
103
|
: undefined;
|
|
@@ -167,7 +176,7 @@ const isNotFound = /(?:^|\\/)404(?:\\.html)?\\/?$/u.test(Astro.url.pathname);
|
|
|
167
176
|
const vitePlugin = {
|
|
168
177
|
name: `@hagicode/hagilight-starlight:${instanceId}`,
|
|
169
178
|
resolveId(id) {
|
|
170
|
-
return modules.has(id) || id === optionsId ? `\0${id}` : null;
|
|
179
|
+
return modules.has(id) || id === optionsId || id === rssConfigId ? `\0${id}` : null;
|
|
171
180
|
},
|
|
172
181
|
load(id) {
|
|
173
182
|
if (!id.startsWith('\0')) return null;
|
|
@@ -178,6 +187,7 @@ const isNotFound = /(?:^|\\/)404(?:\\.html)?\\/?$/u.test(Astro.url.pathname);
|
|
|
178
187
|
rssFeedUrl: getConfiguredRssFeed(),
|
|
179
188
|
})};`;
|
|
180
189
|
}
|
|
190
|
+
if (moduleId === rssConfigId) return `export default ${JSON.stringify(rssConfig)};`;
|
|
181
191
|
return modules.get(moduleId) ?? null;
|
|
182
192
|
},
|
|
183
193
|
};
|
|
@@ -192,6 +202,11 @@ const isNotFound = /(?:^|\\/)404(?:\\.html)?\\/?$/u.test(Astro.url.pathname);
|
|
|
192
202
|
entrypoint: fileURLToPath(new URL('./rss.xml.ts', import.meta.url)),
|
|
193
203
|
prerender: true,
|
|
194
204
|
});
|
|
205
|
+
injectRoute({
|
|
206
|
+
pattern: '/rss.[language].xml',
|
|
207
|
+
entrypoint: fileURLToPath(new URL('./rss.[language].xml.ts', import.meta.url)),
|
|
208
|
+
prerender: true,
|
|
209
|
+
});
|
|
195
210
|
}
|
|
196
211
|
updateConfig({ vite: { plugins: [vitePlugin] } });
|
|
197
212
|
},
|
|
@@ -224,6 +239,7 @@ export default function hagilight(options = {}) {
|
|
|
224
239
|
if (options.rss?.enabled !== undefined && typeof options.rss.enabled !== 'boolean') {
|
|
225
240
|
throw new TypeError('Hagilight rss enabled option must be a boolean.');
|
|
226
241
|
}
|
|
242
|
+
const rssOptions = resolveRssOptions(options.rss);
|
|
227
243
|
if (options.analytics !== undefined
|
|
228
244
|
&& (!options.analytics || typeof options.analytics !== 'object' || Array.isArray(options.analytics))) {
|
|
229
245
|
throw new TypeError('Hagilight analytics options must be an object.');
|
|
@@ -309,12 +325,14 @@ export default function hagilight(options = {}) {
|
|
|
309
325
|
if (generateRss && !astroConfig.site) {
|
|
310
326
|
throw new Error('Hagilight RSS requires the Astro site option. Set site or disable RSS with rss: { enabled: false }.');
|
|
311
327
|
}
|
|
328
|
+
const rssLocales = generateRss ? resolveRssLocales(config.locales) : [];
|
|
312
329
|
const rssFeedUrl = generateRss
|
|
313
330
|
? new URL('rss.xml', new URL((astroConfig.base ?? '/').replace(/\/?$/u, '/'), astroConfig.site)).toString()
|
|
314
331
|
: undefined;
|
|
315
332
|
addIntegration(createConfiguredIntegration(
|
|
316
333
|
instanceId,
|
|
317
334
|
serializedOptions,
|
|
335
|
+
{ options: rssOptions, locales: rssLocales },
|
|
318
336
|
componentIds,
|
|
319
337
|
() => getRssFeedUrl(config) ?? rssFeedUrl,
|
|
320
338
|
generateRss,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hagicode/hagilight-starlight",
|
|
3
|
-
"version": "0.2.3-dev.
|
|
3
|
+
"version": "0.2.3-dev.23.1.f56e71c",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Starlight customizations for HagiCode documentation",
|
|
6
6
|
"repository": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"./locales": "./locales.mjs",
|
|
18
18
|
"./article-promotion-schema": "./article-promotion-schema.mjs",
|
|
19
19
|
"./ai-disclosure-schema": "./ai-disclosure-schema.mjs",
|
|
20
|
+
"./rss-schema": "./rss-schema.mjs",
|
|
20
21
|
"./ContentLayoutToggle": "./ContentLayoutToggle.astro",
|
|
21
22
|
"./Header": "./Header.astro",
|
|
22
23
|
"./Footer": "./Footer.astro",
|
|
@@ -30,13 +31,13 @@
|
|
|
30
31
|
"index.mjs",
|
|
31
32
|
"*.css",
|
|
32
33
|
"*.mjs",
|
|
33
|
-
"rss
|
|
34
|
+
"rss*.xml.ts",
|
|
34
35
|
"*.astro",
|
|
35
36
|
"assets/*.png"
|
|
36
37
|
],
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"@astrojs/rss": "^4.0.19",
|
|
39
|
-
"@hagicode/hagilight": "0.2.3-dev.
|
|
40
|
+
"@hagicode/hagilight": "0.2.3-dev.23.1.f56e71c"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
43
|
"@astrojs/starlight": "^0.42.4",
|
package/rss-renderer.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import rss from '@astrojs/rss';
|
|
2
|
+
import { selectRssEntries } from './rss-utils.mjs';
|
|
3
|
+
|
|
4
|
+
export function generateRssFeed({ site, baseUrl, filename, locales, options, entries }) {
|
|
5
|
+
if (!site) throw new Error('Hagilight RSS requires the Astro site option.');
|
|
6
|
+
const locale = locales.find((item) => item.filename === filename);
|
|
7
|
+
if (!locale && filename !== 'en') {
|
|
8
|
+
throw new Error(`Hagilight RSS has no configured locale for "${filename}".`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const base = new URL(baseUrl, site);
|
|
12
|
+
const items = selectRssEntries(entries, { filename, locales, options }).map(({ id, data }) => {
|
|
13
|
+
const slug = id.replace(/(?:^|\/)index$/u, '');
|
|
14
|
+
return {
|
|
15
|
+
title: data.title,
|
|
16
|
+
description: data.description,
|
|
17
|
+
...(data.lastUpdated instanceof Date && !Number.isNaN(data.lastUpdated.getTime())
|
|
18
|
+
? { pubDate: data.lastUpdated }
|
|
19
|
+
: {}),
|
|
20
|
+
link: new URL(`${slug ? `${slug}/` : ''}`, base).toString(),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return rss({
|
|
25
|
+
title: 'Documentation',
|
|
26
|
+
description: 'Recently updated documentation pages',
|
|
27
|
+
site: base,
|
|
28
|
+
customData: `<language>${locale?.lang ?? 'en'}</language>`,
|
|
29
|
+
items,
|
|
30
|
+
});
|
|
31
|
+
}
|
package/rss-schema.mjs
ADDED
package/rss-utils.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const LANGUAGE_TAG_PATTERN = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/u;
|
|
2
|
+
|
|
3
|
+
export function resolveRssOptions(options = {}) {
|
|
4
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
5
|
+
throw new TypeError('Hagilight rss options must be an object.');
|
|
6
|
+
}
|
|
7
|
+
for (const key of ['includeDocs', 'includeBlog']) {
|
|
8
|
+
if (options[key] !== undefined && typeof options[key] !== 'boolean') {
|
|
9
|
+
throw new TypeError(`Hagilight rss ${key} option must be a boolean.`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
includeDocs: options.includeDocs ?? true,
|
|
14
|
+
includeBlog: options.includeBlog ?? true,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveRssLocales(locales) {
|
|
19
|
+
const configuredLocales = locales === undefined
|
|
20
|
+
? [['root', { lang: 'en' }]]
|
|
21
|
+
: Object.entries(locales);
|
|
22
|
+
if (configuredLocales.length === 0) configuredLocales.push(['root', { lang: 'en' }]);
|
|
23
|
+
|
|
24
|
+
const usedFilenames = new Set();
|
|
25
|
+
const resolved = configuredLocales.map(([route, config]) => {
|
|
26
|
+
const lang = typeof config === 'string' ? config : config?.lang ?? (route === 'root' ? undefined : route);
|
|
27
|
+
if (typeof lang !== 'string' || !LANGUAGE_TAG_PATTERN.test(lang)) {
|
|
28
|
+
throw new TypeError(`Hagilight RSS locale "${route}" must have a valid language tag.`);
|
|
29
|
+
}
|
|
30
|
+
let normalizedLang;
|
|
31
|
+
try {
|
|
32
|
+
[normalizedLang] = Intl.getCanonicalLocales(lang);
|
|
33
|
+
} catch {
|
|
34
|
+
throw new TypeError(`Hagilight RSS locale "${route}" has an invalid language tag "${lang}".`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const filename = /^en(?:-us)?$/iu.test(normalizedLang) ? 'en' : normalizedLang;
|
|
38
|
+
const collisionKey = filename.toLowerCase();
|
|
39
|
+
if (usedFilenames.has(collisionKey)) {
|
|
40
|
+
throw new Error(`Hagilight RSS locales collide on the "${filename}" feed filename.`);
|
|
41
|
+
}
|
|
42
|
+
usedFilenames.add(collisionKey);
|
|
43
|
+
return { route, lang: normalizedLang, filename };
|
|
44
|
+
});
|
|
45
|
+
return resolved;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getLocaleForEntry(id, locales) {
|
|
49
|
+
const localized = locales
|
|
50
|
+
.filter(({ route }) => route !== 'root')
|
|
51
|
+
.sort((a, b) => b.route.length - a.route.length)
|
|
52
|
+
.find(({ route }) => id === route || id.startsWith(`${route}/`));
|
|
53
|
+
if (localized) return { locale: localized, relativeId: id.slice(localized.route.length).replace(/^\/+/, '') };
|
|
54
|
+
|
|
55
|
+
const root = locales.find(({ route }) => route === 'root');
|
|
56
|
+
if (root) return { locale: root, relativeId: id };
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isBlogEntry(id) {
|
|
61
|
+
const segments = id.split('/').filter(Boolean);
|
|
62
|
+
if (segments.at(-1) === 'index') segments.pop();
|
|
63
|
+
return segments[0] === 'blog' && segments.length > 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function selectRssEntries(entries, { filename, locales, options }) {
|
|
67
|
+
const locale = locales.find((item) => item.filename === filename);
|
|
68
|
+
if (!locale) {
|
|
69
|
+
if (filename === 'en') return [];
|
|
70
|
+
throw new Error(`Hagilight RSS has no configured locale for "${filename}".`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return entries
|
|
74
|
+
.filter(({ id, data }) => {
|
|
75
|
+
if (data.rss !== undefined && typeof data.rss !== 'boolean') {
|
|
76
|
+
throw new TypeError(`Hagilight RSS frontmatter field "rss" for "${id}" must be a boolean.`);
|
|
77
|
+
}
|
|
78
|
+
if (data.draft || data.rss === false) return false;
|
|
79
|
+
|
|
80
|
+
const entryLocale = getLocaleForEntry(id, locales);
|
|
81
|
+
if (entryLocale?.locale.filename !== filename) return false;
|
|
82
|
+
const isBlog = isBlogEntry(entryLocale.relativeId);
|
|
83
|
+
return isBlog ? options.includeBlog : options.includeDocs;
|
|
84
|
+
})
|
|
85
|
+
.sort((a, b) => {
|
|
86
|
+
const aDate = a.data.lastUpdated instanceof Date ? a.data.lastUpdated.getTime() : undefined;
|
|
87
|
+
const bDate = b.data.lastUpdated instanceof Date ? b.data.lastUpdated.getTime() : undefined;
|
|
88
|
+
if (aDate === undefined || Number.isNaN(aDate)) return bDate === undefined || Number.isNaN(bDate) ? 0 : 1;
|
|
89
|
+
if (bDate === undefined || Number.isNaN(bDate)) return -1;
|
|
90
|
+
return bDate - aDate;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getCollection } from 'astro:content';
|
|
2
|
+
import type { APIRoute, GetStaticPaths } from 'astro';
|
|
3
|
+
import rssConfig from 'virtual:hagilight-starlight/rss-config';
|
|
4
|
+
import { generateRssFeed } from './rss-renderer.mjs';
|
|
5
|
+
|
|
6
|
+
export const getStaticPaths: GetStaticPaths = () => {
|
|
7
|
+
const filenames = new Set(['en', ...rssConfig.locales.map(({ filename }) => filename)]);
|
|
8
|
+
return [...filenames].map((language) => ({ params: { language } }));
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const GET: APIRoute = async ({ params, site }) => {
|
|
12
|
+
const entries = await getCollection('docs');
|
|
13
|
+
return generateRssFeed({
|
|
14
|
+
site,
|
|
15
|
+
baseUrl: import.meta.env.BASE_URL,
|
|
16
|
+
filename: params.language,
|
|
17
|
+
...rssConfig,
|
|
18
|
+
entries,
|
|
19
|
+
});
|
|
20
|
+
};
|
package/rss.xml.ts
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
1
|
-
import rss from '@astrojs/rss';
|
|
2
1
|
import { getCollection } from 'astro:content';
|
|
3
2
|
import type { APIRoute } from 'astro';
|
|
3
|
+
import rssConfig from 'virtual:hagilight-starlight/rss-config';
|
|
4
|
+
import { generateRssFeed } from './rss-renderer.mjs';
|
|
4
5
|
|
|
5
6
|
export const GET: APIRoute = async ({ site }) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
site: base,
|
|
14
|
-
items: pages
|
|
15
|
-
.sort((a, b) =>
|
|
16
|
-
(b.data.lastUpdated instanceof Date ? b.data.lastUpdated.getTime() : 0)
|
|
17
|
-
- (a.data.lastUpdated instanceof Date ? a.data.lastUpdated.getTime() : 0))
|
|
18
|
-
.map(({ id, data }) => ({
|
|
19
|
-
title: data.title,
|
|
20
|
-
description: data.description,
|
|
21
|
-
...(data.lastUpdated instanceof Date ? { pubDate: data.lastUpdated } : {}),
|
|
22
|
-
link: new URL(`${id.replace(/(?:^|\/)index$/u, '')}/`, base).toString(),
|
|
23
|
-
})),
|
|
7
|
+
const entries = await getCollection('docs');
|
|
8
|
+
return generateRssFeed({
|
|
9
|
+
site,
|
|
10
|
+
baseUrl: import.meta.env.BASE_URL,
|
|
11
|
+
filename: 'en',
|
|
12
|
+
...rssConfig,
|
|
13
|
+
entries,
|
|
24
14
|
});
|
|
25
15
|
};
|