@hagicode/hagilight-starlight 0.2.2 → 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 +49 -5
- package/package.json +5 -2
- 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 +15 -0
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
|
};
|
|
@@ -185,7 +195,19 @@ const isNotFound = /(?:^|\\/)404(?:\\.html)?\\/?$/u.test(Astro.url.pathname);
|
|
|
185
195
|
return {
|
|
186
196
|
name: `@hagicode/hagilight-starlight:${instanceId}`,
|
|
187
197
|
hooks: {
|
|
188
|
-
'astro:config:setup'({ updateConfig }) {
|
|
198
|
+
'astro:config:setup'({ injectRoute, updateConfig }) {
|
|
199
|
+
if (generateRss) {
|
|
200
|
+
injectRoute({
|
|
201
|
+
pattern: '/rss.xml',
|
|
202
|
+
entrypoint: fileURLToPath(new URL('./rss.xml.ts', import.meta.url)),
|
|
203
|
+
prerender: true,
|
|
204
|
+
});
|
|
205
|
+
injectRoute({
|
|
206
|
+
pattern: '/rss.[language].xml',
|
|
207
|
+
entrypoint: fileURLToPath(new URL('./rss.[language].xml.ts', import.meta.url)),
|
|
208
|
+
prerender: true,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
189
211
|
updateConfig({ vite: { plugins: [vitePlugin] } });
|
|
190
212
|
},
|
|
191
213
|
},
|
|
@@ -210,6 +232,14 @@ export default function hagilight(options = {}) {
|
|
|
210
232
|
if (options.notFoundPage?.enabled !== undefined && typeof options.notFoundPage.enabled !== 'boolean') {
|
|
211
233
|
throw new TypeError('Hagilight notFoundPage enabled option must be a boolean.');
|
|
212
234
|
}
|
|
235
|
+
if (options.rss !== undefined
|
|
236
|
+
&& (!options.rss || typeof options.rss !== 'object' || Array.isArray(options.rss))) {
|
|
237
|
+
throw new TypeError('Hagilight rss options must be an object.');
|
|
238
|
+
}
|
|
239
|
+
if (options.rss?.enabled !== undefined && typeof options.rss.enabled !== 'boolean') {
|
|
240
|
+
throw new TypeError('Hagilight rss enabled option must be a boolean.');
|
|
241
|
+
}
|
|
242
|
+
const rssOptions = resolveRssOptions(options.rss);
|
|
213
243
|
if (options.analytics !== undefined
|
|
214
244
|
&& (!options.analytics || typeof options.analytics !== 'object' || Array.isArray(options.analytics))) {
|
|
215
245
|
throw new TypeError('Hagilight analytics options must be an object.');
|
|
@@ -271,7 +301,7 @@ export default function hagilight(options = {}) {
|
|
|
271
301
|
return {
|
|
272
302
|
name: '@hagicode/hagilight-starlight',
|
|
273
303
|
hooks: {
|
|
274
|
-
'config:setup'({ config, updateConfig, addIntegration }) {
|
|
304
|
+
'config:setup'({ astroConfig, config, updateConfig, addIntegration }) {
|
|
275
305
|
if (headerEnabled && config.components?.Header) {
|
|
276
306
|
throw new Error('Hagilight cannot replace an existing Starlight Header override. Set header: { enabled: false } to keep it, or compose @hagicode/hagilight-starlight/Header directly.');
|
|
277
307
|
}
|
|
@@ -291,17 +321,31 @@ export default function hagilight(options = {}) {
|
|
|
291
321
|
throw new Error('Hagilight Google Analytics cannot replace an existing Starlight Head override. Compose @hagicode/hagilight/GoogleAnalytics in your Head instead.');
|
|
292
322
|
}
|
|
293
323
|
|
|
324
|
+
const generateRss = options.rss?.enabled !== false && !getRssFeedUrl(config);
|
|
325
|
+
if (generateRss && !astroConfig.site) {
|
|
326
|
+
throw new Error('Hagilight RSS requires the Astro site option. Set site or disable RSS with rss: { enabled: false }.');
|
|
327
|
+
}
|
|
328
|
+
const rssLocales = generateRss ? resolveRssLocales(config.locales) : [];
|
|
329
|
+
const rssFeedUrl = generateRss
|
|
330
|
+
? new URL('rss.xml', new URL((astroConfig.base ?? '/').replace(/\/?$/u, '/'), astroConfig.site)).toString()
|
|
331
|
+
: undefined;
|
|
294
332
|
addIntegration(createConfiguredIntegration(
|
|
295
333
|
instanceId,
|
|
296
334
|
serializedOptions,
|
|
335
|
+
{ options: rssOptions, locales: rssLocales },
|
|
297
336
|
componentIds,
|
|
298
|
-
() => getRssFeedUrl(config),
|
|
337
|
+
() => getRssFeedUrl(config) ?? rssFeedUrl,
|
|
338
|
+
generateRss,
|
|
299
339
|
));
|
|
300
340
|
updateConfig({
|
|
301
341
|
...(config.logo === undefined ? { logo: { src: defaultLogo, alt: 'HagiCode' } } : {}),
|
|
302
342
|
customCss: [...(config.customCss ?? []), contentWidthCssPath],
|
|
303
343
|
head: [
|
|
304
344
|
...(config.head ?? []),
|
|
345
|
+
...(rssFeedUrl ? [{
|
|
346
|
+
tag: 'link',
|
|
347
|
+
attrs: { rel: 'alternate', type: 'application/rss+xml', href: rssFeedUrl },
|
|
348
|
+
}] : []),
|
|
305
349
|
{ tag: 'script', content: contentWidthHeadScript },
|
|
306
350
|
],
|
|
307
351
|
components: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hagicode/hagilight-starlight",
|
|
3
|
-
"version": "0.2.
|
|
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,11 +31,13 @@
|
|
|
30
31
|
"index.mjs",
|
|
31
32
|
"*.css",
|
|
32
33
|
"*.mjs",
|
|
34
|
+
"rss*.xml.ts",
|
|
33
35
|
"*.astro",
|
|
34
36
|
"assets/*.png"
|
|
35
37
|
],
|
|
36
38
|
"dependencies": {
|
|
37
|
-
"@
|
|
39
|
+
"@astrojs/rss": "^4.0.19",
|
|
40
|
+
"@hagicode/hagilight": "0.2.3-dev.23.1.f56e71c"
|
|
38
41
|
},
|
|
39
42
|
"peerDependencies": {
|
|
40
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
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getCollection } from 'astro:content';
|
|
2
|
+
import type { APIRoute } from 'astro';
|
|
3
|
+
import rssConfig from 'virtual:hagilight-starlight/rss-config';
|
|
4
|
+
import { generateRssFeed } from './rss-renderer.mjs';
|
|
5
|
+
|
|
6
|
+
export const GET: APIRoute = async ({ site }) => {
|
|
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,
|
|
14
|
+
});
|
|
15
|
+
};
|