@hagicode/hagilight-docusaurus 0.0.0-stage → 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.
@@ -0,0 +1,257 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { join, resolve } from 'node:path';
4
+ import { generateRssFeed, resolveRssLocales } from '@hagicode/hagilight-core/rss';
5
+ import { buildSiteMetadata, SITE_METADATA_PATH, } from '@hagicode/hagilight-core/site-metadata';
6
+ import { findDiscoveryOutputs, writeSiteMetadata } from '@hagicode/hagilight-core/site-metadata-writer';
7
+ const SITEMAP_PLUGIN = 'docusaurus-plugin-sitemap';
8
+ const DOCS_PLUGIN = 'docusaurus-plugin-content-docs';
9
+ const BLOG_PLUGIN = 'docusaurus-plugin-content-blog';
10
+ function notFoundRoute(baseUrl) {
11
+ const escapedBase = baseUrl.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
12
+ return new RegExp(`^${escapedBase}404(?:\\.html)?\\/?$`, 'u');
13
+ }
14
+ function isRecord(value) {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
16
+ }
17
+ function siteOwns(site, fileName) {
18
+ return existsSync(join(site.siteDir, 'static', fileName));
19
+ }
20
+ function escapeXml(value) {
21
+ return value
22
+ .replace(/&/gu, '&')
23
+ .replace(/</gu, '&lt;')
24
+ .replace(/>/gu, '&gt;')
25
+ .replace(/"/gu, '&quot;')
26
+ .replace(/'/gu, '&apos;');
27
+ }
28
+ export function sitemapUrls(routesPaths, routesBuildMetadata, siteUrl, localeBaseUrl) {
29
+ const notFound = notFoundRoute(localeBaseUrl);
30
+ const urls = routesPaths
31
+ .filter((route) => !notFound.test(route) && routesBuildMetadata[route]?.noIndex !== true)
32
+ .map((route) => new URL(route, siteUrl).href);
33
+ return [...new Set(urls)];
34
+ }
35
+ export function sitemapXml(urls) {
36
+ const entries = urls.map((url) => ` <url>\n <loc>${escapeXml(url)}</loc>\n </url>`).join('\n');
37
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries}\n</urlset>\n`;
38
+ }
39
+ export function robotsTxt(sitemapUrlList) {
40
+ return ['User-agent: *', 'Allow: /', '', ...sitemapUrlList.map((url) => `Sitemap: ${url}`), ''].join('\n');
41
+ }
42
+ function localeSitemapUrls(site) {
43
+ return site.i18n.locales.map((locale) => new URL(`${site.i18n.localeConfigs[locale].baseUrl}sitemap.xml`, site.url).href);
44
+ }
45
+ function defaultLocaleOutDir(outDir, i18n) {
46
+ const defaultBase = i18n.localeConfigs[i18n.defaultLocale].baseUrl;
47
+ const currentBase = i18n.localeConfigs[i18n.currentLocale].baseUrl;
48
+ const localeSegments = currentBase.slice(defaultBase.length).split('/').filter(Boolean);
49
+ return resolve(outDir, ...localeSegments.map(() => '..'));
50
+ }
51
+ function instanceContents(allContent, pluginName, report) {
52
+ if (!isRecord(allContent) || allContent[pluginName] === undefined)
53
+ return [];
54
+ const instances = allContent[pluginName];
55
+ if (!isRecord(instances)) {
56
+ report.warn(`${pluginName} content has an unexpected shape, so its feed items are skipped.`);
57
+ return [];
58
+ }
59
+ return Object.values(instances);
60
+ }
61
+ function publicationDate(value) {
62
+ if (value instanceof Date)
63
+ return Number.isNaN(value.getTime()) ? undefined : value;
64
+ if (typeof value === 'number' && Number.isFinite(value))
65
+ return value;
66
+ if (typeof value === 'string' && !Number.isNaN(new Date(value).getTime()))
67
+ return value;
68
+ return undefined;
69
+ }
70
+ function toFeedItem(meta, date, siteUrl) {
71
+ if (typeof meta.title !== 'string' || typeof meta.permalink !== 'string')
72
+ return undefined;
73
+ const description = typeof meta.description === 'string' && meta.description.trim() ? meta.description : undefined;
74
+ const published = publicationDate(date);
75
+ return {
76
+ title: meta.title,
77
+ link: new URL(meta.permalink, siteUrl).href,
78
+ ...(description === undefined ? {} : { description }),
79
+ ...(published === undefined ? {} : { date: published }),
80
+ };
81
+ }
82
+ function isListed(meta) {
83
+ return meta.draft !== true && meta.unlisted !== true;
84
+ }
85
+ function docFeedItems(allContent, site, report) {
86
+ const items = [];
87
+ let malformed = 0;
88
+ for (const content of instanceContents(allContent, DOCS_PLUGIN, report)) {
89
+ const current = isRecord(content) && Array.isArray(content.loadedVersions)
90
+ ? content.loadedVersions.find((version) => isRecord(version) && version.isLast === true)
91
+ : undefined;
92
+ if (!isRecord(current) || !Array.isArray(current.docs)) {
93
+ malformed += 1;
94
+ continue;
95
+ }
96
+ for (const doc of current.docs) {
97
+ if (!isRecord(doc)) {
98
+ malformed += 1;
99
+ continue;
100
+ }
101
+ if (!isListed(doc))
102
+ continue;
103
+ const item = toFeedItem(doc, doc.lastUpdatedAt, site.url);
104
+ if (item === undefined)
105
+ malformed += 1;
106
+ else
107
+ items.push(item);
108
+ }
109
+ }
110
+ if (malformed > 0)
111
+ report.warn(`${malformed} docs entries have an unexpected shape and are left out of the RSS feed.`);
112
+ return items;
113
+ }
114
+ function blogFeedItems(allContent, site, report) {
115
+ const items = [];
116
+ let malformed = 0;
117
+ for (const content of instanceContents(allContent, BLOG_PLUGIN, report)) {
118
+ const posts = isRecord(content) && Array.isArray(content.blogPosts) ? content.blogPosts : undefined;
119
+ if (posts === undefined) {
120
+ malformed += 1;
121
+ continue;
122
+ }
123
+ for (const post of posts) {
124
+ const meta = isRecord(post) && isRecord(post.metadata) ? post.metadata : undefined;
125
+ if (meta === undefined) {
126
+ malformed += 1;
127
+ continue;
128
+ }
129
+ if (!isListed(meta))
130
+ continue;
131
+ const item = toFeedItem(meta, meta.date, site.url);
132
+ if (item === undefined)
133
+ malformed += 1;
134
+ else
135
+ items.push(item);
136
+ }
137
+ }
138
+ if (malformed > 0)
139
+ report.warn(`${malformed} blog entries have an unexpected shape and are left out of the RSS feed.`);
140
+ return items;
141
+ }
142
+ export function collectFeedItems(allContent, site, rss, report) {
143
+ if (!rss.enabled)
144
+ return [];
145
+ return [
146
+ ...(rss.includeBlog ? blogFeedItems(allContent, site, report) : []),
147
+ ...(rss.includeDocs ? docFeedItems(allContent, site, report) : []),
148
+ ];
149
+ }
150
+ export function feedHeadTags(site, rss, feedItemCount) {
151
+ if (!rss.enabled || (feedItemCount === 0 && !siteOwns(site, 'rss.xml')))
152
+ return [];
153
+ return [
154
+ {
155
+ tagName: 'link',
156
+ attributes: {
157
+ rel: 'alternate',
158
+ type: 'application/rss+xml',
159
+ title: site.title,
160
+ href: new URL(`${site.baseUrl}rss.xml`, site.url).href,
161
+ },
162
+ },
163
+ ];
164
+ }
165
+ async function renderFeed(site, items) {
166
+ const locale = site.i18n.currentLocale;
167
+ const htmlLang = site.i18n.localeConfigs[locale].htmlLang;
168
+ const [{ lang }] = resolveRssLocales({ [locale]: { lang: htmlLang } });
169
+ const response = await generateRssFeed({
170
+ site: site.url,
171
+ baseUrl: site.baseUrl,
172
+ language: lang,
173
+ title: site.title,
174
+ description: site.tagline.trim() ? site.tagline : site.title,
175
+ items: [...items],
176
+ });
177
+ return response.text();
178
+ }
179
+ /** A locale's base path relative to the site base, with leading and trailing slashes. */
180
+ function localePathFromBase(localeBaseUrl, siteBaseUrl) {
181
+ const relative = localeBaseUrl.startsWith(siteBaseUrl) ? localeBaseUrl.slice(siteBaseUrl.length) : localeBaseUrl;
182
+ const trimmed = relative.replace(/^\/+|\/+$/gu, '');
183
+ return trimmed ? `/${trimmed}/` : '/';
184
+ }
185
+ function metadataLocales(site) {
186
+ return site.i18n.locales.map((locale) => {
187
+ const config = site.i18n.localeConfigs[locale];
188
+ return { code: locale, lang: config.htmlLang, path: localePathFromBase(config.baseUrl, site.baseUrl) };
189
+ });
190
+ }
191
+ /**
192
+ * Write the site metadata file as the last discovery step, so `sitemap.xml`, `robots.txt`
193
+ * and `rss.xml` are already in the output. Only the default locale build writes it, the same
194
+ * rule as `robots.txt`. Analytics flags are true only for production builds that load the provider.
195
+ */
196
+ async function writeSiteMetadataFile(outputs) {
197
+ const { options, site, outDir, report } = outputs;
198
+ if (!options.metadata.enabled)
199
+ return;
200
+ if (site.i18n.currentLocale !== site.i18n.defaultLocale) {
201
+ if (!existsSync(join(defaultLocaleOutDir(outDir, site.i18n), SITE_METADATA_PATH))) {
202
+ report.warn(`${SITE_METADATA_PATH} is written only by the default locale build (${site.i18n.defaultLocale}); build that locale to include it.`);
203
+ }
204
+ return;
205
+ }
206
+ const production = outputs.production;
207
+ const metadata = buildSiteMetadata({
208
+ generator: { name: '@hagicode/hagilight-docusaurus', version: outputs.generatorVersion },
209
+ site: {
210
+ name: options.metadata.name ?? site.title,
211
+ url: site.url,
212
+ base: site.baseUrl,
213
+ description: options.metadata.description ?? site.tagline,
214
+ },
215
+ locales: metadataLocales(site),
216
+ defaultLocale: site.i18n.defaultLocale,
217
+ build: outputs.buildInfo,
218
+ discovery: findDiscoveryOutputs(outDir),
219
+ analytics: {
220
+ googleAnalytics: production && options.googleAnalyticsId !== undefined,
221
+ fiftyOneLa: production && options.fiftyOneLaId !== undefined,
222
+ posthog: production && options.posthogApiKey !== undefined,
223
+ },
224
+ features: { seo: options.seo.enabled, promoto: options.promoto },
225
+ });
226
+ writeSiteMetadata({
227
+ outDir,
228
+ metadata,
229
+ siteOwnedFiles: [join(site.siteDir, 'static', SITE_METADATA_PATH)],
230
+ logger: report,
231
+ });
232
+ }
233
+ export async function writeDiscoveryOutputs(outputs) {
234
+ const { options, site, outDir, report } = outputs;
235
+ const noIndex = options.noIndex;
236
+ if (options.sitemap && outputs.production && !noIndex && !outputs.plugins.some((plugin) => plugin.name === SITEMAP_PLUGIN)) {
237
+ const urls = sitemapUrls(outputs.routesPaths, outputs.routesBuildMetadata, site.url, site.i18n.localeConfigs[site.i18n.currentLocale].baseUrl);
238
+ await writeFile(join(outDir, 'sitemap.xml'), sitemapXml(urls));
239
+ }
240
+ if (options.robots && !noIndex && !siteOwns(site, 'robots.txt')) {
241
+ if (site.i18n.currentLocale === site.i18n.defaultLocale) {
242
+ await writeFile(join(outDir, 'robots.txt'), robotsTxt(localeSitemapUrls(site)));
243
+ }
244
+ else if (!existsSync(join(defaultLocaleOutDir(outDir, site.i18n), 'robots.txt'))) {
245
+ report.warn(`robots.txt is written only by the default locale build (${site.i18n.defaultLocale}); build that locale to include it.`);
246
+ }
247
+ }
248
+ if (options.rss.enabled && !siteOwns(site, 'rss.xml')) {
249
+ if (outputs.feedItems.length === 0) {
250
+ report.info('No content is available for the RSS feed, so rss.xml is not written.');
251
+ }
252
+ else {
253
+ await writeFile(join(outDir, 'rss.xml'), await renderFeed(site, outputs.feedItems));
254
+ }
255
+ }
256
+ await writeSiteMetadataFile(outputs);
257
+ }
@@ -0,0 +1,9 @@
1
+ import { resolveSiteLinks, type SiteLinksOptions } from '@hagicode/hagilight-core/links';
2
+ export interface FooterLinksInput {
3
+ locale: string;
4
+ baseUrl: string;
5
+ links: SiteLinksOptions;
6
+ rssEnabled: boolean;
7
+ }
8
+ export type FooterLinks = ReturnType<typeof resolveSiteLinks>;
9
+ export declare function resolveFooterLinks({ locale, baseUrl, links, rssEnabled }: FooterLinksInput): FooterLinks;
package/dist/footer.js ADDED
@@ -0,0 +1,12 @@
1
+ import { resolveSiteLinks } from '@hagicode/hagilight-core/links';
2
+ export function resolveFooterLinks({ locale, baseUrl, links, rssEnabled }) {
3
+ const sitemap = links.overrides?.sitemap;
4
+ return resolveSiteLinks(locale, {
5
+ ...links,
6
+ rssFeedUrl: links.rssFeedUrl ?? (rssEnabled ? `${baseUrl}rss.xml` : undefined),
7
+ overrides: {
8
+ ...links.overrides,
9
+ sitemap: { ...sitemap, href: sitemap?.href ?? `${baseUrl}sitemap.xml` },
10
+ },
11
+ });
12
+ }
@@ -0,0 +1,12 @@
1
+ import type { LoadContext } from '@docusaurus/types';
2
+ import type { BuildInfo } from '@hagicode/hagilight-core/build-info';
3
+ import type { ResolvedHagilightOptions } from './options.js';
4
+ export declare const PLUGIN_NAME = "@hagicode/hagilight-docusaurus";
5
+ export interface HagilightGlobalData {
6
+ version: string;
7
+ options: ResolvedHagilightOptions;
8
+ i18n: LoadContext['i18n'];
9
+ buildInfo: BuildInfo;
10
+ /** The footer avatar's random pool, validated at build time; absent when the random option is off. */
11
+ footerAvatarPool?: readonly string[];
12
+ }
@@ -0,0 +1 @@
1
+ export const PLUGIN_NAME = '@hagicode/hagilight-docusaurus';
@@ -0,0 +1,8 @@
1
+ import type { PluginModule } from '@docusaurus/types';
2
+ import { PLUGIN_NAME, type HagilightGlobalData } from './global-data.js';
3
+ import { type HagilightDocusaurusOptions } from './options.js';
4
+ export { PLUGIN_NAME };
5
+ export type { HagilightGlobalData };
6
+ export type { AnalyticsOptions, FeatureToggle, FiftyOneLaOptions, GoogleAnalyticsOptions, MetadataOptions, PostHogOptions, HagilightDocusaurusOptions, ResolvedHagilightOptions, RssOptions, SeoOptions, SeoOrganizationOptions, } from './options.js';
7
+ export declare function hagilightDocusaurus(options?: HagilightDocusaurusOptions): PluginModule;
8
+ export default hagilightDocusaurus;
package/dist/index.js ADDED
@@ -0,0 +1,125 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { resolveBuildInfo } from '@hagicode/hagilight-core/build-info-resolver';
4
+ import footerAvatarManifest from '@hagicode/hagilight-core/assets/footer/manifest.json' with { type: 'json' };
5
+ import { randomPoolFromManifest } from '@hagicode/hagilight-core/random-image';
6
+ import { fiftyOneLaHeadTags, googleAnalyticsHeadTags, posthogHeadTags } from './analytics.js';
7
+ import { collectFeedItems, feedHeadTags, writeDiscoveryOutputs } from './discovery.js';
8
+ import { PLUGIN_NAME } from './global-data.js';
9
+ import { resolveOptions } from './options.js';
10
+ import { seoHeadTags } from './seo.js';
11
+ import { findConfigurationWarnings } from './warnings.js';
12
+ export { PLUGIN_NAME };
13
+ const PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
14
+ function packageFile(relativePath) {
15
+ return fileURLToPath(new URL(relativePath, import.meta.url));
16
+ }
17
+ function analyticsHeadTags(options) {
18
+ if (process.env.NODE_ENV !== 'production')
19
+ return [];
20
+ return [
21
+ ...(options.googleAnalyticsId === undefined ? [] : googleAnalyticsHeadTags(options.googleAnalyticsId)),
22
+ ...(options.fiftyOneLaId === undefined ? [] : fiftyOneLaHeadTags(options.fiftyOneLaId)),
23
+ ...(options.posthogApiKey === undefined ? [] : posthogHeadTags(options.posthogApiKey)),
24
+ ];
25
+ }
26
+ /** Exposes the footer site identifier to the client banner, which tags its call to action with it. */
27
+ function promotoSiteIdHeadTags(options) {
28
+ const siteId = options.links.siteId?.trim();
29
+ if (!options.promoto || !siteId)
30
+ return [];
31
+ return [{ tagName: 'meta', attributes: { name: 'hagilight-site-id', content: siteId } }];
32
+ }
33
+ /** The footer avatar pair in the core package; matched by path so the rule holds in any install layout. */
34
+ const FOOTER_AVATAR_ASSET = /[\\/]footer[\\/]avatar\.(?:avif|webp)$/u;
35
+ function clientModules(options) {
36
+ const analyticsEnabled = options.googleAnalyticsId !== undefined || options.fiftyOneLaId !== undefined;
37
+ const footerAvatarPicker = options.footer && options.footerAvatar.randomAvatar;
38
+ return [
39
+ ...(analyticsEnabled ? [packageFile('./client/analytics.js')] : []),
40
+ ...(options.promoto ? [packageFile('./client/promoto.js'), packageFile('../promoto.css')] : []),
41
+ ...(options.footer ? [packageFile('../styles.css')] : []),
42
+ ...(footerAvatarPicker ? [packageFile('./client/random-image.js')] : []),
43
+ ];
44
+ }
45
+ function createPlugin(context, options) {
46
+ for (const warning of findConfigurationWarnings(context, options)) {
47
+ console.warn(`[${PLUGIN_NAME}] ${warning}`);
48
+ }
49
+ const globalData = {
50
+ version: PACKAGE_VERSION,
51
+ options,
52
+ i18n: context.i18n,
53
+ buildInfo: resolveBuildInfo({ root: context.siteDir }),
54
+ footerAvatarPool: options.footer && options.footerAvatar.randomAvatar
55
+ ? randomPoolFromManifest(footerAvatarManifest.pool)
56
+ : undefined,
57
+ };
58
+ const themeEnabled = options.footer || options.seo.enabled;
59
+ const site = {
60
+ title: context.siteConfig.title,
61
+ tagline: context.siteConfig.tagline,
62
+ url: context.siteConfig.url,
63
+ baseUrl: context.siteConfig.baseUrl,
64
+ siteDir: context.siteDir,
65
+ i18n: context.i18n,
66
+ };
67
+ const report = {
68
+ warn: (message) => console.warn(`[${PLUGIN_NAME}] ${message}`),
69
+ info: (message) => console.info(`[${PLUGIN_NAME}] ${message}`),
70
+ };
71
+ let feedItems = [];
72
+ return {
73
+ name: PLUGIN_NAME,
74
+ ...(themeEnabled ? { getThemePath: () => packageFile('./theme') } : {}),
75
+ getClientModules: () => clientModules(options),
76
+ configureWebpack(config) {
77
+ // Docusaurus inlines images under 10 000 bytes as data URIs in every page. The footer avatar is a
78
+ // shared asset, so its two files are emitted as ordinary images instead, once per build.
79
+ const rules = config.module?.rules ?? [];
80
+ for (const rule of rules) {
81
+ const candidate = rule;
82
+ if (candidate.test instanceof RegExp && candidate.use && candidate.test.test('avatar.webp')) {
83
+ candidate.exclude = FOOTER_AVATAR_ASSET;
84
+ }
85
+ }
86
+ rules.push({ test: FOOTER_AVATAR_ASSET, type: 'asset/resource', generator: { filename: 'assets/images/hagilight-footer-[name]-[contenthash:8][ext]' } });
87
+ },
88
+ contentLoaded({ actions }) {
89
+ actions.setGlobalData(globalData);
90
+ },
91
+ allContentLoaded({ allContent }) {
92
+ feedItems = collectFeedItems(allContent, site, options.rss, report);
93
+ },
94
+ async postBuild(props) {
95
+ await writeDiscoveryOutputs({
96
+ options,
97
+ site,
98
+ generatorVersion: PACKAGE_VERSION,
99
+ buildInfo: globalData.buildInfo,
100
+ outDir: props.outDir,
101
+ production: process.env.NODE_ENV === 'production',
102
+ plugins: props.plugins,
103
+ routesPaths: props.routesPaths,
104
+ routesBuildMetadata: props.routesBuildMetadata,
105
+ feedItems,
106
+ report,
107
+ });
108
+ },
109
+ injectHtmlTags() {
110
+ return {
111
+ headTags: [
112
+ ...analyticsHeadTags(options),
113
+ ...seoHeadTags(options.seo, site),
114
+ ...feedHeadTags(site, options.rss, feedItems.length),
115
+ ...promotoSiteIdHeadTags(options),
116
+ ],
117
+ };
118
+ },
119
+ };
120
+ }
121
+ export function hagilightDocusaurus(options) {
122
+ const resolved = resolveOptions(options);
123
+ return (context) => createPlugin(context, resolved);
124
+ }
125
+ export default hagilightDocusaurus;
@@ -0,0 +1,5 @@
1
+ import type { I18nConfig } from '@docusaurus/types';
2
+ export declare const defaultLocale = "en-US";
3
+ export declare const localeConfigs: NonNullable<I18nConfig['localeConfigs']>;
4
+ export declare const locales: [string, ...string[]];
5
+ export declare const i18n: I18nConfig;
@@ -0,0 +1,20 @@
1
+ export const defaultLocale = 'en-US';
2
+ export const localeConfigs = {
3
+ 'en-US': { label: 'English', htmlLang: 'en-US', direction: 'ltr' },
4
+ 'zh-CN': { label: '简体中文', htmlLang: 'zh-CN', direction: 'ltr' },
5
+ 'zh-Hant': { label: '繁體中文', htmlLang: 'zh-Hant', direction: 'ltr' },
6
+ 'fr-FR': { label: 'Français', htmlLang: 'fr-FR', direction: 'ltr' },
7
+ 'de-DE': { label: 'Deutsch', htmlLang: 'de-DE', direction: 'ltr' },
8
+ 'es-ES': { label: 'Español (España)', htmlLang: 'es-ES', direction: 'ltr' },
9
+ 'ja-JP': { label: '日本語', htmlLang: 'ja-JP', direction: 'ltr' },
10
+ 'ko-KR': { label: '한국어', htmlLang: 'ko-KR', direction: 'ltr' },
11
+ 'pt-BR': { label: 'Português (Brasil)', htmlLang: 'pt-BR', direction: 'ltr' },
12
+ 'ru-RU': { label: 'Русский', htmlLang: 'ru-RU', direction: 'ltr' },
13
+ };
14
+ export const locales = [defaultLocale, ...Object.keys(localeConfigs).filter((locale) => locale !== defaultLocale)];
15
+ export const i18n = {
16
+ defaultLocale,
17
+ locales,
18
+ path: 'i18n',
19
+ localeConfigs,
20
+ };
@@ -0,0 +1,90 @@
1
+ import type { SiteLinksOptions } from '@hagicode/hagilight-core/links';
2
+ import { type ResolvedFooterAvatarOptions } from '@hagicode/hagilight-core/footer-avatar';
3
+ import { type SiteMetadataOption } from '@hagicode/hagilight-core/site-metadata';
4
+ export interface FeatureToggle {
5
+ enabled?: boolean;
6
+ }
7
+ export interface GoogleAnalyticsOptions extends FeatureToggle {
8
+ measurementId?: string;
9
+ }
10
+ export interface FiftyOneLaOptions extends FeatureToggle {
11
+ siteId?: string;
12
+ }
13
+ export interface AnalyticsOptions {
14
+ googleAnalytics?: GoogleAnalyticsOptions;
15
+ fiftyOneLa?: FiftyOneLaOptions;
16
+ posthog?: PostHogOptions;
17
+ }
18
+ export interface PostHogOptions extends FeatureToggle {
19
+ apiKey?: string;
20
+ }
21
+ export interface SeoOrganizationOptions {
22
+ name?: string;
23
+ url?: string;
24
+ logo?: string;
25
+ }
26
+ export interface SeoOptions extends FeatureToggle {
27
+ image?: string;
28
+ organization?: SeoOrganizationOptions;
29
+ }
30
+ export interface RssOptions extends FeatureToggle {
31
+ includeDocs?: boolean;
32
+ includeBlog?: boolean;
33
+ }
34
+ export interface MetadataOptions extends FeatureToggle {
35
+ /** Site name. Defaults to `siteConfig.title`. */
36
+ name?: string;
37
+ /** Site description. Defaults to `siteConfig.tagline`. */
38
+ description?: string;
39
+ }
40
+ export interface FooterOptions extends FeatureToggle {
41
+ /** The decorative avatar in the brand block. Defaults to `true`; `false` removes it. */
42
+ avatar?: boolean;
43
+ /** Swap the avatar for one random pool entry per page view. Defaults to `true`. */
44
+ randomAvatar?: boolean;
45
+ }
46
+ export interface HagilightDocusaurusOptions {
47
+ /**
48
+ * Write `.well-known/hagilight.metadata.json` at the end of the build. Defaults to `true`;
49
+ * `false` opts out, and an object may set `name` and `description`. A site-owned file in
50
+ * `static/.well-known/` is kept. Only the default locale build writes the file.
51
+ */
52
+ metadata?: boolean | MetadataOptions;
53
+ footer?: FooterOptions;
54
+ links?: SiteLinksOptions;
55
+ analytics?: AnalyticsOptions;
56
+ promoto?: FeatureToggle;
57
+ languageSelector?: FeatureToggle;
58
+ seo?: SeoOptions;
59
+ sitemap?: FeatureToggle;
60
+ robots?: FeatureToggle;
61
+ rss?: RssOptions;
62
+ noIndex?: boolean;
63
+ }
64
+ export interface ResolvedSeoOptions {
65
+ enabled: boolean;
66
+ image: string | undefined;
67
+ organization: SeoOrganizationOptions;
68
+ }
69
+ export interface ResolvedRssOptions {
70
+ enabled: boolean;
71
+ includeDocs: boolean;
72
+ includeBlog: boolean;
73
+ }
74
+ export interface ResolvedHagilightOptions {
75
+ footer: boolean;
76
+ footerAvatar: ResolvedFooterAvatarOptions;
77
+ links: SiteLinksOptions;
78
+ googleAnalyticsId: string | undefined;
79
+ fiftyOneLaId: string | undefined;
80
+ posthogApiKey: string | undefined;
81
+ promoto: boolean;
82
+ languageSelector: boolean;
83
+ seo: ResolvedSeoOptions;
84
+ sitemap: boolean;
85
+ robots: boolean;
86
+ rss: ResolvedRssOptions;
87
+ noIndex: boolean;
88
+ metadata: SiteMetadataOption;
89
+ }
90
+ export declare function resolveOptions(input: HagilightDocusaurusOptions | undefined): ResolvedHagilightOptions;
@@ -0,0 +1,116 @@
1
+ import { resolveFooterAvatarOptions } from '@hagicode/hagilight-core/footer-avatar';
2
+ import { DEFAULT_POSTHOG_API_KEY, POSTHOG_API_KEY_PATTERN } from '@hagicode/hagilight-core/posthog';
3
+ import { isValidSeoImageReference, resolveAbsoluteHttpUrl } from '@hagicode/hagilight-core/seo';
4
+ import { resolveSiteMetadataOption } from '@hagicode/hagilight-core/site-metadata';
5
+ import { DEFAULT_FIFTY_ONE_LA_ID, DEFAULT_GOOGLE_ANALYTICS_ID, FIFTY_ONE_LA_ID_PATTERN, GOOGLE_ANALYTICS_ID_PATTERN, } from './analytics.js';
6
+ function invalid(path, reason) {
7
+ return new TypeError(`@hagicode/hagilight-docusaurus option "${path}" ${reason}.`);
8
+ }
9
+ function objectOption(value, path) {
10
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
11
+ throw invalid(path, 'must be an object');
12
+ }
13
+ return value;
14
+ }
15
+ function booleanOption(value, path) {
16
+ if (value === undefined)
17
+ return undefined;
18
+ if (typeof value !== 'boolean')
19
+ throw invalid(path, 'must be a boolean');
20
+ return value;
21
+ }
22
+ function nonEmptyStringOption(value, path) {
23
+ if (value === undefined)
24
+ return undefined;
25
+ if (typeof value !== 'string' || !value.trim())
26
+ throw invalid(path, 'must be a non-empty string');
27
+ return value;
28
+ }
29
+ function resolveFooterAvatar(value) {
30
+ if (value === undefined)
31
+ return resolveFooterAvatarOptions();
32
+ const group = objectOption(value, 'footer');
33
+ return resolveFooterAvatarOptions({
34
+ avatar: booleanOption(group.avatar, 'footer.avatar'),
35
+ randomAvatar: booleanOption(group.randomAvatar, 'footer.randomAvatar'),
36
+ });
37
+ }
38
+ function featureEnabled(value, path) {
39
+ if (value === undefined)
40
+ return true;
41
+ return booleanOption(objectOption(value, path).enabled, `${path}.enabled`) ?? true;
42
+ }
43
+ function resolveProvider(value, path, idKey, pattern, defaultId) {
44
+ if (value === undefined)
45
+ return defaultId;
46
+ const group = objectOption(value, path);
47
+ const enabled = booleanOption(group.enabled, `${path}.enabled`) ?? Boolean(group[idKey]);
48
+ if (!enabled)
49
+ return undefined;
50
+ const id = group[idKey];
51
+ const idPath = `${path}.${idKey}`;
52
+ if (typeof id !== 'string' || !id.trim()) {
53
+ throw invalid(idPath, 'is required while the provider is enabled');
54
+ }
55
+ if (!pattern.test(id)) {
56
+ throw invalid(idPath, 'has an invalid format');
57
+ }
58
+ return id;
59
+ }
60
+ function absoluteHttpUrlOption(value, path) {
61
+ try {
62
+ return resolveAbsoluteHttpUrl(value, path).href;
63
+ }
64
+ catch {
65
+ throw invalid(path, 'must be an absolute HTTP(S) URL');
66
+ }
67
+ }
68
+ function resolveSeo(value) {
69
+ const group = value === undefined ? {} : objectOption(value, 'seo');
70
+ const enabled = booleanOption(group.enabled, 'seo.enabled') ?? true;
71
+ const image = nonEmptyStringOption(group.image, 'seo.image');
72
+ if (image !== undefined && !isValidSeoImageReference(image)) {
73
+ throw invalid('seo.image', 'must be an absolute HTTP(S) URL or a site-root path');
74
+ }
75
+ const organization = {};
76
+ if (group.organization !== undefined) {
77
+ const org = objectOption(group.organization, 'seo.organization');
78
+ const name = nonEmptyStringOption(org.name, 'seo.organization.name');
79
+ if (name !== undefined)
80
+ organization.name = name;
81
+ if (org.url !== undefined)
82
+ organization.url = absoluteHttpUrlOption(org.url, 'seo.organization.url');
83
+ const logo = nonEmptyStringOption(org.logo, 'seo.organization.logo');
84
+ if (logo !== undefined)
85
+ organization.logo = absoluteHttpUrlOption(logo, 'seo.organization.logo');
86
+ }
87
+ return { enabled, image, organization };
88
+ }
89
+ function resolveRss(value) {
90
+ const group = value === undefined ? {} : objectOption(value, 'rss');
91
+ return {
92
+ enabled: booleanOption(group.enabled, 'rss.enabled') ?? true,
93
+ includeDocs: booleanOption(group.includeDocs, 'rss.includeDocs') ?? true,
94
+ includeBlog: booleanOption(group.includeBlog, 'rss.includeBlog') ?? true,
95
+ };
96
+ }
97
+ export function resolveOptions(input) {
98
+ const options = input === undefined ? {} : objectOption(input, 'options');
99
+ const analytics = options.analytics === undefined ? {} : objectOption(options.analytics, 'analytics');
100
+ return {
101
+ footer: featureEnabled(options.footer, 'footer'),
102
+ footerAvatar: resolveFooterAvatar(options.footer),
103
+ links: options.links === undefined ? {} : objectOption(options.links, 'links'),
104
+ googleAnalyticsId: resolveProvider(analytics.googleAnalytics, 'analytics.googleAnalytics', 'measurementId', GOOGLE_ANALYTICS_ID_PATTERN, DEFAULT_GOOGLE_ANALYTICS_ID),
105
+ fiftyOneLaId: resolveProvider(analytics.fiftyOneLa, 'analytics.fiftyOneLa', 'siteId', FIFTY_ONE_LA_ID_PATTERN, DEFAULT_FIFTY_ONE_LA_ID),
106
+ posthogApiKey: resolveProvider(analytics.posthog, 'analytics.posthog', 'apiKey', POSTHOG_API_KEY_PATTERN, DEFAULT_POSTHOG_API_KEY),
107
+ promoto: featureEnabled(options.promoto, 'promoto'),
108
+ languageSelector: featureEnabled(options.languageSelector, 'languageSelector'),
109
+ seo: resolveSeo(options.seo),
110
+ sitemap: featureEnabled(options.sitemap, 'sitemap'),
111
+ robots: featureEnabled(options.robots, 'robots'),
112
+ rss: resolveRss(options.rss),
113
+ noIndex: booleanOption(options.noIndex, 'noIndex') ?? false,
114
+ metadata: resolveSiteMetadataOption(options.metadata),
115
+ };
116
+ }