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