@hagicode/hagilight-starlight 0.1.0-dev.148.1.e0c630e
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 +12 -0
- package/ArticlePromotion.astro +630 -0
- package/ArticlePromotionPicker.astro +9 -0
- package/ContentLayoutToggle.astro +31 -0
- package/Footer.astro +201 -0
- package/Header.astro +131 -0
- package/LanguageChooser.astro +386 -0
- package/MarkdownContent.astro +45 -0
- package/NotFoundHero.astro +44 -0
- package/PageTitle.astro +7 -0
- package/PromotoFooter.astro +7 -0
- package/README.md +178 -0
- package/SEOHead.astro +67 -0
- package/ThemeSelect.astro +176 -0
- package/assets/showcase/hero-avatar.webp +0 -0
- package/assets/showcase/heroes.webp +0 -0
- package/assets/showcase/manifest.json +333 -0
- package/assets/showcase/pillar-efficient.svg +1 -0
- package/assets/showcase/pillar-fun.svg +1 -0
- package/assets/showcase/pillar-smart.svg +1 -0
- package/assets/showcase/proposal-workflow.webp +0 -0
- package/assets/showcase/workbench.webp +0 -0
- package/content-width.css +81 -0
- package/dist/ai-disclosures.d.ts +31 -0
- package/dist/ai-disclosures.js +105 -0
- package/dist/article-promotion-copy/de-DE.d.ts +34 -0
- package/dist/article-promotion-copy/de-DE.js +43 -0
- package/dist/article-promotion-copy/en-US.d.ts +34 -0
- package/dist/article-promotion-copy/en-US.js +43 -0
- package/dist/article-promotion-copy/es-ES.d.ts +34 -0
- package/dist/article-promotion-copy/es-ES.js +43 -0
- package/dist/article-promotion-copy/fr-FR.d.ts +34 -0
- package/dist/article-promotion-copy/fr-FR.js +43 -0
- package/dist/article-promotion-copy/index.d.ts +32 -0
- package/dist/article-promotion-copy/index.js +50 -0
- package/dist/article-promotion-copy/ja-JP.d.ts +34 -0
- package/dist/article-promotion-copy/ja-JP.js +43 -0
- package/dist/article-promotion-copy/ko-KR.d.ts +34 -0
- package/dist/article-promotion-copy/ko-KR.js +43 -0
- package/dist/article-promotion-copy/pt-BR.d.ts +34 -0
- package/dist/article-promotion-copy/pt-BR.js +43 -0
- package/dist/article-promotion-copy/ru-RU.d.ts +34 -0
- package/dist/article-promotion-copy/ru-RU.js +43 -0
- package/dist/article-promotion-copy/zh-CN.d.ts +34 -0
- package/dist/article-promotion-copy/zh-CN.js +43 -0
- package/dist/article-promotion-copy/zh-Hant.d.ts +34 -0
- package/dist/article-promotion-copy/zh-Hant.js +43 -0
- package/dist/article-promotion.d.ts +40 -0
- package/dist/article-promotion.js +14 -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 +105 -0
- package/dist/index.js +583 -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/dist/showcase.d.ts +6 -0
- package/dist/showcase.js +5 -0
- package/dist/site-metadata.d.ts +38 -0
- package/dist/site-metadata.js +109 -0
- package/dist/themes.d.ts +43 -0
- package/dist/themes.js +156 -0
- package/dist/windows-download.d.ts +4 -0
- package/dist/windows-download.js +22 -0
- package/package.json +88 -0
- package/rss.[language].xml.ts +20 -0
- package/rss.xml.ts +15 -0
- package/themes.css +160 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { resolveFaviconHeadEntry } from '@hagicode/hagilight-core/favicon';
|
|
4
|
+
import { resolveBuildInfo } from '@hagicode/hagilight-core/build-info-resolver';
|
|
5
|
+
import { resolveFooterAvatarOptions } from '@hagicode/hagilight-core/footer-avatar';
|
|
6
|
+
import { checkContentImageRegistration, registerContentImages } from '@hagicode/hagilight-core/content-images';
|
|
7
|
+
import { DEFAULT_POSTHOG_API_KEY, POSTHOG_API_KEY_PATTERN } from '@hagicode/hagilight-core/posthog';
|
|
8
|
+
import { resolveRssLocales } from '@hagicode/hagilight-core/rss';
|
|
9
|
+
import { RSS_OWNER, registerStarlightRssOwner, } from '@hagicode/hagilight-core/rss-ownership';
|
|
10
|
+
import { isValidSeoImageReference, resolveSeoImageUrl } from '@hagicode/hagilight-core/seo';
|
|
11
|
+
import { resolveSiteMetadataOption } from '@hagicode/hagilight-core/site-metadata';
|
|
12
|
+
import { registerStarlightSiteMetadataOwner } from '@hagicode/hagilight-core/site-metadata-ownership';
|
|
13
|
+
import { resolveRssOptions } from './rss-utils.js';
|
|
14
|
+
import { resolveSeoLocales } from './seo-utils.js';
|
|
15
|
+
import { createStarlightMetadataFinalizer } from './site-metadata.js';
|
|
16
|
+
import { THEME_BOOTSTRAP } from './themes.js';
|
|
17
|
+
const packageRoot = new URL('../', import.meta.url);
|
|
18
|
+
const packageFile = (name) => fileURLToPath(new URL(name, packageRoot));
|
|
19
|
+
const coreExport = (name) => fileURLToPath(import.meta.resolve(`@hagicode/hagilight-core/${name}`));
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function optionalRecord(value, label) {
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (!isRecord(value))
|
|
27
|
+
throw new TypeError(`Hagilight ${label} options must be an object.`);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function optionalBoolean(record, key, label) {
|
|
31
|
+
const value = record?.[key];
|
|
32
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
33
|
+
throw new TypeError(`Hagilight ${label} option must be a boolean.`);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
const NOOP_IMAGE_SERVICE = 'astro/assets/services/noop';
|
|
38
|
+
const NOOP_IMAGE_SERVICE_WARNING = '@hagicode/hagilight-starlight: the Astro image service is the passthrough no-op, so the showcase AVIF and JPEG tiers will serve the original bytes. Use the default sharp service or another transforming image service.';
|
|
39
|
+
const GOOGLE_ID_PATTERN = /^G-[A-Z0-9]+$/u;
|
|
40
|
+
const FIFTY_ONE_LA_ID_PATTERN = /^[A-Za-z0-9_-]+$/u;
|
|
41
|
+
const DEFAULT_GOOGLE_ANALYTICS_ID = 'G-EN03FMT2Q4';
|
|
42
|
+
const DEFAULT_51LA_ID = 'L6b88a5yK4h2Xnci';
|
|
43
|
+
const defaultLogo = coreExport('logo.png');
|
|
44
|
+
const contentWidthCssPath = packageFile('content-width.css');
|
|
45
|
+
const themesCssPath = packageFile('themes.css');
|
|
46
|
+
const contentWidthHeadScript = `(() => {
|
|
47
|
+
let mode = 'wide';
|
|
48
|
+
try {
|
|
49
|
+
const stored = localStorage.getItem('hagilight-content-width');
|
|
50
|
+
if (stored === 'wide' || stored === 'narrow') mode = stored;
|
|
51
|
+
} catch {
|
|
52
|
+
mode = 'wide';
|
|
53
|
+
}
|
|
54
|
+
document.documentElement.dataset.hagilightContentWidth = mode;
|
|
55
|
+
})();`;
|
|
56
|
+
function resolveProvider(config, { name, idKey, pattern, defaultId }) {
|
|
57
|
+
if (config !== undefined && !isRecord(config)) {
|
|
58
|
+
throw new TypeError(`Hagilight ${name} analytics options must be an object.`);
|
|
59
|
+
}
|
|
60
|
+
const enabled = config === undefined ? true : config.enabled ?? Boolean(config[idKey]);
|
|
61
|
+
if (!enabled)
|
|
62
|
+
return undefined;
|
|
63
|
+
const id = config === undefined ? defaultId : config[idKey];
|
|
64
|
+
if (typeof id !== 'string' || !id.trim()) {
|
|
65
|
+
throw new Error(`Hagilight ${name} analytics is enabled but its ${idKey} is missing.`);
|
|
66
|
+
}
|
|
67
|
+
if (!pattern.test(id)) {
|
|
68
|
+
throw new Error(`Hagilight ${name} analytics ${idKey} has an invalid format.`);
|
|
69
|
+
}
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
function resolveAIDisclosures(config) {
|
|
73
|
+
if (config === undefined) {
|
|
74
|
+
return {
|
|
75
|
+
isAITranslation: false,
|
|
76
|
+
isAIAuthor: false,
|
|
77
|
+
sourceLocale: 'root',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (!isRecord(config)) {
|
|
81
|
+
throw new TypeError('Hagilight aiDisclosures options must be an object.');
|
|
82
|
+
}
|
|
83
|
+
for (const key of ['isAITranslation', 'isAIAuthor']) {
|
|
84
|
+
if (config[key] !== undefined && typeof config[key] !== 'boolean') {
|
|
85
|
+
throw new TypeError(`Hagilight aiDisclosures ${key} option must be a boolean.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const sourceLocale = config.sourceLocale === undefined ? 'root' : config.sourceLocale;
|
|
89
|
+
if (typeof sourceLocale !== 'string' || !sourceLocale.trim() || /[/\\]/u.test(sourceLocale)) {
|
|
90
|
+
throw new TypeError('Hagilight aiDisclosures sourceLocale option must be a non-empty string.');
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
isAITranslation: config.isAITranslation ?? false,
|
|
94
|
+
isAIAuthor: config.isAIAuthor ?? false,
|
|
95
|
+
sourceLocale: sourceLocale.trim(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function resolveHagicodePromotion(config) {
|
|
99
|
+
if (config === undefined)
|
|
100
|
+
return true;
|
|
101
|
+
if (!isRecord(config)) {
|
|
102
|
+
throw new TypeError('Hagilight hagicodePromotion options must be an object.');
|
|
103
|
+
}
|
|
104
|
+
if (config.enabled !== undefined && typeof config.enabled !== 'boolean') {
|
|
105
|
+
throw new TypeError('Hagilight hagicodePromotion enabled option must be a boolean.');
|
|
106
|
+
}
|
|
107
|
+
return config.enabled ?? true;
|
|
108
|
+
}
|
|
109
|
+
function resolveHagicodePromotionRandomAvatar(config) {
|
|
110
|
+
if (config === undefined || !isRecord(config))
|
|
111
|
+
return true;
|
|
112
|
+
if (config.randomAvatar !== undefined && typeof config.randomAvatar !== 'boolean') {
|
|
113
|
+
throw new TypeError('Hagilight hagicodePromotion randomAvatar option must be a boolean.');
|
|
114
|
+
}
|
|
115
|
+
return config.randomAvatar ?? true;
|
|
116
|
+
}
|
|
117
|
+
function getRssFeedUrl(config) {
|
|
118
|
+
if (!Array.isArray(config.head))
|
|
119
|
+
return undefined;
|
|
120
|
+
const rssLink = config.head.find((entry) => {
|
|
121
|
+
const attrs = entry?.attrs;
|
|
122
|
+
return entry?.tag === 'link'
|
|
123
|
+
&& attrs?.rel === 'alternate'
|
|
124
|
+
&& typeof attrs.type === 'string'
|
|
125
|
+
&& attrs.type.toLowerCase() === 'application/rss+xml'
|
|
126
|
+
&& typeof attrs.href === 'string';
|
|
127
|
+
});
|
|
128
|
+
return rssLink?.attrs?.href;
|
|
129
|
+
}
|
|
130
|
+
function optionalSeoText(value, field) {
|
|
131
|
+
if (value === undefined)
|
|
132
|
+
return undefined;
|
|
133
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
134
|
+
throw new TypeError(`Hagilight SEO ${field} must be a non-empty string.`);
|
|
135
|
+
}
|
|
136
|
+
return value.trim();
|
|
137
|
+
}
|
|
138
|
+
function resolveSeoOptions(config) {
|
|
139
|
+
if (config !== undefined && !isRecord(config)) {
|
|
140
|
+
throw new TypeError('Hagilight seo options must be an object.');
|
|
141
|
+
}
|
|
142
|
+
if (config?.enabled !== undefined && typeof config.enabled !== 'boolean') {
|
|
143
|
+
throw new TypeError('Hagilight seo enabled option must be a boolean.');
|
|
144
|
+
}
|
|
145
|
+
const image = config?.image;
|
|
146
|
+
if (image !== undefined && !isValidSeoImageReference(image)) {
|
|
147
|
+
throw new TypeError('Hagilight SEO image must be an absolute HTTP(S) URL or a site-root path.');
|
|
148
|
+
}
|
|
149
|
+
let organization;
|
|
150
|
+
if (config?.organization !== undefined) {
|
|
151
|
+
const identity = config.organization;
|
|
152
|
+
if (!isRecord(identity)) {
|
|
153
|
+
throw new TypeError('Hagilight SEO organization option must be an object.');
|
|
154
|
+
}
|
|
155
|
+
const name = optionalSeoText(identity.name, 'organization name');
|
|
156
|
+
const url = optionalSeoText(identity.url, 'organization URL');
|
|
157
|
+
if (!name)
|
|
158
|
+
throw new TypeError('Hagilight SEO organization name is required.');
|
|
159
|
+
if (!url)
|
|
160
|
+
throw new TypeError('Hagilight SEO organization URL is required.');
|
|
161
|
+
let parsedUrl;
|
|
162
|
+
try {
|
|
163
|
+
parsedUrl = new URL(url);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
throw new TypeError('Hagilight SEO organization URL must be an absolute HTTP(S) URL.');
|
|
167
|
+
}
|
|
168
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol) || parsedUrl.username || parsedUrl.password) {
|
|
169
|
+
throw new TypeError('Hagilight SEO organization URL must be an absolute HTTP(S) URL.');
|
|
170
|
+
}
|
|
171
|
+
if (identity.logo !== undefined && !isValidSeoImageReference(identity.logo)) {
|
|
172
|
+
throw new TypeError('Hagilight SEO organization logo must be an absolute HTTP(S) URL or a site-root path.');
|
|
173
|
+
}
|
|
174
|
+
organization = {
|
|
175
|
+
name,
|
|
176
|
+
url: parsedUrl.href,
|
|
177
|
+
...(identity.logo === undefined ? {} : { logo: identity.logo }),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
enabled: config?.enabled ?? true,
|
|
182
|
+
title: optionalSeoText(config?.title, 'title'),
|
|
183
|
+
description: optionalSeoText(config?.description, 'description'),
|
|
184
|
+
...(image === undefined ? {} : { image }),
|
|
185
|
+
...(organization === undefined ? {} : { organization }),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function resolveAstroSite(site) {
|
|
189
|
+
if (site === undefined || site === null || site === '') {
|
|
190
|
+
throw new Error('Hagilight SEO requires an absolute Astro site URL. Set site or disable generated SEO with seo: { enabled: false }.');
|
|
191
|
+
}
|
|
192
|
+
let parsed;
|
|
193
|
+
try {
|
|
194
|
+
parsed = new URL(site);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
throw new Error('Hagilight SEO requires Astro site to be an absolute HTTP(S) URL.');
|
|
198
|
+
}
|
|
199
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password) {
|
|
200
|
+
throw new Error('Hagilight SEO requires Astro site to be an absolute HTTP(S) URL.');
|
|
201
|
+
}
|
|
202
|
+
return parsed.href;
|
|
203
|
+
}
|
|
204
|
+
function resolveDefaultSeoLocale(defaultLocale, locales) {
|
|
205
|
+
const configured = isRecord(defaultLocale)
|
|
206
|
+
? defaultLocale.locale ?? defaultLocale.lang
|
|
207
|
+
: defaultLocale;
|
|
208
|
+
if (configured === undefined) {
|
|
209
|
+
return locales.find(({ route }) => route === 'root')?.route ?? locales[0].route;
|
|
210
|
+
}
|
|
211
|
+
const found = locales.find(({ route, lang }) => route === configured || lang.toLowerCase() === String(configured).toLowerCase());
|
|
212
|
+
if (!found) {
|
|
213
|
+
throw new Error(`Hagilight SEO default locale "${configured}" is not present in Starlight locales.`);
|
|
214
|
+
}
|
|
215
|
+
return found.route;
|
|
216
|
+
}
|
|
217
|
+
function createConfiguredIntegration(instanceId, serializedOptions, rssConfig, rssLocaleFeedUrls, componentIds, getConfiguredRssFeed, generateRss, unregisterRssOwner, contentImagesEnabled, siteMetadata, unregisterSiteMetadataOwner) {
|
|
218
|
+
const optionsId = `virtual:hagilight-starlight/${instanceId}/options`;
|
|
219
|
+
const rssConfigId = 'virtual:hagilight-starlight/rss-config';
|
|
220
|
+
const headerPath = componentIds.header ? packageFile('Header.astro') : undefined;
|
|
221
|
+
const footerPath = packageFile('Footer.astro');
|
|
222
|
+
const pageTitlePath = packageFile('PageTitle.astro');
|
|
223
|
+
const markdownContentPath = packageFile('MarkdownContent.astro');
|
|
224
|
+
const seoHeadPath = packageFile('SEOHead.astro');
|
|
225
|
+
const headerSource = headerPath
|
|
226
|
+
? `---
|
|
227
|
+
import Header from ${JSON.stringify(headerPath)};
|
|
228
|
+
import options from '${optionsId}';
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
<Header links={options.links} />
|
|
232
|
+
`
|
|
233
|
+
: undefined;
|
|
234
|
+
const promotionImport = serializedOptions.promotoEnabled
|
|
235
|
+
? `import PromotoBanner from ${JSON.stringify(coreExport('PromotoBanner'))};`
|
|
236
|
+
: '';
|
|
237
|
+
const promotionRender = serializedOptions.promotoEnabled
|
|
238
|
+
? '<PromotoBanner siteId={options.links?.siteId} />'
|
|
239
|
+
: '';
|
|
240
|
+
const analyticsImport = serializedOptions.fiftyOneLaId
|
|
241
|
+
? `import Analytics51LA from ${JSON.stringify(coreExport('Analytics51LA'))};`
|
|
242
|
+
: '';
|
|
243
|
+
const analyticsRender = serializedOptions.fiftyOneLaId
|
|
244
|
+
? '<Analytics51LA siteId={options.fiftyOneLaId} />'
|
|
245
|
+
: '';
|
|
246
|
+
const footerSource = `---
|
|
247
|
+
import Footer from ${JSON.stringify(footerPath)};
|
|
248
|
+
${promotionImport}
|
|
249
|
+
${analyticsImport}
|
|
250
|
+
import options from '${optionsId}';
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
<Footer
|
|
254
|
+
locale={Astro.locals?.starlightRoute?.locale}
|
|
255
|
+
links={options.links}
|
|
256
|
+
rssFeedUrl={options.rssFeedUrl}
|
|
257
|
+
rssLocaleFeedUrls={options.rssLocaleFeedUrls}
|
|
258
|
+
buildInfo={options.buildInfo}
|
|
259
|
+
avatar={options.footerAvatar.avatar}
|
|
260
|
+
randomAvatar={options.footerAvatar.randomAvatar}
|
|
261
|
+
/>
|
|
262
|
+
${promotionRender}
|
|
263
|
+
${analyticsRender}
|
|
264
|
+
`;
|
|
265
|
+
const pageTitleSource = `---
|
|
266
|
+
import PageTitle from ${JSON.stringify(pageTitlePath)};
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
<PageTitle />
|
|
270
|
+
`;
|
|
271
|
+
const markdownContentSource = `---
|
|
272
|
+
import MarkdownContent from ${JSON.stringify(markdownContentPath)};
|
|
273
|
+
import options from '${optionsId}';
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
<MarkdownContent
|
|
277
|
+
aiDisclosures={options.aiDisclosures}
|
|
278
|
+
hagicodePromotionEnabled={options.hagicodePromotionEnabled}
|
|
279
|
+
hagicodePromotionRandomAvatar={options.hagicodePromotionRandomAvatar}
|
|
280
|
+
><slot /></MarkdownContent>
|
|
281
|
+
`;
|
|
282
|
+
const headSource = `---
|
|
283
|
+
import DefaultHead from '@astrojs/starlight/components/Head.astro';
|
|
284
|
+
import SEOHead from ${JSON.stringify(seoHeadPath)};
|
|
285
|
+
import GoogleAnalytics from ${JSON.stringify(coreExport('GoogleAnalytics'))};
|
|
286
|
+
${serializedOptions.posthogApiKey ? `import PostHog from ${JSON.stringify(coreExport('PostHog'))};` : ''}
|
|
287
|
+
import options from '${optionsId}';
|
|
288
|
+
const isNotFound = /(?:^|\\/)404(?:\\.html)?\\/?$/u.test(Astro.url.pathname);
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
{options.seoEnabled ? <SEOHead options={options} /> : <DefaultHead />}
|
|
292
|
+
${serializedOptions.posthogApiKey ? '<PostHog apiKey={options.posthogApiKey} />' : ''}
|
|
293
|
+
{!isNotFound && options.googleAnalyticsMeasurementId && (
|
|
294
|
+
<GoogleAnalytics measurementId={options.googleAnalyticsMeasurementId} />
|
|
295
|
+
)}
|
|
296
|
+
`;
|
|
297
|
+
const modules = new Map([
|
|
298
|
+
...(componentIds.header && headerSource ? [[componentIds.header, headerSource]] : []),
|
|
299
|
+
[componentIds.footer, footerSource],
|
|
300
|
+
...(componentIds.pageTitle ? [[componentIds.pageTitle, pageTitleSource]] : []),
|
|
301
|
+
...(componentIds.markdownContent ? [[componentIds.markdownContent, markdownContentSource]] : []),
|
|
302
|
+
...(componentIds.head ? [[componentIds.head, headSource]] : []),
|
|
303
|
+
]);
|
|
304
|
+
let buildInfo;
|
|
305
|
+
const vitePlugin = {
|
|
306
|
+
name: `@hagicode/hagilight-starlight:${instanceId}`,
|
|
307
|
+
resolveId(id) {
|
|
308
|
+
return modules.has(id) || id === optionsId || id === rssConfigId ? `\0${id}` : null;
|
|
309
|
+
},
|
|
310
|
+
load(id) {
|
|
311
|
+
if (!id.startsWith('\0'))
|
|
312
|
+
return null;
|
|
313
|
+
const moduleId = id.slice(1);
|
|
314
|
+
if (moduleId === optionsId) {
|
|
315
|
+
return `export default ${JSON.stringify({
|
|
316
|
+
...serializedOptions,
|
|
317
|
+
rssFeedUrl: getConfiguredRssFeed(),
|
|
318
|
+
rssLocaleFeedUrls,
|
|
319
|
+
buildInfo,
|
|
320
|
+
})};`;
|
|
321
|
+
}
|
|
322
|
+
if (moduleId === rssConfigId)
|
|
323
|
+
return `export default ${JSON.stringify(rssConfig)};`;
|
|
324
|
+
return modules.get(moduleId) ?? null;
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
name: `@hagicode/hagilight-starlight:${instanceId}`,
|
|
329
|
+
[RSS_OWNER]: { package: 'starlight', enabled: generateRss },
|
|
330
|
+
hooks: {
|
|
331
|
+
'astro:config:setup'({ config, injectRoute, updateConfig, logger }) {
|
|
332
|
+
registerContentImages({ config, logger, enabled: contentImagesEnabled });
|
|
333
|
+
buildInfo = resolveBuildInfo({ root: fileURLToPath(config.root) });
|
|
334
|
+
if (generateRss) {
|
|
335
|
+
injectRoute({
|
|
336
|
+
pattern: '/rss.xml',
|
|
337
|
+
entrypoint: packageFile('rss.xml.ts'),
|
|
338
|
+
prerender: true,
|
|
339
|
+
});
|
|
340
|
+
injectRoute({
|
|
341
|
+
pattern: '/rss.[language].xml',
|
|
342
|
+
entrypoint: packageFile('rss.[language].xml.ts'),
|
|
343
|
+
prerender: true,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
// Starlight splices its own sitemap into the integration list before this setup runs, so
|
|
347
|
+
// the finalizer appended in the same update runs after the sitemap has written its index.
|
|
348
|
+
updateConfig({
|
|
349
|
+
vite: { plugins: [vitePlugin] },
|
|
350
|
+
...(siteMetadata.enabled
|
|
351
|
+
? { integrations: [createStarlightMetadataFinalizer(siteMetadata, () => buildInfo)] }
|
|
352
|
+
: {}),
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
'astro:config:done'({ config, logger }) {
|
|
356
|
+
unregisterRssOwner();
|
|
357
|
+
unregisterSiteMetadataOwner();
|
|
358
|
+
checkContentImageRegistration({ config, logger, enabled: contentImagesEnabled });
|
|
359
|
+
if (config.image?.service?.entrypoint === NOOP_IMAGE_SERVICE) {
|
|
360
|
+
logger.warn(NOOP_IMAGE_SERVICE_WARNING);
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/** Starlight plugin that applies HagiCode header, footer, content, SEO, RSS, and analytics customizations. */
|
|
367
|
+
export default function hagilight(options = {}) {
|
|
368
|
+
if (!isRecord(options)) {
|
|
369
|
+
throw new TypeError('Hagilight Starlight options must be an object.');
|
|
370
|
+
}
|
|
371
|
+
const input = options;
|
|
372
|
+
const header = optionalRecord(input.header, 'header');
|
|
373
|
+
const headerEnabled = optionalBoolean(header, 'enabled', 'header enabled') !== false;
|
|
374
|
+
const notFoundPage = optionalRecord(input.notFoundPage, 'notFoundPage');
|
|
375
|
+
const notFoundPageEnabled = optionalBoolean(notFoundPage, 'enabled', 'notFoundPage enabled') !== false;
|
|
376
|
+
const rss = optionalRecord(input.rss, 'rss');
|
|
377
|
+
const rssEnabled = optionalBoolean(rss, 'enabled', 'rss enabled') !== false;
|
|
378
|
+
const rssOptions = resolveRssOptions(rss);
|
|
379
|
+
const analytics = optionalRecord(input.analytics, 'analytics') ?? {};
|
|
380
|
+
const seo = resolveSeoOptions(input.seo);
|
|
381
|
+
const contentComponents = optionalRecord(input.contentComponents, 'contentComponents');
|
|
382
|
+
const themes = optionalRecord(input.themes, 'themes');
|
|
383
|
+
const themesEnabled = optionalBoolean(themes, 'enabled', 'themes enabled') !== false;
|
|
384
|
+
const pageTitleEnabled = optionalBoolean(contentComponents, 'pageTitle', 'contentComponents pageTitle') !== false;
|
|
385
|
+
const markdownContentEnabled = optionalBoolean(contentComponents, 'markdownContent', 'contentComponents markdownContent') !== false;
|
|
386
|
+
const aiDisclosures = resolveAIDisclosures(input.aiDisclosures);
|
|
387
|
+
const hagicodePromotionEnabled = resolveHagicodePromotion(input.hagicodePromotion);
|
|
388
|
+
const hagicodePromotionRandomAvatar = resolveHagicodePromotionRandomAvatar(input.hagicodePromotion);
|
|
389
|
+
const footerOptions = optionalRecord(input.footer, 'footer');
|
|
390
|
+
const footerAvatar = resolveFooterAvatarOptions({
|
|
391
|
+
avatar: optionalBoolean(footerOptions, 'avatar', 'footer avatar'),
|
|
392
|
+
randomAvatar: optionalBoolean(footerOptions, 'randomAvatar', 'footer randomAvatar'),
|
|
393
|
+
});
|
|
394
|
+
const googleAnalyticsMeasurementId = resolveProvider(analytics.googleAnalytics, {
|
|
395
|
+
name: 'Google Analytics',
|
|
396
|
+
idKey: 'measurementId',
|
|
397
|
+
pattern: GOOGLE_ID_PATTERN,
|
|
398
|
+
defaultId: DEFAULT_GOOGLE_ANALYTICS_ID,
|
|
399
|
+
});
|
|
400
|
+
const fiftyOneLaId = resolveProvider(analytics.fiftyOneLa, {
|
|
401
|
+
name: '51LA',
|
|
402
|
+
idKey: 'siteId',
|
|
403
|
+
pattern: FIFTY_ONE_LA_ID_PATTERN,
|
|
404
|
+
defaultId: DEFAULT_51LA_ID,
|
|
405
|
+
});
|
|
406
|
+
const posthog = optionalRecord(analytics.posthog, 'PostHog analytics');
|
|
407
|
+
optionalBoolean(posthog, 'enabled', 'PostHog analytics enabled');
|
|
408
|
+
const posthogApiKey = resolveProvider(analytics.posthog, {
|
|
409
|
+
name: 'PostHog',
|
|
410
|
+
idKey: 'apiKey',
|
|
411
|
+
pattern: POSTHOG_API_KEY_PATTERN,
|
|
412
|
+
defaultId: DEFAULT_POSTHOG_API_KEY,
|
|
413
|
+
});
|
|
414
|
+
const promoto = optionalRecord(input.promoto, 'promoto');
|
|
415
|
+
const promotoEnabled = optionalBoolean(promoto, 'enabled', 'promoto enabled') !== false;
|
|
416
|
+
const contentImages = optionalRecord(input.contentImages, 'contentImages');
|
|
417
|
+
const contentImagesEnabled = optionalBoolean(contentImages, 'enabled', 'contentImages enabled') !== false;
|
|
418
|
+
const instanceId = randomUUID();
|
|
419
|
+
const componentIds = {
|
|
420
|
+
header: headerEnabled
|
|
421
|
+
? `virtual:hagilight-starlight/${instanceId}/Header.astro`
|
|
422
|
+
: undefined,
|
|
423
|
+
hero: notFoundPageEnabled
|
|
424
|
+
? packageFile('NotFoundHero.astro')
|
|
425
|
+
: undefined,
|
|
426
|
+
footer: `virtual:hagilight-starlight/${instanceId}/Footer.astro`,
|
|
427
|
+
pageTitle: pageTitleEnabled ? `virtual:hagilight-starlight/${instanceId}/PageTitle.astro` : undefined,
|
|
428
|
+
markdownContent: markdownContentEnabled
|
|
429
|
+
? `virtual:hagilight-starlight/${instanceId}/MarkdownContent.astro`
|
|
430
|
+
: undefined,
|
|
431
|
+
head: seo.enabled || googleAnalyticsMeasurementId || posthogApiKey
|
|
432
|
+
? `virtual:hagilight-starlight/${instanceId}/Head.astro`
|
|
433
|
+
: undefined,
|
|
434
|
+
themeSelect: themesEnabled ? packageFile('ThemeSelect.astro') : undefined,
|
|
435
|
+
};
|
|
436
|
+
const serializedOptions = {
|
|
437
|
+
promotoEnabled,
|
|
438
|
+
hagicodePromotionEnabled,
|
|
439
|
+
hagicodePromotionRandomAvatar,
|
|
440
|
+
footerAvatar,
|
|
441
|
+
links: options.links ?? {},
|
|
442
|
+
googleAnalyticsMeasurementId,
|
|
443
|
+
fiftyOneLaId,
|
|
444
|
+
posthogApiKey,
|
|
445
|
+
aiDisclosures,
|
|
446
|
+
seoEnabled: seo.enabled,
|
|
447
|
+
seo,
|
|
448
|
+
};
|
|
449
|
+
const unregisterRssOwner = registerStarlightRssOwner(rssEnabled);
|
|
450
|
+
const metadata = resolveSiteMetadataOption(input.metadata);
|
|
451
|
+
const unregisterSiteMetadataOwner = registerStarlightSiteMetadataOwner(metadata.enabled);
|
|
452
|
+
return {
|
|
453
|
+
name: '@hagicode/hagilight-starlight',
|
|
454
|
+
hooks: {
|
|
455
|
+
'config:setup'({ astroConfig, config, updateConfig, addIntegration }) {
|
|
456
|
+
if (headerEnabled && config.components?.Header) {
|
|
457
|
+
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.');
|
|
458
|
+
}
|
|
459
|
+
if (notFoundPageEnabled && config.components?.Hero) {
|
|
460
|
+
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.');
|
|
461
|
+
}
|
|
462
|
+
if (config.components?.Footer) {
|
|
463
|
+
throw new Error('Hagilight cannot replace an existing Starlight Footer override.');
|
|
464
|
+
}
|
|
465
|
+
if (pageTitleEnabled && config.components?.PageTitle) {
|
|
466
|
+
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.');
|
|
467
|
+
}
|
|
468
|
+
if (markdownContentEnabled && config.components?.MarkdownContent) {
|
|
469
|
+
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.');
|
|
470
|
+
}
|
|
471
|
+
if (themesEnabled && config.components?.ThemeSelect) {
|
|
472
|
+
throw new Error('Hagilight cannot replace an existing Starlight ThemeSelect override. Set themes: { enabled: false } to keep it, or compose @hagicode/hagilight-starlight/ThemeSelect into your component.');
|
|
473
|
+
}
|
|
474
|
+
if (componentIds.head && config.components?.Head) {
|
|
475
|
+
if (seo.enabled) {
|
|
476
|
+
throw new Error('Hagilight SEO cannot replace an existing Starlight Head override. Set seo: { enabled: false }, analytics.googleAnalytics: { enabled: false }, and analytics.posthog: { enabled: false } to keep your Head, or compose SEO, @hagicode/hagilight-core/GoogleAnalytics and @hagicode/hagilight-core/PostHog explicitly.');
|
|
477
|
+
}
|
|
478
|
+
throw new Error('Hagilight Google Analytics or PostHog cannot replace an existing Starlight Head override. Set analytics.googleAnalytics: { enabled: false } and analytics.posthog: { enabled: false } to keep it, or compose @hagicode/hagilight-core/GoogleAnalytics and @hagicode/hagilight-core/PostHog in your Head.');
|
|
479
|
+
}
|
|
480
|
+
let seoSite;
|
|
481
|
+
let seoLocales = [];
|
|
482
|
+
let defaultSeoLocale = 'root';
|
|
483
|
+
if (seo.enabled) {
|
|
484
|
+
seoSite = resolveAstroSite(astroConfig.site);
|
|
485
|
+
seoLocales = resolveSeoLocales(config.locales);
|
|
486
|
+
defaultSeoLocale = resolveDefaultSeoLocale(config.defaultLocale, seoLocales);
|
|
487
|
+
}
|
|
488
|
+
const basePath = astroConfig.base ?? '/';
|
|
489
|
+
serializedOptions.seoSite = seoSite;
|
|
490
|
+
serializedOptions.seoBasePath = basePath;
|
|
491
|
+
serializedOptions.seoLocales = seoLocales;
|
|
492
|
+
serializedOptions.seoDefaultLocale = defaultSeoLocale;
|
|
493
|
+
serializedOptions.seoFormat = astroConfig.build?.format ?? 'directory';
|
|
494
|
+
serializedOptions.seoTrailingSlash = astroConfig.trailingSlash ?? 'ignore';
|
|
495
|
+
serializedOptions.seo = seo.enabled
|
|
496
|
+
? {
|
|
497
|
+
...seo,
|
|
498
|
+
...(seo.image === undefined
|
|
499
|
+
? {}
|
|
500
|
+
: { image: resolveSeoImageUrl(seo.image, { site: seoSite, basePath }) }),
|
|
501
|
+
...(seo.organization === undefined
|
|
502
|
+
? {}
|
|
503
|
+
: {
|
|
504
|
+
organization: {
|
|
505
|
+
...seo.organization,
|
|
506
|
+
...(seo.organization.logo === undefined
|
|
507
|
+
? {}
|
|
508
|
+
: { logo: resolveSeoImageUrl(seo.organization.logo, { site: seoSite, basePath }) }),
|
|
509
|
+
},
|
|
510
|
+
}),
|
|
511
|
+
}
|
|
512
|
+
: seo;
|
|
513
|
+
serializedOptions.consumerHead = config.head ?? [];
|
|
514
|
+
const generateRss = rssEnabled && !getRssFeedUrl(config);
|
|
515
|
+
if (generateRss && !astroConfig.site) {
|
|
516
|
+
throw new Error('Hagilight RSS requires the Astro site option. Set site or disable RSS with rss: { enabled: false }.');
|
|
517
|
+
}
|
|
518
|
+
const rssLocales = generateRss ? resolveRssLocales(config.locales) : [];
|
|
519
|
+
const baseUrl = generateRss
|
|
520
|
+
? new URL((astroConfig.base ?? '/').replace(/\/?$/u, '/'), astroConfig.site)
|
|
521
|
+
: undefined;
|
|
522
|
+
const rssFeedUrl = baseUrl ? new URL('rss.xml', baseUrl).toString() : undefined;
|
|
523
|
+
const rssLocaleFeedUrls = baseUrl
|
|
524
|
+
? Object.fromEntries(rssLocales
|
|
525
|
+
.filter(({ filename }) => filename !== 'en')
|
|
526
|
+
.map(({ lang, filename }) => [
|
|
527
|
+
lang,
|
|
528
|
+
new URL(`rss.${filename}.xml`, baseUrl).toString(),
|
|
529
|
+
]))
|
|
530
|
+
: {};
|
|
531
|
+
addIntegration(createConfiguredIntegration(instanceId, serializedOptions, { options: rssOptions, locales: rssLocales }, rssLocaleFeedUrls, componentIds, () => getRssFeedUrl(config) ?? rssFeedUrl, generateRss, unregisterRssOwner, contentImagesEnabled, {
|
|
532
|
+
enabled: metadata.enabled,
|
|
533
|
+
name: metadata.name,
|
|
534
|
+
description: metadata.description,
|
|
535
|
+
title: config.title,
|
|
536
|
+
siteDescription: config.description,
|
|
537
|
+
locales: config.locales,
|
|
538
|
+
defaultLocale: config.defaultLocale,
|
|
539
|
+
site: astroConfig.site,
|
|
540
|
+
base: astroConfig.base,
|
|
541
|
+
publicDir: astroConfig.publicDir,
|
|
542
|
+
analytics: {
|
|
543
|
+
googleAnalytics: serializedOptions.googleAnalyticsMeasurementId !== undefined,
|
|
544
|
+
fiftyOneLa: serializedOptions.fiftyOneLaId !== undefined,
|
|
545
|
+
posthog: serializedOptions.posthogApiKey !== undefined,
|
|
546
|
+
},
|
|
547
|
+
features: { seo: seo.enabled, promoto: promotoEnabled },
|
|
548
|
+
}, unregisterSiteMetadataOwner));
|
|
549
|
+
updateConfig({
|
|
550
|
+
...(config.logo === undefined ? { logo: { src: defaultLogo, alt: 'HagiCode' } } : {}),
|
|
551
|
+
customCss: [
|
|
552
|
+
...(config.customCss ?? []),
|
|
553
|
+
contentWidthCssPath,
|
|
554
|
+
...(themesEnabled ? [themesCssPath] : []),
|
|
555
|
+
],
|
|
556
|
+
head: [
|
|
557
|
+
...(config.head ?? []),
|
|
558
|
+
...(rssFeedUrl ? [{
|
|
559
|
+
tag: 'link',
|
|
560
|
+
attrs: { rel: 'alternate', type: 'application/rss+xml', href: rssFeedUrl },
|
|
561
|
+
}] : []),
|
|
562
|
+
{ tag: 'script', content: contentWidthHeadScript },
|
|
563
|
+
...(themesEnabled ? [{ tag: 'script', content: THEME_BOOTSTRAP }] : []),
|
|
564
|
+
...(() => {
|
|
565
|
+
const entry = resolveFaviconHeadEntry(config.head, config.favicon === undefined ? {} : { href: config.favicon });
|
|
566
|
+
return entry ? [entry] : [];
|
|
567
|
+
})(),
|
|
568
|
+
],
|
|
569
|
+
components: {
|
|
570
|
+
...config.components,
|
|
571
|
+
...(componentIds.header ? { Header: componentIds.header } : {}),
|
|
572
|
+
...(componentIds.hero ? { Hero: componentIds.hero } : {}),
|
|
573
|
+
Footer: componentIds.footer,
|
|
574
|
+
...(componentIds.pageTitle ? { PageTitle: componentIds.pageTitle } : {}),
|
|
575
|
+
...(componentIds.markdownContent ? { MarkdownContent: componentIds.markdownContent } : {}),
|
|
576
|
+
...(componentIds.head ? { Head: componentIds.head } : {}),
|
|
577
|
+
...(componentIds.themeSelect ? { ThemeSelect: componentIds.themeSelect } : {}),
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
@@ -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;
|