@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 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 { locale = Astro.locals?.starlightRoute?.locale, links, rssFeedUrl } = Astro.props;
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(instanceId, serializedOptions, componentIds, getConfiguredRssFeed, generateRss) {
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 rssFeedUrl = generateRss
313
- ? new URL('rss.xml', new URL((astroConfig.base ?? '/').replace(/\/?$/u, '/'), astroConfig.site)).toString()
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.22.1.69e2b54",
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.xml.ts",
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.22.1.69e2b54"
40
+ "@hagicode/hagilight": "0.2.3-dev.24.1.02be9bf"
40
41
  },
41
42
  "peerDependencies": {
42
43
  "@astrojs/starlight": "^0.42.4",
@@ -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
@@ -0,0 +1,8 @@
1
+ import { z } from 'astro/zod';
2
+
3
+ const rssSchema = z.object({
4
+ rss: z.boolean().optional(),
5
+ });
6
+
7
+ export { rssSchema };
8
+ export default rssSchema;
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
- if (!site) throw new Error('Hagilight RSS requires the Astro site option.');
7
-
8
- const pages = await getCollection('docs', ({ data }) => !data.draft);
9
- const base = new URL(import.meta.env.BASE_URL, site);
10
- return rss({
11
- title: 'Documentation',
12
- description: 'Recently updated documentation pages',
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
  };