@hagicode/hagilight-docusaurus 0.0.0-stage → 0.1.0
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/README.md +132 -2
- package/dist/analytics.d.ts +7 -0
- package/dist/analytics.js +33 -0
- package/dist/client/analytics.d.ts +23 -0
- package/dist/client/analytics.js +33 -0
- package/dist/client/promoto-markup.d.ts +1 -0
- package/dist/client/promoto-markup.js +24 -0
- package/dist/client/promoto.d.ts +2 -0
- package/dist/client/promoto.js +23 -0
- package/dist/discovery.d.ts +38 -0
- package/dist/discovery.js +200 -0
- package/dist/footer.d.ts +9 -0
- package/dist/footer.js +12 -0
- package/dist/global-data.d.ts +8 -0
- package/dist/global-data.js +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +91 -0
- package/dist/locales.d.ts +5 -0
- package/dist/locales.js +20 -0
- package/dist/options.d.ts +63 -0
- package/dist/options.js +101 -0
- package/dist/seo.d.ts +14 -0
- package/dist/seo.js +32 -0
- package/dist/theme/Footer/FooterView.d.ts +8 -0
- package/dist/theme/Footer/FooterView.js +9 -0
- package/dist/theme/Footer/index.d.ts +2 -0
- package/dist/theme/Footer/index.js +20 -0
- package/dist/theme/SiteMetadata/SharingMetadata.d.ts +8 -0
- package/dist/theme/SiteMetadata/SharingMetadata.js +4 -0
- package/dist/theme/SiteMetadata/index.d.ts +2 -0
- package/dist/theme/SiteMetadata/index.js +15 -0
- package/dist/warnings.d.ts +4 -0
- package/dist/warnings.js +70 -0
- package/package.json +66 -4
- package/promoto.css +396 -0
- package/styles.css +83 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { fiftyOneLaHeadTags, googleAnalyticsHeadTags } from './analytics.js';
|
|
4
|
+
import { collectFeedItems, feedHeadTags, writeDiscoveryOutputs } from './discovery.js';
|
|
5
|
+
import { PLUGIN_NAME } from './global-data.js';
|
|
6
|
+
import { resolveOptions } from './options.js';
|
|
7
|
+
import { seoHeadTags } from './seo.js';
|
|
8
|
+
import { findConfigurationWarnings } from './warnings.js';
|
|
9
|
+
export { PLUGIN_NAME };
|
|
10
|
+
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
11
|
+
function packageFile(relativePath) {
|
|
12
|
+
return fileURLToPath(new URL(relativePath, import.meta.url));
|
|
13
|
+
}
|
|
14
|
+
function analyticsHeadTags(options) {
|
|
15
|
+
if (process.env.NODE_ENV !== 'production')
|
|
16
|
+
return [];
|
|
17
|
+
return [
|
|
18
|
+
...(options.googleAnalyticsId === undefined ? [] : googleAnalyticsHeadTags(options.googleAnalyticsId)),
|
|
19
|
+
...(options.fiftyOneLaId === undefined ? [] : fiftyOneLaHeadTags(options.fiftyOneLaId)),
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
function clientModules(options) {
|
|
23
|
+
const analyticsEnabled = options.googleAnalyticsId !== undefined || options.fiftyOneLaId !== undefined;
|
|
24
|
+
return [
|
|
25
|
+
...(analyticsEnabled ? [packageFile('./client/analytics.js')] : []),
|
|
26
|
+
...(options.promoto ? [packageFile('./client/promoto.js'), packageFile('../promoto.css')] : []),
|
|
27
|
+
...(options.footer ? [packageFile('../styles.css')] : []),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
function createPlugin(context, options) {
|
|
31
|
+
for (const warning of findConfigurationWarnings(context, options)) {
|
|
32
|
+
console.warn(`[${PLUGIN_NAME}] ${warning}`);
|
|
33
|
+
}
|
|
34
|
+
const globalData = {
|
|
35
|
+
version: PACKAGE_VERSION,
|
|
36
|
+
options,
|
|
37
|
+
i18n: context.i18n,
|
|
38
|
+
};
|
|
39
|
+
const themeEnabled = options.footer || options.seo.enabled;
|
|
40
|
+
const site = {
|
|
41
|
+
title: context.siteConfig.title,
|
|
42
|
+
tagline: context.siteConfig.tagline,
|
|
43
|
+
url: context.siteConfig.url,
|
|
44
|
+
baseUrl: context.siteConfig.baseUrl,
|
|
45
|
+
siteDir: context.siteDir,
|
|
46
|
+
i18n: context.i18n,
|
|
47
|
+
};
|
|
48
|
+
const report = {
|
|
49
|
+
warn: (message) => console.warn(`[${PLUGIN_NAME}] ${message}`),
|
|
50
|
+
info: (message) => console.info(`[${PLUGIN_NAME}] ${message}`),
|
|
51
|
+
};
|
|
52
|
+
let feedItems = [];
|
|
53
|
+
return {
|
|
54
|
+
name: PLUGIN_NAME,
|
|
55
|
+
...(themeEnabled ? { getThemePath: () => packageFile('./theme') } : {}),
|
|
56
|
+
getClientModules: () => clientModules(options),
|
|
57
|
+
contentLoaded({ actions }) {
|
|
58
|
+
actions.setGlobalData(globalData);
|
|
59
|
+
},
|
|
60
|
+
allContentLoaded({ allContent }) {
|
|
61
|
+
feedItems = collectFeedItems(allContent, site, options.rss, report);
|
|
62
|
+
},
|
|
63
|
+
async postBuild(props) {
|
|
64
|
+
await writeDiscoveryOutputs({
|
|
65
|
+
options,
|
|
66
|
+
site,
|
|
67
|
+
outDir: props.outDir,
|
|
68
|
+
production: process.env.NODE_ENV === 'production',
|
|
69
|
+
plugins: props.plugins,
|
|
70
|
+
routesPaths: props.routesPaths,
|
|
71
|
+
routesBuildMetadata: props.routesBuildMetadata,
|
|
72
|
+
feedItems,
|
|
73
|
+
report,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
injectHtmlTags() {
|
|
77
|
+
return {
|
|
78
|
+
headTags: [
|
|
79
|
+
...analyticsHeadTags(options),
|
|
80
|
+
...seoHeadTags(options.seo, site),
|
|
81
|
+
...feedHeadTags(site, options.rss, feedItems.length),
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function hagilightDocusaurus(options) {
|
|
88
|
+
const resolved = resolveOptions(options);
|
|
89
|
+
return (context) => createPlugin(context, resolved);
|
|
90
|
+
}
|
|
91
|
+
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;
|
package/dist/locales.js
ADDED
|
@@ -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,63 @@
|
|
|
1
|
+
import type { SiteLinksOptions } from '@hagicode/hagilight-core/links';
|
|
2
|
+
export interface FeatureToggle {
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface GoogleAnalyticsOptions extends FeatureToggle {
|
|
6
|
+
measurementId?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FiftyOneLaOptions extends FeatureToggle {
|
|
9
|
+
siteId?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AnalyticsOptions {
|
|
12
|
+
googleAnalytics?: GoogleAnalyticsOptions;
|
|
13
|
+
fiftyOneLa?: FiftyOneLaOptions;
|
|
14
|
+
}
|
|
15
|
+
export interface SeoOrganizationOptions {
|
|
16
|
+
name?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
logo?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SeoOptions extends FeatureToggle {
|
|
21
|
+
image?: string;
|
|
22
|
+
organization?: SeoOrganizationOptions;
|
|
23
|
+
}
|
|
24
|
+
export interface RssOptions extends FeatureToggle {
|
|
25
|
+
includeDocs?: boolean;
|
|
26
|
+
includeBlog?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface HagilightDocusaurusOptions {
|
|
29
|
+
footer?: FeatureToggle;
|
|
30
|
+
links?: SiteLinksOptions;
|
|
31
|
+
analytics?: AnalyticsOptions;
|
|
32
|
+
promoto?: FeatureToggle;
|
|
33
|
+
languageSelector?: FeatureToggle;
|
|
34
|
+
seo?: SeoOptions;
|
|
35
|
+
sitemap?: FeatureToggle;
|
|
36
|
+
robots?: FeatureToggle;
|
|
37
|
+
rss?: RssOptions;
|
|
38
|
+
noIndex?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface ResolvedSeoOptions {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
image: string | undefined;
|
|
43
|
+
organization: SeoOrganizationOptions;
|
|
44
|
+
}
|
|
45
|
+
export interface ResolvedRssOptions {
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
includeDocs: boolean;
|
|
48
|
+
includeBlog: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface ResolvedHagilightOptions {
|
|
51
|
+
footer: boolean;
|
|
52
|
+
links: SiteLinksOptions;
|
|
53
|
+
googleAnalyticsId: string | undefined;
|
|
54
|
+
fiftyOneLaId: string | undefined;
|
|
55
|
+
promoto: boolean;
|
|
56
|
+
languageSelector: boolean;
|
|
57
|
+
seo: ResolvedSeoOptions;
|
|
58
|
+
sitemap: boolean;
|
|
59
|
+
robots: boolean;
|
|
60
|
+
rss: ResolvedRssOptions;
|
|
61
|
+
noIndex: boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare function resolveOptions(input: HagilightDocusaurusOptions | undefined): ResolvedHagilightOptions;
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isValidSeoImageReference, resolveAbsoluteHttpUrl } from '@hagicode/hagilight-core/seo';
|
|
2
|
+
import { DEFAULT_FIFTY_ONE_LA_ID, DEFAULT_GOOGLE_ANALYTICS_ID, FIFTY_ONE_LA_ID_PATTERN, GOOGLE_ANALYTICS_ID_PATTERN, } from './analytics.js';
|
|
3
|
+
function invalid(path, reason) {
|
|
4
|
+
return new TypeError(`@hagicode/hagilight-docusaurus option "${path}" ${reason}.`);
|
|
5
|
+
}
|
|
6
|
+
function objectOption(value, path) {
|
|
7
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
8
|
+
throw invalid(path, 'must be an object');
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function booleanOption(value, path) {
|
|
13
|
+
if (value === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (typeof value !== 'boolean')
|
|
16
|
+
throw invalid(path, 'must be a boolean');
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function nonEmptyStringOption(value, path) {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (typeof value !== 'string' || !value.trim())
|
|
23
|
+
throw invalid(path, 'must be a non-empty string');
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function featureEnabled(value, path) {
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return true;
|
|
29
|
+
return booleanOption(objectOption(value, path).enabled, `${path}.enabled`) ?? true;
|
|
30
|
+
}
|
|
31
|
+
function resolveProvider(value, path, idKey, pattern, defaultId) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return defaultId;
|
|
34
|
+
const group = objectOption(value, path);
|
|
35
|
+
const enabled = booleanOption(group.enabled, `${path}.enabled`) ?? Boolean(group[idKey]);
|
|
36
|
+
if (!enabled)
|
|
37
|
+
return undefined;
|
|
38
|
+
const id = group[idKey];
|
|
39
|
+
const idPath = `${path}.${idKey}`;
|
|
40
|
+
if (typeof id !== 'string' || !id.trim()) {
|
|
41
|
+
throw invalid(idPath, 'is required while the provider is enabled');
|
|
42
|
+
}
|
|
43
|
+
if (!pattern.test(id)) {
|
|
44
|
+
throw invalid(idPath, 'has an invalid format');
|
|
45
|
+
}
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
function absoluteHttpUrlOption(value, path) {
|
|
49
|
+
try {
|
|
50
|
+
return resolveAbsoluteHttpUrl(value, path).href;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw invalid(path, 'must be an absolute HTTP(S) URL');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function resolveSeo(value) {
|
|
57
|
+
const group = value === undefined ? {} : objectOption(value, 'seo');
|
|
58
|
+
const enabled = booleanOption(group.enabled, 'seo.enabled') ?? true;
|
|
59
|
+
const image = nonEmptyStringOption(group.image, 'seo.image');
|
|
60
|
+
if (image !== undefined && !isValidSeoImageReference(image)) {
|
|
61
|
+
throw invalid('seo.image', 'must be an absolute HTTP(S) URL or a site-root path');
|
|
62
|
+
}
|
|
63
|
+
const organization = {};
|
|
64
|
+
if (group.organization !== undefined) {
|
|
65
|
+
const org = objectOption(group.organization, 'seo.organization');
|
|
66
|
+
const name = nonEmptyStringOption(org.name, 'seo.organization.name');
|
|
67
|
+
if (name !== undefined)
|
|
68
|
+
organization.name = name;
|
|
69
|
+
if (org.url !== undefined)
|
|
70
|
+
organization.url = absoluteHttpUrlOption(org.url, 'seo.organization.url');
|
|
71
|
+
const logo = nonEmptyStringOption(org.logo, 'seo.organization.logo');
|
|
72
|
+
if (logo !== undefined)
|
|
73
|
+
organization.logo = absoluteHttpUrlOption(logo, 'seo.organization.logo');
|
|
74
|
+
}
|
|
75
|
+
return { enabled, image, organization };
|
|
76
|
+
}
|
|
77
|
+
function resolveRss(value) {
|
|
78
|
+
const group = value === undefined ? {} : objectOption(value, 'rss');
|
|
79
|
+
return {
|
|
80
|
+
enabled: booleanOption(group.enabled, 'rss.enabled') ?? true,
|
|
81
|
+
includeDocs: booleanOption(group.includeDocs, 'rss.includeDocs') ?? true,
|
|
82
|
+
includeBlog: booleanOption(group.includeBlog, 'rss.includeBlog') ?? true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function resolveOptions(input) {
|
|
86
|
+
const options = input === undefined ? {} : objectOption(input, 'options');
|
|
87
|
+
const analytics = options.analytics === undefined ? {} : objectOption(options.analytics, 'analytics');
|
|
88
|
+
return {
|
|
89
|
+
footer: featureEnabled(options.footer, 'footer'),
|
|
90
|
+
links: options.links === undefined ? {} : objectOption(options.links, 'links'),
|
|
91
|
+
googleAnalyticsId: resolveProvider(analytics.googleAnalytics, 'analytics.googleAnalytics', 'measurementId', GOOGLE_ANALYTICS_ID_PATTERN, DEFAULT_GOOGLE_ANALYTICS_ID),
|
|
92
|
+
fiftyOneLaId: resolveProvider(analytics.fiftyOneLa, 'analytics.fiftyOneLa', 'siteId', FIFTY_ONE_LA_ID_PATTERN, DEFAULT_FIFTY_ONE_LA_ID),
|
|
93
|
+
promoto: featureEnabled(options.promoto, 'promoto'),
|
|
94
|
+
languageSelector: featureEnabled(options.languageSelector, 'languageSelector'),
|
|
95
|
+
seo: resolveSeo(options.seo),
|
|
96
|
+
sitemap: featureEnabled(options.sitemap, 'sitemap'),
|
|
97
|
+
robots: featureEnabled(options.robots, 'robots'),
|
|
98
|
+
rss: resolveRss(options.rss),
|
|
99
|
+
noIndex: booleanOption(options.noIndex, 'noIndex') ?? false,
|
|
100
|
+
};
|
|
101
|
+
}
|
package/dist/seo.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HtmlTagObject } from '@docusaurus/types';
|
|
2
|
+
import type { ResolvedSeoOptions } from './options.js';
|
|
3
|
+
export interface SeoSite {
|
|
4
|
+
title: string;
|
|
5
|
+
url: string;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveSharingImage(image: string | undefined, site: Pick<SeoSite, 'url' | 'baseUrl'>): string | undefined;
|
|
9
|
+
export interface SharingMetadataValues {
|
|
10
|
+
siteName: string;
|
|
11
|
+
image: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare function sharingMetadataFor(seo: ResolvedSeoOptions, site: Pick<SeoSite, 'title' | 'url' | 'baseUrl'>, themeImage: string | undefined): SharingMetadataValues | undefined;
|
|
14
|
+
export declare function seoHeadTags(seo: ResolvedSeoOptions, site: SeoSite): HtmlTagObject[];
|
package/dist/seo.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { buildOrganizationStructuredData, resolveSeoImageUrl, serializeJsonLd } from '@hagicode/hagilight-core/seo';
|
|
2
|
+
export function resolveSharingImage(image, site) {
|
|
3
|
+
return image === undefined ? undefined : resolveSeoImageUrl(image, { site: site.url, basePath: site.baseUrl });
|
|
4
|
+
}
|
|
5
|
+
export function sharingMetadataFor(seo, site, themeImage) {
|
|
6
|
+
if (!seo.enabled)
|
|
7
|
+
return undefined;
|
|
8
|
+
return {
|
|
9
|
+
siteName: site.title,
|
|
10
|
+
image: themeImage === undefined ? resolveSharingImage(seo.image, site) : undefined,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function seoHeadTags(seo, site) {
|
|
14
|
+
if (!seo.enabled)
|
|
15
|
+
return [];
|
|
16
|
+
const organization = buildOrganizationStructuredData({
|
|
17
|
+
name: seo.organization.name ?? site.title,
|
|
18
|
+
url: seo.organization.url ?? site.url,
|
|
19
|
+
...(seo.organization.logo === undefined ? {} : { logo: seo.organization.logo }),
|
|
20
|
+
});
|
|
21
|
+
const website = {
|
|
22
|
+
'@context': 'https://schema.org',
|
|
23
|
+
'@type': 'WebSite',
|
|
24
|
+
name: site.title,
|
|
25
|
+
url: new URL(site.baseUrl, site.url).href,
|
|
26
|
+
};
|
|
27
|
+
return [organization, website].map((data) => ({
|
|
28
|
+
tagName: 'script',
|
|
29
|
+
attributes: { type: 'application/ld+json' },
|
|
30
|
+
innerHTML: serializeJsonLd(data),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import type { FooterLinks } from '../../footer.js';
|
|
3
|
+
export interface FooterViewProps {
|
|
4
|
+
links: FooterLinks;
|
|
5
|
+
version: string;
|
|
6
|
+
year: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function FooterView({ links, version, year }: FooterViewProps): ReactElement;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { siteLinkGaAttributes } from '@hagicode/hagilight-core/analytics-events';
|
|
3
|
+
function LinkSection({ heading, navLabel, links, tagged }) {
|
|
4
|
+
return (_jsxs("section", { children: [_jsx("h2", { children: heading }), _jsx("nav", { "aria-label": navLabel, children: _jsx("ul", { children: links.map((link) => (_jsx("li", { children: _jsx("a", { href: link.href, target: link.target, rel: link.rel, "aria-label": link.ariaLabel, ...(tagged ? siteLinkGaAttributes(link, 'footer') : {}), children: link.label }) }, link.id))) }) })] }));
|
|
5
|
+
}
|
|
6
|
+
export function FooterView({ links, version, year }) {
|
|
7
|
+
const { labels } = links;
|
|
8
|
+
return (_jsxs("footer", { className: "hagilight-footer", children: [_jsxs("div", { className: "hagilight-footer__sections", children: [links.relatedSites.length > 0 && (_jsxs("section", { children: [_jsx("h2", { children: labels.relatedSites }), _jsx("nav", { "aria-label": labels.navigation.relatedSites, children: _jsx("ul", { children: links.relatedSites.map((site) => (_jsx("li", { children: _jsx("a", { href: site.href, target: site.target, rel: site.rel, children: site.name }) }, site.id))) }) })] })), _jsx(LinkSection, { heading: labels.quickLinks, navLabel: labels.navigation.quickLinks, links: links.quick, tagged: true }), _jsx(LinkSection, { heading: labels.community, navLabel: labels.navigation.community, links: links.community, tagged: true })] }), links.filings.length > 0 && (_jsx("nav", { className: "hagilight-footer__filings", "aria-label": labels.navigation.filings, children: links.filings.map((link) => (_jsx("a", { href: link.href, target: link.target, rel: link.rel, "aria-label": link.ariaLabel, children: link.label }, link.id))) })), _jsxs("p", { className: "hagilight-footer__copyright", children: [_jsxs("span", { children: ["\u00A9 ", year, " HagiCode"] }), ' | ', _jsxs("span", { className: "hagilight-footer__attribution", children: ["Powered By hagilight-docusaurus@", version] })] })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import FooterDefaults from '@theme-init/Footer';
|
|
3
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
4
|
+
import { usePluginData } from '@docusaurus/useGlobalData';
|
|
5
|
+
import { resolveFooterLinks } from '../../footer.js';
|
|
6
|
+
import { PLUGIN_NAME } from '../../global-data.js';
|
|
7
|
+
import { FooterView } from './FooterView.js';
|
|
8
|
+
export default function Footer() {
|
|
9
|
+
const { i18n, siteConfig } = useDocusaurusContext();
|
|
10
|
+
const { version, options } = usePluginData(PLUGIN_NAME);
|
|
11
|
+
if (!options.footer)
|
|
12
|
+
return _jsx(FooterDefaults, {});
|
|
13
|
+
const links = resolveFooterLinks({
|
|
14
|
+
locale: i18n.currentLocale,
|
|
15
|
+
baseUrl: siteConfig.baseUrl,
|
|
16
|
+
links: options.links,
|
|
17
|
+
rssEnabled: options.rss.enabled,
|
|
18
|
+
});
|
|
19
|
+
return _jsx(FooterView, { links: links, version: version, year: new Date().getFullYear() });
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Props as HeadProps } from '@docusaurus/Head';
|
|
2
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
3
|
+
export interface SharingMetadataProps {
|
|
4
|
+
Head: (props: HeadProps) => ReactNode;
|
|
5
|
+
siteName: string;
|
|
6
|
+
image: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare function SharingMetadata({ Head, siteName, image }: SharingMetadataProps): ReactElement;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function SharingMetadata({ Head, siteName, image }) {
|
|
3
|
+
return (_jsxs(Head, { children: [_jsx("meta", { property: "og:site_name", content: siteName }), image !== undefined && _jsx("meta", { property: "og:image", content: image }), image !== undefined && _jsx("meta", { name: "twitter:image", content: image })] }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Head from '@docusaurus/Head';
|
|
3
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
4
|
+
import { usePluginData } from '@docusaurus/useGlobalData';
|
|
5
|
+
import ClassicSiteMetadata from '@docusaurus/theme-classic/lib/theme/SiteMetadata/index.js';
|
|
6
|
+
import { PLUGIN_NAME } from '../../global-data.js';
|
|
7
|
+
import { sharingMetadataFor } from '../../seo.js';
|
|
8
|
+
import { SharingMetadata } from './SharingMetadata.js';
|
|
9
|
+
export default function SiteMetadata() {
|
|
10
|
+
const { siteConfig } = useDocusaurusContext();
|
|
11
|
+
const { options } = usePluginData(PLUGIN_NAME);
|
|
12
|
+
const themeImage = siteConfig.themeConfig?.image;
|
|
13
|
+
const sharing = sharingMetadataFor(options.seo, siteConfig, typeof themeImage === 'string' ? themeImage : undefined);
|
|
14
|
+
return (_jsxs(_Fragment, { children: [sharing !== undefined && _jsx(SharingMetadata, { Head: Head, ...sharing }), _jsx(ClassicSiteMetadata, {})] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { LoadContext } from '@docusaurus/types';
|
|
2
|
+
import type { ResolvedHagilightOptions } from './options.js';
|
|
3
|
+
export declare function siteOwnsThemeComponent(siteDir: string, name: string): boolean;
|
|
4
|
+
export declare function findConfigurationWarnings(context: Pick<LoadContext, 'siteDir' | 'siteConfig' | 'i18n'>, options: ResolvedHagilightOptions): string[];
|
package/dist/warnings.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { locales as catalogLocales } from './locales.js';
|
|
4
|
+
const THEME_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js'];
|
|
5
|
+
const GTAG_PLUGIN = '@docusaurus/plugin-google-gtag';
|
|
6
|
+
const CLASSIC_PRESET = '@docusaurus/preset-classic';
|
|
7
|
+
const CATALOG_LOCALES = new Set(catalogLocales);
|
|
8
|
+
export function siteOwnsThemeComponent(siteDir, name) {
|
|
9
|
+
const themeDir = join(siteDir, 'src', 'theme');
|
|
10
|
+
return THEME_EXTENSIONS.some((ext) => existsSync(join(themeDir, name, `index.${ext}`)) || existsSync(join(themeDir, `${name}.${ext}`)));
|
|
11
|
+
}
|
|
12
|
+
function entryName(entry) {
|
|
13
|
+
if (typeof entry === 'string')
|
|
14
|
+
return entry;
|
|
15
|
+
if (Array.isArray(entry) && typeof entry[0] === 'string')
|
|
16
|
+
return entry[0];
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
function entryOptions(entry) {
|
|
20
|
+
if (!Array.isArray(entry))
|
|
21
|
+
return undefined;
|
|
22
|
+
const options = entry[1];
|
|
23
|
+
return typeof options === 'object' && options !== null ? options : undefined;
|
|
24
|
+
}
|
|
25
|
+
function hasPluginEntry(entries, name) {
|
|
26
|
+
return Array.isArray(entries) && entries.some((entry) => entryName(entry) === name);
|
|
27
|
+
}
|
|
28
|
+
function hasLocaleDropdown(themeConfig) {
|
|
29
|
+
const navbar = themeConfig.navbar;
|
|
30
|
+
if (typeof navbar !== 'object' || navbar === null)
|
|
31
|
+
return false;
|
|
32
|
+
const items = navbar.items;
|
|
33
|
+
return Array.isArray(items) && items.some((item) => item?.type === 'localeDropdown');
|
|
34
|
+
}
|
|
35
|
+
function officialGtagConfigured(siteConfig) {
|
|
36
|
+
const raw = siteConfig;
|
|
37
|
+
if (hasPluginEntry(raw.plugins, GTAG_PLUGIN))
|
|
38
|
+
return true;
|
|
39
|
+
return Array.isArray(raw.presets)
|
|
40
|
+
&& raw.presets.some((preset) => entryName(preset) === CLASSIC_PRESET && entryOptions(preset)?.gtag !== undefined);
|
|
41
|
+
}
|
|
42
|
+
export function findConfigurationWarnings(context, options) {
|
|
43
|
+
const warnings = [];
|
|
44
|
+
const themeConfig = (context.siteConfig.themeConfig ?? {});
|
|
45
|
+
if (options.languageSelector && context.i18n.locales.length > 1) {
|
|
46
|
+
if (!hasLocaleDropdown(themeConfig)) {
|
|
47
|
+
warnings.push("Several locales are configured but themeConfig.navbar.items has no localeDropdown item, so visitors cannot switch language. Add { type: 'localeDropdown' } or set languageSelector: { enabled: false }.");
|
|
48
|
+
}
|
|
49
|
+
const nonCatalog = context.i18n.locales.filter((locale) => !CATALOG_LOCALES.has(locale));
|
|
50
|
+
if (nonCatalog.length > 0) {
|
|
51
|
+
warnings.push(`${nonCatalog.join(', ')} ${nonCatalog.length === 1 ? 'is' : 'are'} not in the Hagilight locale catalog, so its native label and lang value are not checked against the Starlight locales. Use catalog locales or set languageSelector: { enabled: false }.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (options.footer && siteOwnsThemeComponent(context.siteDir, 'Footer')) {
|
|
55
|
+
warnings.push('src/theme/Footer replaces the Hagilight footer. Remove the site Footer component or set footer: { enabled: false } to keep one footer.');
|
|
56
|
+
}
|
|
57
|
+
if (options.seo.enabled && siteOwnsThemeComponent(context.siteDir, 'SiteMetadata')) {
|
|
58
|
+
warnings.push('src/theme/SiteMetadata replaces the Hagilight sharing-image fallback. Remove the site component or set seo: { enabled: false }; the Organization and WebSite JSON-LD are still emitted.');
|
|
59
|
+
}
|
|
60
|
+
if (options.seo.enabled && options.seo.image === undefined && themeConfig.image === undefined) {
|
|
61
|
+
warnings.push('Neither themeConfig.image nor seo.image is set, so pages without their own image get no default sharing image. Set one of them, or set seo: { enabled: false }.');
|
|
62
|
+
}
|
|
63
|
+
if (options.footer && themeConfig.footer !== undefined) {
|
|
64
|
+
warnings.push('themeConfig.footer is ignored by the Hagilight footer. Move its links into the links option or set footer: { enabled: false }.');
|
|
65
|
+
}
|
|
66
|
+
if (options.googleAnalyticsId !== undefined && (themeConfig.gtag !== undefined || officialGtagConfigured(context.siteConfig))) {
|
|
67
|
+
warnings.push('Google Analytics would load twice: the site already configures gtag. Remove themeConfig.gtag or the official gtag plugin, or set analytics.googleAnalytics: { enabled: false }.');
|
|
68
|
+
}
|
|
69
|
+
return warnings;
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hagicode/hagilight-docusaurus",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Docusaurus plugin for HagiCode site features: footer, analytics, promote card, locales, SEO, sitemap, robots.txt, and RSS",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/HagiCode-org/hagilight.git",
|
|
9
|
+
"directory": "packages/docusaurus"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"provenance": true
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -b",
|
|
20
|
+
"prepack": "tsc -b"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./locales": {
|
|
28
|
+
"types": "./dist/locales.d.ts",
|
|
29
|
+
"default": "./dist/locales.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"README.md",
|
|
34
|
+
"styles.css",
|
|
35
|
+
"promoto.css",
|
|
36
|
+
"dist/**/*.js",
|
|
37
|
+
"dist/**/*.d.ts"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@docusaurus/types": "^3.9.2",
|
|
41
|
+
"@hagicode/hagilight-core": "0.1.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@docusaurus/core": "^3.9.2",
|
|
45
|
+
"@docusaurus/theme-classic": "^3.9.2",
|
|
46
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
47
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@docusaurus/core": "^3.9.2",
|
|
51
|
+
"@docusaurus/module-type-aliases": "^3.9.2",
|
|
52
|
+
"@docusaurus/theme-classic": "^3.9.2",
|
|
53
|
+
"@types/react": "^19.0.0",
|
|
54
|
+
"@types/react-dom": "^19.0.0",
|
|
55
|
+
"react": "^19.0.0",
|
|
56
|
+
"react-dom": "^19.0.0",
|
|
57
|
+
"react-helmet-async": "^3.0.0"
|
|
58
|
+
},
|
|
59
|
+
"keywords": [
|
|
60
|
+
"docusaurus",
|
|
61
|
+
"docusaurus-plugin",
|
|
62
|
+
"multilingual-documentation",
|
|
63
|
+
"hreflang",
|
|
64
|
+
"structured-data",
|
|
65
|
+
"rss",
|
|
66
|
+
"sitemap"
|
|
67
|
+
]
|
|
68
|
+
}
|