@hagicode/hagilight-starlight 0.3.1 → 0.3.2-dev.57.1.ee1f17e

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.
Files changed (58) hide show
  1. package/AIDisclosureNotice.astro +1 -1
  2. package/ArticlePromotion.astro +1 -1
  3. package/ContentLayoutToggle.astro +2 -2
  4. package/Footer.astro +10 -2
  5. package/Header.astro +1 -1
  6. package/LanguageChooser.astro +2 -2
  7. package/MarkdownContent.astro +2 -2
  8. package/NotFoundHero.astro +1 -1
  9. package/PromotoFooter.astro +1 -1
  10. package/SEOHead.astro +2 -4
  11. package/dist/ai-disclosures.d.ts +31 -0
  12. package/dist/ai-disclosures.js +105 -0
  13. package/dist/article-promotion.d.ts +15 -0
  14. package/dist/article-promotion.js +134 -0
  15. package/dist/content-width-i18n.d.ts +6 -0
  16. package/dist/content-width-i18n.js +15 -0
  17. package/dist/content-width.d.ts +5 -0
  18. package/dist/content-width.js +42 -0
  19. package/dist/index.d.ts +58 -0
  20. package/dist/index.js +490 -0
  21. package/dist/language-routing.d.ts +18 -0
  22. package/dist/language-routing.js +95 -0
  23. package/dist/locales.d.ts +48 -0
  24. package/dist/locales.js +13 -0
  25. package/dist/not-found.d.ts +9 -0
  26. package/dist/not-found.js +28 -0
  27. package/dist/rss-renderer.d.ts +14 -0
  28. package/dist/rss-renderer.js +30 -0
  29. package/dist/rss-utils.d.ts +27 -0
  30. package/dist/rss-utils.js +65 -0
  31. package/dist/schema.d.ts +32 -0
  32. package/dist/schema.js +25 -0
  33. package/dist/seo-utils.d.ts +74 -0
  34. package/dist/seo-utils.js +219 -0
  35. package/package.json +22 -27
  36. package/rss.[language].xml.ts +1 -1
  37. package/rss.xml.ts +1 -1
  38. package/ai-disclosure-schema.d.ts +0 -8
  39. package/ai-disclosure-schema.mjs +0 -9
  40. package/ai-disclosures.mjs +0 -108
  41. package/article-promotion-schema.d.ts +0 -7
  42. package/article-promotion-schema.mjs +0 -8
  43. package/article-promotion.mjs +0 -138
  44. package/content-width-i18n.mjs +0 -16
  45. package/content-width.mjs +0 -46
  46. package/index.d.ts +0 -17
  47. package/index.mjs +0 -535
  48. package/language-routing.mjs +0 -109
  49. package/locales.d.ts +0 -8
  50. package/locales.mjs +0 -14
  51. package/not-found.mjs +0 -33
  52. package/rss-renderer.mjs +0 -31
  53. package/rss-schema.d.ts +0 -7
  54. package/rss-schema.mjs +0 -8
  55. package/rss-utils.mjs +0 -62
  56. package/seo-schema.d.ts +0 -5
  57. package/seo-schema.mjs +0 -5
  58. package/seo-utils.mjs +0 -266
package/index.mjs DELETED
@@ -1,535 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { fileURLToPath } from 'node:url';
3
- import { resolveFaviconHeadEntry } from '@hagicode/hagilight/favicon';
4
- import {
5
- HAGILIGHT_RSS_OWNER,
6
- registerStarlightRssOwner,
7
- } from '@hagicode/hagilight/integration';
8
- import { resolveRssLocales, resolveRssOptions } from './rss-utils.mjs';
9
- import {
10
- isValidSeoImageReference,
11
- resolveSeoImageUrl,
12
- resolveSeoLocales,
13
- } from './seo-utils.mjs';
14
-
15
- const GOOGLE_ID_PATTERN = /^G-[A-Z0-9]+$/u;
16
- const FIFTY_ONE_LA_ID_PATTERN = /^[A-Za-z0-9_-]+$/u;
17
- const DEFAULT_GOOGLE_ANALYTICS_ID = 'G-EN03FMT2Q4';
18
- const DEFAULT_51LA_ID = 'L6b88a5yK4h2Xnci';
19
- const defaultLogo = fileURLToPath(import.meta.resolve('@hagicode/hagilight/logo.png'));
20
- const contentWidthCssPath = fileURLToPath(new URL('./content-width.css', import.meta.url));
21
- const contentWidthHeadScript = `(() => {
22
- let mode = 'wide';
23
- try {
24
- const stored = localStorage.getItem('hagilight-content-width');
25
- if (stored === 'wide' || stored === 'narrow') mode = stored;
26
- } catch {
27
- mode = 'wide';
28
- }
29
- document.documentElement.dataset.hagilightContentWidth = mode;
30
- })();`;
31
-
32
- function resolveProvider(config, { name, idKey, pattern, defaultId }) {
33
- if (config !== undefined && (!config || typeof config !== 'object' || Array.isArray(config))) {
34
- throw new TypeError(`Hagilight ${name} analytics options must be an object.`);
35
- }
36
-
37
- const enabled = config === undefined ? true : config.enabled ?? Boolean(config[idKey]);
38
- if (!enabled) return undefined;
39
-
40
- const id = config === undefined ? defaultId : config[idKey];
41
- if (typeof id !== 'string' || !id.trim()) {
42
- throw new Error(`Hagilight ${name} analytics is enabled but its ${idKey} is missing.`);
43
- }
44
- if (!pattern.test(id)) {
45
- throw new Error(`Hagilight ${name} analytics ${idKey} has an invalid format.`);
46
- }
47
- return id;
48
- }
49
-
50
- function resolveAIDisclosures(config) {
51
- if (config === undefined) {
52
- return {
53
- isAITranslation: false,
54
- isAIAuthor: false,
55
- sourceLocale: 'root',
56
- };
57
- }
58
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
59
- throw new TypeError('Hagilight aiDisclosures options must be an object.');
60
- }
61
- for (const key of ['isAITranslation', 'isAIAuthor']) {
62
- if (config[key] !== undefined && typeof config[key] !== 'boolean') {
63
- throw new TypeError(`Hagilight aiDisclosures ${key} option must be a boolean.`);
64
- }
65
- }
66
- const sourceLocale = config.sourceLocale === undefined ? 'root' : config.sourceLocale;
67
- if (typeof sourceLocale !== 'string' || !sourceLocale.trim() || /[/\\]/u.test(sourceLocale)) {
68
- throw new TypeError('Hagilight aiDisclosures sourceLocale option must be a non-empty string.');
69
- }
70
- return {
71
- isAITranslation: config.isAITranslation ?? false,
72
- isAIAuthor: config.isAIAuthor ?? false,
73
- sourceLocale: sourceLocale.trim(),
74
- };
75
- }
76
-
77
- function resolveHagicodePromotion(config) {
78
- if (config === undefined) return true;
79
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
80
- throw new TypeError('Hagilight hagicodePromotion options must be an object.');
81
- }
82
- if (config.enabled !== undefined && typeof config.enabled !== 'boolean') {
83
- throw new TypeError('Hagilight hagicodePromotion enabled option must be a boolean.');
84
- }
85
- return config.enabled ?? true;
86
- }
87
-
88
- function getRssFeedUrl(config) {
89
- if (!Array.isArray(config.head)) return undefined;
90
- const rssLink = config.head.find((entry) => {
91
- const attrs = entry?.attrs;
92
- return entry?.tag === 'link'
93
- && attrs?.rel === 'alternate'
94
- && typeof attrs.type === 'string'
95
- && attrs.type.toLowerCase() === 'application/rss+xml'
96
- && typeof attrs.href === 'string';
97
- });
98
- return rssLink?.attrs.href;
99
- }
100
-
101
- function optionalSeoText(value, field) {
102
- if (value === undefined) return undefined;
103
- if (typeof value !== 'string' || !value.trim()) {
104
- throw new TypeError(`Hagilight SEO ${field} must be a non-empty string.`);
105
- }
106
- return value.trim();
107
- }
108
-
109
- function resolveSeoOptions(config) {
110
- if (config !== undefined && (!config || typeof config !== 'object' || Array.isArray(config))) {
111
- throw new TypeError('Hagilight seo options must be an object.');
112
- }
113
- if (config?.enabled !== undefined && typeof config.enabled !== 'boolean') {
114
- throw new TypeError('Hagilight seo enabled option must be a boolean.');
115
- }
116
-
117
- const image = config?.image;
118
- if (image !== undefined && !isValidSeoImageReference(image)) {
119
- throw new TypeError('Hagilight SEO image must be an absolute HTTP(S) URL or a site-root path.');
120
- }
121
-
122
- let organization;
123
- if (config?.organization !== undefined) {
124
- const identity = config.organization;
125
- if (!identity || typeof identity !== 'object' || Array.isArray(identity)) {
126
- throw new TypeError('Hagilight SEO organization option must be an object.');
127
- }
128
- const name = optionalSeoText(identity.name, 'organization name');
129
- const url = optionalSeoText(identity.url, 'organization URL');
130
- if (!name) throw new TypeError('Hagilight SEO organization name is required.');
131
- if (!url) throw new TypeError('Hagilight SEO organization URL is required.');
132
- let parsedUrl;
133
- try {
134
- parsedUrl = new URL(url);
135
- } catch {
136
- throw new TypeError('Hagilight SEO organization URL must be an absolute HTTP(S) URL.');
137
- }
138
- if (!['http:', 'https:'].includes(parsedUrl.protocol) || parsedUrl.username || parsedUrl.password) {
139
- throw new TypeError('Hagilight SEO organization URL must be an absolute HTTP(S) URL.');
140
- }
141
- if (identity.logo !== undefined && !isValidSeoImageReference(identity.logo)) {
142
- throw new TypeError('Hagilight SEO organization logo must be an absolute HTTP(S) URL or a site-root path.');
143
- }
144
- organization = {
145
- name,
146
- url: parsedUrl.href,
147
- ...(identity.logo === undefined ? {} : { logo: identity.logo }),
148
- };
149
- }
150
-
151
- return {
152
- enabled: config?.enabled ?? true,
153
- title: optionalSeoText(config?.title, 'title'),
154
- description: optionalSeoText(config?.description, 'description'),
155
- ...(image === undefined ? {} : { image }),
156
- ...(organization === undefined ? {} : { organization }),
157
- };
158
- }
159
-
160
- function resolveAstroSite(site) {
161
- if (site === undefined || site === null || site === '') {
162
- throw new Error('Hagilight SEO requires an absolute Astro site URL. Set site or disable generated SEO with seo: { enabled: false }.');
163
- }
164
- let parsed;
165
- try {
166
- parsed = new URL(site);
167
- } catch {
168
- throw new Error('Hagilight SEO requires Astro site to be an absolute HTTP(S) URL.');
169
- }
170
- if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password) {
171
- throw new Error('Hagilight SEO requires Astro site to be an absolute HTTP(S) URL.');
172
- }
173
- return parsed.href;
174
- }
175
-
176
- function resolveDefaultSeoLocale(defaultLocale, locales) {
177
- const configured = typeof defaultLocale === 'object' && defaultLocale !== null
178
- ? defaultLocale.locale ?? defaultLocale.lang
179
- : defaultLocale;
180
- if (configured === undefined) {
181
- return locales.find(({ route }) => route === 'root')?.route ?? locales[0].route;
182
- }
183
- const found = locales.find(({ route, lang }) =>
184
- route === configured || lang.toLowerCase() === String(configured).toLowerCase(),
185
- );
186
- if (!found) {
187
- throw new Error(`Hagilight SEO default locale "${configured}" is not present in Starlight locales.`);
188
- }
189
- return found.route;
190
- }
191
-
192
- function createConfiguredIntegration(
193
- instanceId,
194
- serializedOptions,
195
- rssConfig,
196
- rssLocaleFeedUrls,
197
- componentIds,
198
- getConfiguredRssFeed,
199
- generateRss,
200
- unregisterRssOwner,
201
- ) {
202
- const optionsId = `virtual:hagilight-starlight/${instanceId}/options`;
203
- const rssConfigId = 'virtual:hagilight-starlight/rss-config';
204
- const headerPath = componentIds.header
205
- ? fileURLToPath(new URL('./Header.astro', import.meta.url))
206
- : undefined;
207
- const footerPath = fileURLToPath(new URL('./Footer.astro', import.meta.url));
208
- const pageTitlePath = fileURLToPath(new URL('./PageTitle.astro', import.meta.url));
209
- const markdownContentPath = fileURLToPath(new URL('./MarkdownContent.astro', import.meta.url));
210
- const seoHeadPath = fileURLToPath(new URL('./SEOHead.astro', import.meta.url));
211
- const headerSource = headerPath
212
- ? `---
213
- import Header from ${JSON.stringify(headerPath)};
214
- import options from '${optionsId}';
215
- ---
216
-
217
- <Header links={options.links} />
218
- `
219
- : undefined;
220
- const promotionImport = serializedOptions.promotoEnabled
221
- ? "import PromotoBanner from '@hagicode/hagilight/PromotoBanner';"
222
- : '';
223
- const promotionRender = serializedOptions.promotoEnabled ? '<PromotoBanner />' : '';
224
- const analyticsImport = serializedOptions.fiftyOneLaId
225
- ? "import Analytics51LA from '@hagicode/hagilight/Analytics51LA';"
226
- : '';
227
- const analyticsRender = serializedOptions.fiftyOneLaId
228
- ? '<Analytics51LA siteId={options.fiftyOneLaId} />'
229
- : '';
230
- const footerSource = `---
231
- import Footer from ${JSON.stringify(footerPath)};
232
- ${promotionImport}
233
- ${analyticsImport}
234
- import options from '${optionsId}';
235
- ---
236
-
237
- <Footer
238
- locale={Astro.locals?.starlightRoute?.locale}
239
- links={options.links}
240
- rssFeedUrl={options.rssFeedUrl}
241
- rssLocaleFeedUrls={options.rssLocaleFeedUrls}
242
- />
243
- ${promotionRender}
244
- ${analyticsRender}
245
- `;
246
- const pageTitleSource = `---
247
- import PageTitle from ${JSON.stringify(pageTitlePath)};
248
- ---
249
-
250
- <PageTitle />
251
- `;
252
- const markdownContentSource = `---
253
- import MarkdownContent from ${JSON.stringify(markdownContentPath)};
254
- import options from '${optionsId}';
255
- ---
256
-
257
- <MarkdownContent
258
- aiDisclosures={options.aiDisclosures}
259
- hagicodePromotionEnabled={options.hagicodePromotionEnabled}
260
- ><slot /></MarkdownContent>
261
- `;
262
- const headSource = `---
263
- import DefaultHead from '@astrojs/starlight/components/Head.astro';
264
- import SEOHead from ${JSON.stringify(seoHeadPath)};
265
- import GoogleAnalytics from '@hagicode/hagilight/GoogleAnalytics';
266
- import options from '${optionsId}';
267
- const isNotFound = /(?:^|\\/)404(?:\\.html)?\\/?$/u.test(Astro.url.pathname);
268
- ---
269
-
270
- {options.seoEnabled ? <SEOHead options={options} /> : <DefaultHead />}
271
- {!isNotFound && options.googleAnalyticsMeasurementId && (
272
- <GoogleAnalytics measurementId={options.googleAnalyticsMeasurementId} />
273
- )}
274
- `;
275
- const modules = new Map([
276
- ...(componentIds.header ? [[componentIds.header, headerSource]] : []),
277
- [componentIds.footer, footerSource],
278
- ...(componentIds.pageTitle ? [[componentIds.pageTitle, pageTitleSource]] : []),
279
- ...(componentIds.markdownContent ? [[componentIds.markdownContent, markdownContentSource]] : []),
280
- ...(componentIds.head ? [[componentIds.head, headSource]] : []),
281
- ]);
282
- const vitePlugin = {
283
- name: `@hagicode/hagilight-starlight:${instanceId}`,
284
- resolveId(id) {
285
- return modules.has(id) || id === optionsId || id === rssConfigId ? `\0${id}` : null;
286
- },
287
- load(id) {
288
- if (!id.startsWith('\0')) return null;
289
- const moduleId = id.slice(1);
290
- if (moduleId === optionsId) {
291
- return `export default ${JSON.stringify({
292
- ...serializedOptions,
293
- rssFeedUrl: getConfiguredRssFeed(),
294
- rssLocaleFeedUrls,
295
- })};`;
296
- }
297
- if (moduleId === rssConfigId) return `export default ${JSON.stringify(rssConfig)};`;
298
- return modules.get(moduleId) ?? null;
299
- },
300
- };
301
-
302
- return {
303
- name: `@hagicode/hagilight-starlight:${instanceId}`,
304
- [HAGILIGHT_RSS_OWNER]: { package: 'starlight', enabled: generateRss },
305
- hooks: {
306
- 'astro:config:setup'({ injectRoute, updateConfig }) {
307
- if (generateRss) {
308
- injectRoute({
309
- pattern: '/rss.xml',
310
- entrypoint: fileURLToPath(new URL('./rss.xml.ts', import.meta.url)),
311
- prerender: true,
312
- });
313
- injectRoute({
314
- pattern: '/rss.[language].xml',
315
- entrypoint: fileURLToPath(new URL('./rss.[language].xml.ts', import.meta.url)),
316
- prerender: true,
317
- });
318
- }
319
- updateConfig({ vite: { plugins: [vitePlugin] } });
320
- },
321
- 'astro:config:done'() {
322
- unregisterRssOwner();
323
- },
324
- },
325
- };
326
- }
327
-
328
- export default function hagilight(options = {}) {
329
- if (!options || typeof options !== 'object' || Array.isArray(options)) {
330
- throw new TypeError('Hagilight Starlight options must be an object.');
331
- }
332
- if (options.header !== undefined
333
- && (!options.header || typeof options.header !== 'object' || Array.isArray(options.header))) {
334
- throw new TypeError('Hagilight header options must be an object.');
335
- }
336
- if (options.header?.enabled !== undefined && typeof options.header.enabled !== 'boolean') {
337
- throw new TypeError('Hagilight header enabled option must be a boolean.');
338
- }
339
- if (options.notFoundPage !== undefined
340
- && (!options.notFoundPage || typeof options.notFoundPage !== 'object' || Array.isArray(options.notFoundPage))) {
341
- throw new TypeError('Hagilight notFoundPage options must be an object.');
342
- }
343
- if (options.notFoundPage?.enabled !== undefined && typeof options.notFoundPage.enabled !== 'boolean') {
344
- throw new TypeError('Hagilight notFoundPage enabled option must be a boolean.');
345
- }
346
- if (options.rss !== undefined
347
- && (!options.rss || typeof options.rss !== 'object' || Array.isArray(options.rss))) {
348
- throw new TypeError('Hagilight rss options must be an object.');
349
- }
350
- if (options.rss?.enabled !== undefined && typeof options.rss.enabled !== 'boolean') {
351
- throw new TypeError('Hagilight rss enabled option must be a boolean.');
352
- }
353
- const rssOptions = resolveRssOptions(options.rss);
354
- if (options.analytics !== undefined
355
- && (!options.analytics || typeof options.analytics !== 'object' || Array.isArray(options.analytics))) {
356
- throw new TypeError('Hagilight analytics options must be an object.');
357
- }
358
- const seo = resolveSeoOptions(options.seo);
359
- if (options.contentComponents !== undefined
360
- && (!options.contentComponents || typeof options.contentComponents !== 'object' || Array.isArray(options.contentComponents))) {
361
- throw new TypeError('Hagilight contentComponents options must be an object.');
362
- }
363
- for (const key of ['pageTitle', 'markdownContent']) {
364
- if (options.contentComponents?.[key] !== undefined && typeof options.contentComponents[key] !== 'boolean') {
365
- throw new TypeError(`Hagilight contentComponents ${key} option must be a boolean.`);
366
- }
367
- }
368
- const analytics = options.analytics ?? {};
369
- const aiDisclosures = resolveAIDisclosures(options.aiDisclosures);
370
- const hagicodePromotionEnabled = resolveHagicodePromotion(options.hagicodePromotion);
371
- const googleAnalyticsMeasurementId = resolveProvider(analytics.googleAnalytics, {
372
- name: 'Google Analytics',
373
- idKey: 'measurementId',
374
- pattern: GOOGLE_ID_PATTERN,
375
- defaultId: DEFAULT_GOOGLE_ANALYTICS_ID,
376
- });
377
- const fiftyOneLaId = resolveProvider(analytics.fiftyOneLa, {
378
- name: '51LA',
379
- idKey: 'siteId',
380
- pattern: FIFTY_ONE_LA_ID_PATTERN,
381
- defaultId: DEFAULT_51LA_ID,
382
- });
383
- const instanceId = randomUUID();
384
- const headerEnabled = options.header?.enabled !== false;
385
- const notFoundPageEnabled = options.notFoundPage?.enabled !== false;
386
- const pageTitleEnabled = options.contentComponents?.pageTitle !== false;
387
- const markdownContentEnabled = options.contentComponents?.markdownContent !== false;
388
- const componentIds = {
389
- header: headerEnabled
390
- ? `virtual:hagilight-starlight/${instanceId}/Header.astro`
391
- : undefined,
392
- hero: notFoundPageEnabled
393
- ? fileURLToPath(new URL('./NotFoundHero.astro', import.meta.url))
394
- : undefined,
395
- footer: `virtual:hagilight-starlight/${instanceId}/Footer.astro`,
396
- pageTitle: pageTitleEnabled ? `virtual:hagilight-starlight/${instanceId}/PageTitle.astro` : undefined,
397
- markdownContent: markdownContentEnabled
398
- ? `virtual:hagilight-starlight/${instanceId}/MarkdownContent.astro`
399
- : undefined,
400
- head: seo.enabled || googleAnalyticsMeasurementId
401
- ? `virtual:hagilight-starlight/${instanceId}/Head.astro`
402
- : undefined,
403
- };
404
- const serializedOptions = {
405
- promotoEnabled: options.promoto?.enabled !== false,
406
- hagicodePromotionEnabled,
407
- links: options.links ?? {},
408
- googleAnalyticsMeasurementId,
409
- fiftyOneLaId,
410
- aiDisclosures,
411
- seoEnabled: seo.enabled,
412
- seo,
413
- };
414
- const unregisterRssOwner = registerStarlightRssOwner(options.rss?.enabled !== false);
415
-
416
- return {
417
- name: '@hagicode/hagilight-starlight',
418
- hooks: {
419
- 'config:setup'({ astroConfig, config, updateConfig, addIntegration }) {
420
- if (headerEnabled && config.components?.Header) {
421
- 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.');
422
- }
423
- if (notFoundPageEnabled && config.components?.Hero) {
424
- throw new Error('Hagilight cannot replace an existing Starlight Hero override. Set notFoundPage: { enabled: false } to keep it, or compose @hagicode/hagilight-starlight/NotFoundHero directly.');
425
- }
426
- if (config.components?.Footer) {
427
- throw new Error('Hagilight cannot replace an existing Starlight Footer override.');
428
- }
429
- if (pageTitleEnabled && config.components?.PageTitle) {
430
- throw new Error('Hagilight cannot replace an existing Starlight PageTitle override. Set contentComponents: { pageTitle: false } and compose @hagicode/hagilight-starlight/ContentLayoutToggle into your PageTitle component.');
431
- }
432
- if (markdownContentEnabled && config.components?.MarkdownContent) {
433
- throw new Error('Hagilight cannot replace an existing Starlight MarkdownContent override. Set contentComponents: { markdownContent: false } and compose @hagicode/hagilight-starlight/MarkdownContent into your MarkdownContent component.');
434
- }
435
- if (componentIds.head && config.components?.Head) {
436
- if (seo.enabled) {
437
- throw new Error('Hagilight SEO cannot replace an existing Starlight Head override. Set seo: { enabled: false } and disable Google Analytics to keep your Head, or compose your site metadata explicitly.');
438
- }
439
- throw new Error('Hagilight Google Analytics cannot replace an existing Starlight Head override. Disable Google Analytics or compose @hagicode/hagilight/GoogleAnalytics in your Head instead.');
440
- }
441
-
442
- let seoSite;
443
- let seoLocales = [];
444
- let defaultSeoLocale = 'root';
445
- if (seo.enabled) {
446
- seoSite = resolveAstroSite(astroConfig.site);
447
- seoLocales = resolveSeoLocales(config.locales);
448
- defaultSeoLocale = resolveDefaultSeoLocale(config.defaultLocale, seoLocales);
449
- }
450
- const basePath = astroConfig.base ?? '/';
451
- serializedOptions.seoSite = seoSite;
452
- serializedOptions.seoBasePath = basePath;
453
- serializedOptions.seoLocales = seoLocales;
454
- serializedOptions.seoDefaultLocale = defaultSeoLocale;
455
- serializedOptions.seoFormat = astroConfig.build?.format ?? 'directory';
456
- serializedOptions.seoTrailingSlash = astroConfig.trailingSlash ?? 'ignore';
457
- serializedOptions.seo = seo.enabled
458
- ? {
459
- ...seo,
460
- ...(seo.image === undefined
461
- ? {}
462
- : { image: resolveSeoImageUrl(seo.image, { site: seoSite, basePath }) }),
463
- ...(seo.organization === undefined
464
- ? {}
465
- : {
466
- organization: {
467
- ...seo.organization,
468
- ...(seo.organization.logo === undefined
469
- ? {}
470
- : { logo: resolveSeoImageUrl(seo.organization.logo, { site: seoSite, basePath }) }),
471
- },
472
- }),
473
- }
474
- : seo;
475
- serializedOptions.consumerHead = config.head ?? [];
476
-
477
- const generateRss = options.rss?.enabled !== false && !getRssFeedUrl(config);
478
- if (generateRss && !astroConfig.site) {
479
- throw new Error('Hagilight RSS requires the Astro site option. Set site or disable RSS with rss: { enabled: false }.');
480
- }
481
- const rssLocales = generateRss ? resolveRssLocales(config.locales) : [];
482
- const baseUrl = generateRss
483
- ? new URL((astroConfig.base ?? '/').replace(/\/?$/u, '/'), astroConfig.site)
484
- : undefined;
485
- const rssFeedUrl = baseUrl ? new URL('rss.xml', baseUrl).toString() : undefined;
486
- const rssLocaleFeedUrls = baseUrl
487
- ? Object.fromEntries(rssLocales
488
- .filter(({ filename }) => filename !== 'en')
489
- .map(({ lang, filename }) => [
490
- lang,
491
- new URL(`rss.${filename}.xml`, baseUrl).toString(),
492
- ]))
493
- : {};
494
- addIntegration(createConfiguredIntegration(
495
- instanceId,
496
- serializedOptions,
497
- { options: rssOptions, locales: rssLocales },
498
- rssLocaleFeedUrls,
499
- componentIds,
500
- () => getRssFeedUrl(config) ?? rssFeedUrl,
501
- generateRss,
502
- unregisterRssOwner,
503
- ));
504
- updateConfig({
505
- ...(config.logo === undefined ? { logo: { src: defaultLogo, alt: 'HagiCode' } } : {}),
506
- customCss: [...(config.customCss ?? []), contentWidthCssPath],
507
- head: [
508
- ...(config.head ?? []),
509
- ...(rssFeedUrl ? [{
510
- tag: 'link',
511
- attrs: { rel: 'alternate', type: 'application/rss+xml', href: rssFeedUrl },
512
- }] : []),
513
- { tag: 'script', content: contentWidthHeadScript },
514
- ...(() => {
515
- const entry = resolveFaviconHeadEntry(
516
- config.head,
517
- config.favicon === undefined ? {} : { href: config.favicon },
518
- );
519
- return entry ? [entry] : [];
520
- })(),
521
- ],
522
- components: {
523
- ...config.components,
524
- ...(componentIds.header ? { Header: componentIds.header } : {}),
525
- ...(componentIds.hero ? { Hero: componentIds.hero } : {}),
526
- Footer: componentIds.footer,
527
- ...(componentIds.pageTitle ? { PageTitle: componentIds.pageTitle } : {}),
528
- ...(componentIds.markdownContent ? { MarkdownContent: componentIds.markdownContent } : {}),
529
- ...(componentIds.head ? { Head: componentIds.head } : {}),
530
- },
531
- });
532
- },
533
- },
534
- };
535
- }
@@ -1,109 +0,0 @@
1
- import { locales } from './locales.mjs';
2
-
3
- export const DEFAULT_LANGUAGE_OPTIONS = Object.entries(locales).map(([code, locale]) => ({
4
- code,
5
- label: locale.label,
6
- lang: locale.lang,
7
- }));
8
-
9
- const normalizeLang = (lang) => lang?.replaceAll('_', '-').toLowerCase();
10
-
11
- export function getConfiguredLanguageOptions(locales, currentLocale) {
12
- const catalog = DEFAULT_LANGUAGE_OPTIONS;
13
- return Object.entries(locales ?? {})
14
- .map(([code, locale]) => {
15
- const lang = typeof locale === 'string' ? locale : locale?.lang ?? code;
16
- const catalogEntry = catalog.find((entry) =>
17
- entry.code === code || normalizeLang(entry.lang) === normalizeLang(lang),
18
- );
19
- return {
20
- code,
21
- label: typeof locale === 'object' && locale?.label
22
- ? locale.label
23
- : catalogEntry?.label ?? code,
24
- lang,
25
- selected: code === currentLocale,
26
- catalogOrder: catalogEntry ? catalog.indexOf(catalogEntry) : catalog.length,
27
- };
28
- })
29
- .sort((left, right) => left.catalogOrder - right.catalogOrder)
30
- .map(({ catalogOrder, ...option }) => option);
31
- }
32
-
33
- export function getKeyboardTargetIndex(index, key, count) {
34
- if (count < 1) return -1;
35
- switch (key) {
36
- case 'ArrowDown':
37
- case 'ArrowRight':
38
- return (index + 1) % count;
39
- case 'ArrowUp':
40
- case 'ArrowLeft':
41
- return (index - 1 + count) % count;
42
- case 'Home':
43
- return 0;
44
- case 'End':
45
- return count - 1;
46
- default:
47
- return index;
48
- }
49
- }
50
-
51
- export function buildLocaleNavigationTarget(
52
- currentUrl,
53
- targetLocale,
54
- configuredLocales,
55
- basePath = '/',
56
- trailingSlash = 'ignore',
57
- ) {
58
- const targetUrl = new URL(currentUrl);
59
- const base = basePath.replace(/\/+$/u, '');
60
- const hasBase = base === ''
61
- ? true
62
- : targetUrl.pathname === base || targetUrl.pathname.startsWith(`${base}/`);
63
- let pathname = hasBase ? targetUrl.pathname.slice(base.length) || '/' : targetUrl.pathname;
64
- const [, firstSegment = '', remainder = ''] = pathname.match(/^\/([^/]*)(.*)$/u) ?? [];
65
- const isHtmlRoute = firstSegment.endsWith('.html');
66
- const currentSlug = isHtmlRoute ? firstSegment.slice(0, -5) : firstSegment;
67
- const currentLocale = configuredLocales.find((locale) => locale !== 'root' && locale === currentSlug);
68
-
69
- if (currentLocale) {
70
- if (targetLocale === 'root') {
71
- pathname = isHtmlRoute ? '/index.html' : remainder || '/';
72
- } else {
73
- pathname = `/${targetLocale}${isHtmlRoute ? '.html' : remainder}`;
74
- }
75
- } else if (targetLocale !== 'root') {
76
- pathname = firstSegment === 'index.html'
77
- ? `/${targetLocale}.html`
78
- : `/${targetLocale}${pathname}`;
79
- }
80
-
81
- targetUrl.pathname = `${hasBase ? base : ''}${pathname}` || '/';
82
- if (trailingSlash === 'never') {
83
- targetUrl.pathname = targetUrl.pathname.replace(/\/+$/u, '') || '/';
84
- }
85
- return targetUrl;
86
- }
87
-
88
- export function persistStarlightLocaleSelection(locale) {
89
- try {
90
- const storedValue = window.localStorage.getItem('starlight-route');
91
- let routePreference = {};
92
- if (storedValue) {
93
- try {
94
- const parsed = JSON.parse(storedValue);
95
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
96
- routePreference = parsed;
97
- }
98
- } catch {
99
- routePreference = {};
100
- }
101
- }
102
- window.localStorage.setItem(
103
- 'starlight-route',
104
- JSON.stringify({ ...routePreference, lang: locale }),
105
- );
106
- } catch {
107
- // Language navigation must remain available when browser storage is blocked.
108
- }
109
- }
package/locales.d.ts DELETED
@@ -1,8 +0,0 @@
1
- export interface LocaleDefinition {
2
- label: string;
3
- lang: string;
4
- }
5
-
6
- export const locales: Record<string, LocaleDefinition>;
7
-
8
- export { locales as default };
package/locales.mjs DELETED
@@ -1,14 +0,0 @@
1
- export const locales = {
2
- root: { label: 'English', lang: 'en-US' },
3
- 'zh-CN': { label: '简体中文', lang: 'zh-CN' },
4
- 'zh-Hant': { label: '繁體中文', lang: 'zh-Hant' },
5
- 'fr-FR': { label: 'Français', lang: 'fr-FR' },
6
- 'de-DE': { label: 'Deutsch', lang: 'de-DE' },
7
- 'es-ES': { label: 'Español (España)', lang: 'es-ES' },
8
- 'ja-JP': { label: '日本語', lang: 'ja-JP' },
9
- 'ko-KR': { label: '한국어', lang: 'ko-KR' },
10
- 'pt-BR': { label: 'Português (Brasil)', lang: 'pt-BR' },
11
- 'ru-RU': { label: 'Русский', lang: 'ru-RU' },
12
- };
13
-
14
- export default locales;