@hagicode/hagilight-starlight 0.2.3-dev.22.1.69e2b54 → 0.2.3-dev.24.1.02be9bf
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/Footer.astro +11 -1
- package/index.mjs +35 -4
- 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/Footer.astro
CHANGED
|
@@ -7,12 +7,22 @@ interface Props {
|
|
|
7
7
|
locale?: string;
|
|
8
8
|
links?: SiteLinksOptions;
|
|
9
9
|
rssFeedUrl?: string;
|
|
10
|
+
rssLocaleFeedUrls?: Readonly<Record<string, string>>;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
locale = Astro.locals?.starlightRoute?.locale,
|
|
15
|
+
links,
|
|
16
|
+
rssFeedUrl,
|
|
17
|
+
rssLocaleFeedUrls,
|
|
18
|
+
} = Astro.props;
|
|
19
|
+
const currentLanguage = Astro.locals?.starlightRoute?.lang;
|
|
13
20
|
const resolvedLinks = resolveSiteLinks(locale, {
|
|
14
21
|
...links,
|
|
15
22
|
...(rssFeedUrl === undefined ? {} : { rssFeedUrl }),
|
|
23
|
+
...(currentLanguage && rssLocaleFeedUrls?.[currentLanguage]
|
|
24
|
+
? { rssLocaleFeedUrl: rssLocaleFeedUrls[currentLanguage] }
|
|
25
|
+
: {}),
|
|
16
26
|
});
|
|
17
27
|
---
|
|
18
28
|
|
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,17 @@ 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
|
+
rssLocaleFeedUrls,
|
|
96
|
+
componentIds,
|
|
97
|
+
getConfiguredRssFeed,
|
|
98
|
+
generateRss,
|
|
99
|
+
) {
|
|
91
100
|
const optionsId = `virtual:hagilight-starlight/${instanceId}/options`;
|
|
101
|
+
const rssConfigId = 'virtual:hagilight-starlight/rss-config';
|
|
92
102
|
const headerPath = componentIds.header
|
|
93
103
|
? fileURLToPath(new URL('./Header.astro', import.meta.url))
|
|
94
104
|
: undefined;
|
|
@@ -125,6 +135,7 @@ import options from '${optionsId}';
|
|
|
125
135
|
locale={Astro.locals?.starlightRoute?.locale}
|
|
126
136
|
links={options.links}
|
|
127
137
|
rssFeedUrl={options.rssFeedUrl}
|
|
138
|
+
rssLocaleFeedUrls={options.rssLocaleFeedUrls}
|
|
128
139
|
/>
|
|
129
140
|
${promotionRender}
|
|
130
141
|
${analyticsRender}
|
|
@@ -167,7 +178,7 @@ const isNotFound = /(?:^|\\/)404(?:\\.html)?\\/?$/u.test(Astro.url.pathname);
|
|
|
167
178
|
const vitePlugin = {
|
|
168
179
|
name: `@hagicode/hagilight-starlight:${instanceId}`,
|
|
169
180
|
resolveId(id) {
|
|
170
|
-
return modules.has(id) || id === optionsId ? `\0${id}` : null;
|
|
181
|
+
return modules.has(id) || id === optionsId || id === rssConfigId ? `\0${id}` : null;
|
|
171
182
|
},
|
|
172
183
|
load(id) {
|
|
173
184
|
if (!id.startsWith('\0')) return null;
|
|
@@ -176,8 +187,10 @@ const isNotFound = /(?:^|\\/)404(?:\\.html)?\\/?$/u.test(Astro.url.pathname);
|
|
|
176
187
|
return `export default ${JSON.stringify({
|
|
177
188
|
...serializedOptions,
|
|
178
189
|
rssFeedUrl: getConfiguredRssFeed(),
|
|
190
|
+
rssLocaleFeedUrls,
|
|
179
191
|
})};`;
|
|
180
192
|
}
|
|
193
|
+
if (moduleId === rssConfigId) return `export default ${JSON.stringify(rssConfig)};`;
|
|
181
194
|
return modules.get(moduleId) ?? null;
|
|
182
195
|
},
|
|
183
196
|
};
|
|
@@ -192,6 +205,11 @@ const isNotFound = /(?:^|\\/)404(?:\\.html)?\\/?$/u.test(Astro.url.pathname);
|
|
|
192
205
|
entrypoint: fileURLToPath(new URL('./rss.xml.ts', import.meta.url)),
|
|
193
206
|
prerender: true,
|
|
194
207
|
});
|
|
208
|
+
injectRoute({
|
|
209
|
+
pattern: '/rss.[language].xml',
|
|
210
|
+
entrypoint: fileURLToPath(new URL('./rss.[language].xml.ts', import.meta.url)),
|
|
211
|
+
prerender: true,
|
|
212
|
+
});
|
|
195
213
|
}
|
|
196
214
|
updateConfig({ vite: { plugins: [vitePlugin] } });
|
|
197
215
|
},
|
|
@@ -224,6 +242,7 @@ export default function hagilight(options = {}) {
|
|
|
224
242
|
if (options.rss?.enabled !== undefined && typeof options.rss.enabled !== 'boolean') {
|
|
225
243
|
throw new TypeError('Hagilight rss enabled option must be a boolean.');
|
|
226
244
|
}
|
|
245
|
+
const rssOptions = resolveRssOptions(options.rss);
|
|
227
246
|
if (options.analytics !== undefined
|
|
228
247
|
&& (!options.analytics || typeof options.analytics !== 'object' || Array.isArray(options.analytics))) {
|
|
229
248
|
throw new TypeError('Hagilight analytics options must be an object.');
|
|
@@ -309,12 +328,24 @@ export default function hagilight(options = {}) {
|
|
|
309
328
|
if (generateRss && !astroConfig.site) {
|
|
310
329
|
throw new Error('Hagilight RSS requires the Astro site option. Set site or disable RSS with rss: { enabled: false }.');
|
|
311
330
|
}
|
|
312
|
-
const
|
|
313
|
-
|
|
331
|
+
const rssLocales = generateRss ? resolveRssLocales(config.locales) : [];
|
|
332
|
+
const baseUrl = generateRss
|
|
333
|
+
? new URL((astroConfig.base ?? '/').replace(/\/?$/u, '/'), astroConfig.site)
|
|
314
334
|
: undefined;
|
|
335
|
+
const rssFeedUrl = baseUrl ? new URL('rss.xml', baseUrl).toString() : undefined;
|
|
336
|
+
const rssLocaleFeedUrls = baseUrl
|
|
337
|
+
? Object.fromEntries(rssLocales
|
|
338
|
+
.filter(({ filename }) => filename !== 'en')
|
|
339
|
+
.map(({ lang, filename }) => [
|
|
340
|
+
lang,
|
|
341
|
+
new URL(`rss.${filename}.xml`, baseUrl).toString(),
|
|
342
|
+
]))
|
|
343
|
+
: {};
|
|
315
344
|
addIntegration(createConfiguredIntegration(
|
|
316
345
|
instanceId,
|
|
317
346
|
serializedOptions,
|
|
347
|
+
{ options: rssOptions, locales: rssLocales },
|
|
348
|
+
rssLocaleFeedUrls,
|
|
318
349
|
componentIds,
|
|
319
350
|
() => getRssFeedUrl(config) ?? rssFeedUrl,
|
|
320
351
|
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.24.1.02be9bf",
|
|
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.24.1.02be9bf"
|
|
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
|
};
|