@hagicode/hagilight-starlight 0.3.2-dev.55.1.1e29eba → 0.3.2-dev.58.1.bee3693
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/AIDisclosureNotice.astro +1 -1
- package/ArticlePromotion.astro +1 -1
- package/ContentLayoutToggle.astro +2 -2
- package/Footer.astro +10 -2
- package/Header.astro +1 -1
- package/LanguageChooser.astro +2 -2
- package/MarkdownContent.astro +2 -2
- package/NotFoundHero.astro +1 -1
- package/PromotoFooter.astro +1 -1
- package/README.md +32 -0
- package/SEOHead.astro +2 -4
- package/dist/ai-disclosures.d.ts +31 -0
- package/dist/ai-disclosures.js +105 -0
- package/dist/article-promotion.d.ts +15 -0
- package/dist/article-promotion.js +134 -0
- package/dist/content-width-i18n.d.ts +6 -0
- package/dist/content-width-i18n.js +15 -0
- package/dist/content-width.d.ts +5 -0
- package/dist/content-width.js +42 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +490 -0
- package/dist/language-routing.d.ts +18 -0
- package/dist/language-routing.js +95 -0
- package/dist/locales.d.ts +48 -0
- package/dist/locales.js +13 -0
- package/dist/not-found.d.ts +9 -0
- package/dist/not-found.js +28 -0
- package/dist/rss-renderer.d.ts +14 -0
- package/dist/rss-renderer.js +30 -0
- package/dist/rss-utils.d.ts +27 -0
- package/dist/rss-utils.js +65 -0
- package/dist/schema.d.ts +32 -0
- package/dist/schema.js +25 -0
- package/dist/seo-utils.d.ts +74 -0
- package/dist/seo-utils.js +219 -0
- package/package.json +23 -27
- package/rss.[language].xml.ts +1 -1
- package/rss.xml.ts +1 -1
- package/ai-disclosure-schema.d.ts +0 -8
- package/ai-disclosure-schema.mjs +0 -9
- package/ai-disclosures.mjs +0 -108
- package/article-promotion-schema.d.ts +0 -7
- package/article-promotion-schema.mjs +0 -8
- package/article-promotion.mjs +0 -138
- package/content-width-i18n.mjs +0 -16
- package/content-width.mjs +0 -46
- package/index.d.ts +0 -17
- package/index.mjs +0 -535
- package/language-routing.mjs +0 -109
- package/locales.d.ts +0 -8
- package/locales.mjs +0 -14
- package/not-found.mjs +0 -33
- package/rss-renderer.mjs +0 -31
- package/rss-schema.d.ts +0 -7
- package/rss-schema.mjs +0 -8
- package/rss-utils.mjs +0 -62
- package/seo-schema.d.ts +0 -5
- package/seo-schema.mjs +0 -5
- 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
|
-
}
|
package/language-routing.mjs
DELETED
|
@@ -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
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;
|