@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 CHANGED
@@ -1,3 +1,133 @@
1
- # Temporary Holding Version
1
+ # @hagicode/hagilight-docusaurus
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Docusaurus plugin that adds the Hagilight footer, promotion banner, analytics, SEO metadata, and discovery files (`sitemap.xml`, `robots.txt`, `rss.xml`) to a Docusaurus 3 site.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @hagicode/hagilight-docusaurus
9
+ ```
10
+
11
+ Peer dependencies are `@docusaurus/core` and `@docusaurus/theme-classic` `^3.9.2`, and `react` `^18 || ^19`. The demo site uses Docusaurus 3.10.
12
+
13
+ ## Minimal configuration
14
+
15
+ ```js
16
+ // docusaurus.config.mjs
17
+ import { hagilightDocusaurus } from '@hagicode/hagilight-docusaurus';
18
+ import { i18n } from '@hagicode/hagilight-docusaurus/locales';
19
+
20
+ export default {
21
+ title: 'My docs',
22
+ url: 'https://docs.example.com',
23
+ baseUrl: '/',
24
+ i18n,
25
+ plugins: [
26
+ hagilightDocusaurus({
27
+ analytics: {
28
+ googleAnalytics: { measurementId: 'G-XXXXXXXXXX' },
29
+ fiftyOneLa: { enabled: false },
30
+ },
31
+ }),
32
+ ],
33
+ themeConfig: {
34
+ image: 'img/social-card.png',
35
+ navbar: {
36
+ items: [{ type: 'localeDropdown' }],
37
+ },
38
+ },
39
+ };
40
+ ```
41
+
42
+ Keep `"type": "module"` out of the site's `package.json` and name an ESM config `docusaurus.config.mjs`. Docusaurus's generated registry needs webpack's CommonJS rewrite during SSR, which an ESM-typed package skips, so the production build fails with `require.resolveWeak is not a function`.
43
+
44
+ ## Options
45
+
46
+ Every option is optional. Feature toggles take an object with `enabled`, which defaults to `true`.
47
+
48
+ | Option | Default | Effect |
49
+ | --- | --- | --- |
50
+ | `footer` | `{ enabled: true }` | Replaces the Docusaurus footer with the Hagilight footer. Uses `links`. |
51
+ | `links` | `{}` | Site link configuration from `@hagicode/hagilight-core`: `overrides`, `extraLinks`, `removeLinks`, `relatedSites`, `rssFeedUrl`, `rssLocaleFeedUrl`, `siteId`, `siteUrl`. |
52
+ | `analytics.googleAnalytics` | built-in ID, enabled | `{ enabled, measurementId }`. Loads gtag in production builds only. |
53
+ | `analytics.fiftyOneLa` | built-in ID, enabled | `{ enabled, siteId }`. Loads 51LA in production builds only. |
54
+ | `promoto` | `{ enabled: true }` | Shows the promotion banner and its styles. |
55
+ | `languageSelector` | `{ enabled: true }` | Controls the locale configuration warnings. It does not render a dropdown. Add `{ type: 'localeDropdown' }` to `themeConfig.navbar.items` for the switcher. |
56
+ | `seo` | `{ enabled: true }` | Emits sharing metadata and Organization and WebSite JSON-LD. Sub-options: `image`, `organization.name`, `organization.url`, `organization.logo`. |
57
+ | `sitemap` | `{ enabled: true }` | Writes a fallback `sitemap.xml` in production builds (see below). |
58
+ | `robots` | `{ enabled: true }` | Writes `robots.txt` in the default-locale build (see below). |
59
+ | `rss` | `{ enabled: true, includeDocs: true, includeBlog: true }` | Writes `rss.xml` for each locale that has feed items. `includeDocs` and `includeBlog` choose the sources. |
60
+ | `noIndex` | `false` | Stops this plugin from writing `sitemap.xml` and `robots.txt`. |
61
+
62
+ ### Analytics defaults
63
+
64
+ The built-in analytics IDs (`G-EN03FMT2Q4` and 51LA `L6b88a5yK4h2Xnci`) belong to HagiCode. A site that omits `analytics` sends its traffic to HagiCode's properties. Set your own IDs, or disable each provider with `enabled: false`.
65
+
66
+ An analytics object enables its provider when it contains an ID. Set `enabled` explicitly to override that. `enabled: true` with no ID is a configuration error.
67
+
68
+ ### SEO
69
+
70
+ - `seo.image` is an absolute HTTP(S) URL or a site-root path. When it is unset, `themeConfig.image` is the fallback sharing image.
71
+ - `seo.organization.url` and `seo.organization.logo` must be absolute HTTP(S) URLs.
72
+ - The Organization and WebSite JSON-LD is still emitted when a site replaces `SiteMetadata`.
73
+
74
+ ## Locale catalog
75
+
76
+ `@hagicode/hagilight-docusaurus/locales` exports the locale table used by the plugin:
77
+
78
+ ```js
79
+ import { i18n, locales, localeConfigs, defaultLocale } from '@hagicode/hagilight-docusaurus/locales';
80
+ ```
81
+
82
+ - `i18n` is a complete Docusaurus `i18n` config (`defaultLocale`, `locales`, `path: 'i18n'`, `localeConfigs`).
83
+ - `locales` and `localeConfigs` are the pieces `i18n` is built from, for sites that assemble their own `i18n` object.
84
+
85
+ Catalog locales are `en-US` (default), `zh-CN`, `zh-Hant`, `fr-FR`, `de-DE`, `es-ES`, `ja-JP`, `ko-KR`, `pt-BR`, and `ru-RU`.
86
+
87
+ ## Discovery output
88
+
89
+ ### sitemap.xml
90
+
91
+ Written in production builds only, into each locale's output directory, when:
92
+
93
+ - `sitemap` is enabled, and
94
+ - `noIndex` is not set, and
95
+ - the official `docusaurus-plugin-sitemap` is not in the loaded plugins.
96
+
97
+ The fallback lists every built route except the 404 page, with absolute URLs built from `url` and that locale's base URL. If the official sitemap plugin is loaded, this plugin writes nothing.
98
+
99
+ ### robots.txt
100
+
101
+ Written only by the default-locale build. It contains `User-agent: *`, `Allow: /`, and one absolute `Sitemap:` line for every configured locale.
102
+
103
+ - A site-owned `static/robots.txt` takes precedence, and this plugin writes nothing.
104
+ - A build of a non-default locale alone prints a warning when the default-locale build has not written `robots.txt`.
105
+ - Build every locale (run `docusaurus build` without `--locale`) so `robots.txt` lists all sitemaps.
106
+
107
+ ### rss.xml
108
+
109
+ Written per locale, into that locale's output directory, when feed items exist. Drafts and unlisted entries are excluded.
110
+
111
+ - A site-owned `static/rss.xml` takes precedence.
112
+ - The `<link rel="alternate">` head tag is added when feed items exist or the site owns `rss.xml`.
113
+ - With no items, the build logs an info message and writes nothing.
114
+
115
+ ## Configuration warnings
116
+
117
+ The plugin prints a `[@hagicode/hagilight-docusaurus]` warning during the build when:
118
+
119
+ - Several locales are configured but `themeConfig.navbar.items` has no `localeDropdown` item, and `languageSelector` is enabled.
120
+ - A configured locale is not in the Hagilight catalog, so its native label and `lang` value are not checked. Use catalog locales or disable `languageSelector`.
121
+ - `src/theme/Footer` exists in the site while `footer` is enabled. Remove the site component or disable `footer`.
122
+ - `src/theme/SiteMetadata` exists while `seo` is enabled. Remove it or disable `seo`.
123
+ - Neither `themeConfig.image` nor `seo.image` is set while `seo` is enabled.
124
+ - `themeConfig.footer` is set while the Hagilight footer is enabled. Its links are ignored.
125
+ - Google Analytics is configured twice, through `themeConfig.gtag` or the official gtag plugin, and through this plugin.
126
+
127
+ ## Plugin ordering
128
+
129
+ The sitemap check looks at every loaded plugin, so the position of `hagilightDocusaurus()` in `plugins` does not change the result. The official `docusaurus-plugin-sitemap` must be loaded to be detected.
130
+
131
+ ## Demo
132
+
133
+ The demo site for this package lives in the monorepo at `examples/demo-docusaurus-web` and is published to https://docusaurus.hagicode.com/.
@@ -0,0 +1,7 @@
1
+ import type { HtmlTagObject } from '@docusaurus/types';
2
+ export declare const DEFAULT_GOOGLE_ANALYTICS_ID = "G-EN03FMT2Q4";
3
+ export declare const DEFAULT_FIFTY_ONE_LA_ID = "L6b88a5yK4h2Xnci";
4
+ export declare const GOOGLE_ANALYTICS_ID_PATTERN: RegExp;
5
+ export declare const FIFTY_ONE_LA_ID_PATTERN: RegExp;
6
+ export declare function googleAnalyticsHeadTags(measurementId: string): HtmlTagObject[];
7
+ export declare function fiftyOneLaHeadTags(siteId: string): HtmlTagObject[];
@@ -0,0 +1,33 @@
1
+ export const DEFAULT_GOOGLE_ANALYTICS_ID = 'G-EN03FMT2Q4';
2
+ export const DEFAULT_FIFTY_ONE_LA_ID = 'L6b88a5yK4h2Xnci';
3
+ export const GOOGLE_ANALYTICS_ID_PATTERN = /^G-[A-Z0-9]+$/u;
4
+ export const FIFTY_ONE_LA_ID_PATTERN = /^[A-Za-z0-9_-]+$/u;
5
+ export function googleAnalyticsHeadTags(measurementId) {
6
+ return [
7
+ {
8
+ tagName: 'script',
9
+ attributes: { async: true, src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}` },
10
+ },
11
+ {
12
+ tagName: 'script',
13
+ innerHTML: [
14
+ 'window.dataLayer = window.dataLayer || [];',
15
+ 'function gtag(){dataLayer.push(arguments);}',
16
+ "gtag('js', new Date());",
17
+ `gtag('config', ${JSON.stringify(measurementId)});`,
18
+ ].join('\n'),
19
+ },
20
+ ];
21
+ }
22
+ export function fiftyOneLaHeadTags(siteId) {
23
+ return [
24
+ {
25
+ tagName: 'script',
26
+ attributes: { charset: 'UTF-8', id: 'LA_COLLECT', src: 'https://sdk.51.la/js-sdk-pro.min.js' },
27
+ },
28
+ {
29
+ tagName: 'script',
30
+ innerHTML: `LA.init(${JSON.stringify({ id: siteId, ck: siteId, autoTrack: true, hashMode: true, screenRecord: true })});`,
31
+ },
32
+ ];
33
+ }
@@ -0,0 +1,23 @@
1
+ export interface RouteLocation {
2
+ pathname: string;
3
+ search: string;
4
+ hash: string;
5
+ }
6
+ export interface RouteDidUpdate {
7
+ location: RouteLocation;
8
+ previousLocation: RouteLocation | null;
9
+ }
10
+ export interface PageSnapshot {
11
+ title: string;
12
+ location: string;
13
+ path: string;
14
+ }
15
+ export interface RouteReportDeps {
16
+ getGtag: () => unknown;
17
+ readPage: () => PageSnapshot;
18
+ schedule: (task: () => void) => void;
19
+ }
20
+ export declare function installClickTracking(target?: Pick<EventTarget, 'addEventListener'>): boolean;
21
+ export declare function reportPageView(gtag: unknown, page: PageSnapshot): boolean;
22
+ export declare function handleRouteDidUpdate(update: RouteDidUpdate, deps: RouteReportDeps): void;
23
+ export declare function onRouteDidUpdate(update: RouteDidUpdate): void;
@@ -0,0 +1,33 @@
1
+ import { installGaEventTracking } from '@hagicode/hagilight-core/analytics-events';
2
+ export function installClickTracking(target) {
3
+ return installGaEventTracking(target);
4
+ }
5
+ export function reportPageView(gtag, page) {
6
+ if (typeof gtag !== 'function')
7
+ return false;
8
+ gtag('event', 'page_view', { page_title: page.title, page_location: page.location, page_path: page.path });
9
+ return true;
10
+ }
11
+ export function handleRouteDidUpdate(update, deps) {
12
+ if (!update.previousLocation)
13
+ return;
14
+ deps.schedule(() => {
15
+ reportPageView(deps.getGtag(), deps.readPage());
16
+ });
17
+ }
18
+ const browserDeps = {
19
+ getGtag: () => globalThis.gtag,
20
+ readPage: () => ({
21
+ title: document.title,
22
+ location: window.location.href,
23
+ path: `${window.location.pathname}${window.location.search}`,
24
+ }),
25
+ schedule: (task) => {
26
+ window.setTimeout(task, 0);
27
+ },
28
+ };
29
+ export function onRouteDidUpdate(update) {
30
+ handleRouteDidUpdate(update, browserDeps);
31
+ }
32
+ if (typeof document !== 'undefined')
33
+ installClickTracking(document);
@@ -0,0 +1 @@
1
+ export declare const PROMOTO_SHELL_MARKUP = "\n <section class=\"hagilight-promoto__shell\" data-promoto-shell hidden aria-label=\"Featured promotion\">\n <div class=\"hagilight-promoto__viewport\">\n <div class=\"hagilight-promoto__track\" data-promoto-track></div>\n </div>\n <nav class=\"hagilight-promoto__controls\" data-promoto-controls aria-label=\"Promotion controls\" hidden>\n <button type=\"button\" class=\"hagilight-promoto__control\" data-promoto-previous aria-label=\"Previous promotion\">\n <svg class=\"hagilight-promoto__icon\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 6l-6 6 6 6\" /></svg>\n </button>\n <span class=\"hagilight-promoto__count\" data-promoto-count aria-label=\"Promotion progress\"></span>\n <button type=\"button\" class=\"hagilight-promoto__control\" data-promoto-next aria-label=\"Next promotion\">\n <svg class=\"hagilight-promoto__icon\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M9 6l6 6-6 6\" /></svg>\n </button>\n <button type=\"button\" class=\"hagilight-promoto__control\" data-promoto-pause aria-pressed=\"false\" aria-label=\"Pause automatic promotion rotation\">\n <svg class=\"hagilight-promoto__icon hagilight-promoto__icon--pause\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M9 6v12M15 6v12\" /></svg>\n <svg class=\"hagilight-promoto__icon hagilight-promoto__icon--play\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M8 5.5v13l11-6.5z\" /></svg>\n </button>\n </nav>\n <button type=\"button\" class=\"hagilight-promoto__dismiss\" data-promoto-dismiss aria-label=\"Dismiss promotion\">\n <svg class=\"hagilight-promoto__icon\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M6.5 6.5l11 11M17.5 6.5l-11 11\" /></svg>\n </button>\n <span class=\"hagilight-promoto__status\" data-promoto-status aria-live=\"polite\"></span>\n </section>\n";
@@ -0,0 +1,24 @@
1
+ export const PROMOTO_SHELL_MARKUP = `
2
+ <section class="hagilight-promoto__shell" data-promoto-shell hidden aria-label="Featured promotion">
3
+ <div class="hagilight-promoto__viewport">
4
+ <div class="hagilight-promoto__track" data-promoto-track></div>
5
+ </div>
6
+ <nav class="hagilight-promoto__controls" data-promoto-controls aria-label="Promotion controls" hidden>
7
+ <button type="button" class="hagilight-promoto__control" data-promoto-previous aria-label="Previous promotion">
8
+ <svg class="hagilight-promoto__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15 6l-6 6 6 6" /></svg>
9
+ </button>
10
+ <span class="hagilight-promoto__count" data-promoto-count aria-label="Promotion progress"></span>
11
+ <button type="button" class="hagilight-promoto__control" data-promoto-next aria-label="Next promotion">
12
+ <svg class="hagilight-promoto__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
13
+ </button>
14
+ <button type="button" class="hagilight-promoto__control" data-promoto-pause aria-pressed="false" aria-label="Pause automatic promotion rotation">
15
+ <svg class="hagilight-promoto__icon hagilight-promoto__icon--pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6v12M15 6v12" /></svg>
16
+ <svg class="hagilight-promoto__icon hagilight-promoto__icon--play" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5.5v13l11-6.5z" /></svg>
17
+ </button>
18
+ </nav>
19
+ <button type="button" class="hagilight-promoto__dismiss" data-promoto-dismiss aria-label="Dismiss promotion">
20
+ <svg class="hagilight-promoto__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11" /></svg>
21
+ </button>
22
+ <span class="hagilight-promoto__status" data-promoto-status aria-live="polite"></span>
23
+ </section>
24
+ `;
@@ -0,0 +1,2 @@
1
+ export declare function mountPromotoBanner(doc: Document, locale: string): void;
2
+ export declare function onRouteDidUpdate(): void;
@@ -0,0 +1,23 @@
1
+ import { definePromotoBannerElement } from '@hagicode/hagilight-core/promoto-banner';
2
+ import { PROMOTO_SHELL_MARKUP } from './promoto-markup.js';
3
+ const ELEMENT_NAME = 'hagilight-promoto-banner';
4
+ const DEFAULT_LOCALE = 'en-US';
5
+ const mounts = new WeakMap();
6
+ export function mountPromotoBanner(doc, locale) {
7
+ const footer = doc.querySelector('footer');
8
+ const previous = mounts.get(doc);
9
+ if (previous?.banner.isConnected && previous.footer === footer && previous.locale === locale)
10
+ return;
11
+ doc.querySelectorAll(ELEMENT_NAME).forEach((existing) => existing.remove());
12
+ const banner = doc.createElement(ELEMENT_NAME);
13
+ banner.className = 'hagilight-promoto';
14
+ banner.dataset.locale = locale;
15
+ banner.hidden = true;
16
+ banner.innerHTML = PROMOTO_SHELL_MARKUP;
17
+ doc.body.append(banner);
18
+ mounts.set(doc, { banner, footer, locale });
19
+ }
20
+ export function onRouteDidUpdate() {
21
+ definePromotoBannerElement();
22
+ mountPromotoBanner(document, document.documentElement.lang || DEFAULT_LOCALE);
23
+ }
@@ -0,0 +1,38 @@
1
+ import type { HtmlTagObject, LoadContext } from '@docusaurus/types';
2
+ import { type RssFeedItem } from '@hagicode/hagilight-core/rss';
3
+ import type { ResolvedHagilightOptions, ResolvedRssOptions } from './options.js';
4
+ type Warn = (message: string) => void;
5
+ export interface DiscoveryReporter {
6
+ warn: Warn;
7
+ info: Warn;
8
+ }
9
+ export interface DiscoverySite {
10
+ title: string;
11
+ tagline: string;
12
+ url: string;
13
+ baseUrl: string;
14
+ siteDir: string;
15
+ i18n: LoadContext['i18n'];
16
+ }
17
+ export interface DiscoveryOutputs {
18
+ options: ResolvedHagilightOptions;
19
+ site: DiscoverySite;
20
+ outDir: string;
21
+ production: boolean;
22
+ plugins: readonly {
23
+ name: string;
24
+ }[];
25
+ routesPaths: readonly string[];
26
+ routesBuildMetadata: Readonly<Record<string, {
27
+ noIndex: boolean;
28
+ } | undefined>>;
29
+ feedItems: readonly RssFeedItem[];
30
+ report: DiscoveryReporter;
31
+ }
32
+ export declare function sitemapUrls(routesPaths: readonly string[], routesBuildMetadata: DiscoveryOutputs['routesBuildMetadata'], siteUrl: string, localeBaseUrl: string): string[];
33
+ export declare function sitemapXml(urls: readonly string[]): string;
34
+ export declare function robotsTxt(sitemapUrlList: readonly string[]): string;
35
+ export declare function collectFeedItems(allContent: unknown, site: DiscoverySite, rss: ResolvedRssOptions, report: DiscoveryReporter): RssFeedItem[];
36
+ export declare function feedHeadTags(site: DiscoverySite, rss: ResolvedRssOptions, feedItemCount: number): HtmlTagObject[];
37
+ export declare function writeDiscoveryOutputs(outputs: DiscoveryOutputs): Promise<void>;
38
+ export {};
@@ -0,0 +1,200 @@
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
+ const SITEMAP_PLUGIN = 'docusaurus-plugin-sitemap';
6
+ const DOCS_PLUGIN = 'docusaurus-plugin-content-docs';
7
+ const BLOG_PLUGIN = 'docusaurus-plugin-content-blog';
8
+ function notFoundRoute(baseUrl) {
9
+ const escapedBase = baseUrl.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
10
+ return new RegExp(`^${escapedBase}404(?:\\.html)?\\/?$`, 'u');
11
+ }
12
+ function isRecord(value) {
13
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
14
+ }
15
+ function siteOwns(site, fileName) {
16
+ return existsSync(join(site.siteDir, 'static', fileName));
17
+ }
18
+ function escapeXml(value) {
19
+ return value
20
+ .replace(/&/gu, '&amp;')
21
+ .replace(/</gu, '&lt;')
22
+ .replace(/>/gu, '&gt;')
23
+ .replace(/"/gu, '&quot;')
24
+ .replace(/'/gu, '&apos;');
25
+ }
26
+ export function sitemapUrls(routesPaths, routesBuildMetadata, siteUrl, localeBaseUrl) {
27
+ const notFound = notFoundRoute(localeBaseUrl);
28
+ const urls = routesPaths
29
+ .filter((route) => !notFound.test(route) && routesBuildMetadata[route]?.noIndex !== true)
30
+ .map((route) => new URL(route, siteUrl).href);
31
+ return [...new Set(urls)];
32
+ }
33
+ export function sitemapXml(urls) {
34
+ const entries = urls.map((url) => ` <url>\n <loc>${escapeXml(url)}</loc>\n </url>`).join('\n');
35
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries}\n</urlset>\n`;
36
+ }
37
+ export function robotsTxt(sitemapUrlList) {
38
+ return ['User-agent: *', 'Allow: /', '', ...sitemapUrlList.map((url) => `Sitemap: ${url}`), ''].join('\n');
39
+ }
40
+ function localeSitemapUrls(site) {
41
+ return site.i18n.locales.map((locale) => new URL(`${site.i18n.localeConfigs[locale].baseUrl}sitemap.xml`, site.url).href);
42
+ }
43
+ function defaultLocaleOutDir(outDir, i18n) {
44
+ const defaultBase = i18n.localeConfigs[i18n.defaultLocale].baseUrl;
45
+ const currentBase = i18n.localeConfigs[i18n.currentLocale].baseUrl;
46
+ const localeSegments = currentBase.slice(defaultBase.length).split('/').filter(Boolean);
47
+ return resolve(outDir, ...localeSegments.map(() => '..'));
48
+ }
49
+ function instanceContents(allContent, pluginName, report) {
50
+ if (!isRecord(allContent) || allContent[pluginName] === undefined)
51
+ return [];
52
+ const instances = allContent[pluginName];
53
+ if (!isRecord(instances)) {
54
+ report.warn(`${pluginName} content has an unexpected shape, so its feed items are skipped.`);
55
+ return [];
56
+ }
57
+ return Object.values(instances);
58
+ }
59
+ function publicationDate(value) {
60
+ if (value instanceof Date)
61
+ return Number.isNaN(value.getTime()) ? undefined : value;
62
+ if (typeof value === 'number' && Number.isFinite(value))
63
+ return value;
64
+ if (typeof value === 'string' && !Number.isNaN(new Date(value).getTime()))
65
+ return value;
66
+ return undefined;
67
+ }
68
+ function toFeedItem(meta, date, siteUrl) {
69
+ if (typeof meta.title !== 'string' || typeof meta.permalink !== 'string')
70
+ return undefined;
71
+ const description = typeof meta.description === 'string' && meta.description.trim() ? meta.description : undefined;
72
+ const published = publicationDate(date);
73
+ return {
74
+ title: meta.title,
75
+ link: new URL(meta.permalink, siteUrl).href,
76
+ ...(description === undefined ? {} : { description }),
77
+ ...(published === undefined ? {} : { date: published }),
78
+ };
79
+ }
80
+ function isListed(meta) {
81
+ return meta.draft !== true && meta.unlisted !== true;
82
+ }
83
+ function docFeedItems(allContent, site, report) {
84
+ const items = [];
85
+ let malformed = 0;
86
+ for (const content of instanceContents(allContent, DOCS_PLUGIN, report)) {
87
+ const current = isRecord(content) && Array.isArray(content.loadedVersions)
88
+ ? content.loadedVersions.find((version) => isRecord(version) && version.isLast === true)
89
+ : undefined;
90
+ if (!isRecord(current) || !Array.isArray(current.docs)) {
91
+ malformed += 1;
92
+ continue;
93
+ }
94
+ for (const doc of current.docs) {
95
+ if (!isRecord(doc)) {
96
+ malformed += 1;
97
+ continue;
98
+ }
99
+ if (!isListed(doc))
100
+ continue;
101
+ const item = toFeedItem(doc, doc.lastUpdatedAt, site.url);
102
+ if (item === undefined)
103
+ malformed += 1;
104
+ else
105
+ items.push(item);
106
+ }
107
+ }
108
+ if (malformed > 0)
109
+ report.warn(`${malformed} docs entries have an unexpected shape and are left out of the RSS feed.`);
110
+ return items;
111
+ }
112
+ function blogFeedItems(allContent, site, report) {
113
+ const items = [];
114
+ let malformed = 0;
115
+ for (const content of instanceContents(allContent, BLOG_PLUGIN, report)) {
116
+ const posts = isRecord(content) && Array.isArray(content.blogPosts) ? content.blogPosts : undefined;
117
+ if (posts === undefined) {
118
+ malformed += 1;
119
+ continue;
120
+ }
121
+ for (const post of posts) {
122
+ const meta = isRecord(post) && isRecord(post.metadata) ? post.metadata : undefined;
123
+ if (meta === undefined) {
124
+ malformed += 1;
125
+ continue;
126
+ }
127
+ if (!isListed(meta))
128
+ continue;
129
+ const item = toFeedItem(meta, meta.date, site.url);
130
+ if (item === undefined)
131
+ malformed += 1;
132
+ else
133
+ items.push(item);
134
+ }
135
+ }
136
+ if (malformed > 0)
137
+ report.warn(`${malformed} blog entries have an unexpected shape and are left out of the RSS feed.`);
138
+ return items;
139
+ }
140
+ export function collectFeedItems(allContent, site, rss, report) {
141
+ if (!rss.enabled)
142
+ return [];
143
+ return [
144
+ ...(rss.includeBlog ? blogFeedItems(allContent, site, report) : []),
145
+ ...(rss.includeDocs ? docFeedItems(allContent, site, report) : []),
146
+ ];
147
+ }
148
+ export function feedHeadTags(site, rss, feedItemCount) {
149
+ if (!rss.enabled || (feedItemCount === 0 && !siteOwns(site, 'rss.xml')))
150
+ return [];
151
+ return [
152
+ {
153
+ tagName: 'link',
154
+ attributes: {
155
+ rel: 'alternate',
156
+ type: 'application/rss+xml',
157
+ title: site.title,
158
+ href: new URL(`${site.baseUrl}rss.xml`, site.url).href,
159
+ },
160
+ },
161
+ ];
162
+ }
163
+ async function renderFeed(site, items) {
164
+ const locale = site.i18n.currentLocale;
165
+ const htmlLang = site.i18n.localeConfigs[locale].htmlLang;
166
+ const [{ lang }] = resolveRssLocales({ [locale]: { lang: htmlLang } });
167
+ const response = await generateRssFeed({
168
+ site: site.url,
169
+ baseUrl: site.baseUrl,
170
+ language: lang,
171
+ title: site.title,
172
+ description: site.tagline.trim() ? site.tagline : site.title,
173
+ items: [...items],
174
+ });
175
+ return response.text();
176
+ }
177
+ export async function writeDiscoveryOutputs(outputs) {
178
+ const { options, site, outDir, report } = outputs;
179
+ const noIndex = options.noIndex;
180
+ if (options.sitemap && outputs.production && !noIndex && !outputs.plugins.some((plugin) => plugin.name === SITEMAP_PLUGIN)) {
181
+ const urls = sitemapUrls(outputs.routesPaths, outputs.routesBuildMetadata, site.url, site.i18n.localeConfigs[site.i18n.currentLocale].baseUrl);
182
+ await writeFile(join(outDir, 'sitemap.xml'), sitemapXml(urls));
183
+ }
184
+ if (options.robots && !noIndex && !siteOwns(site, 'robots.txt')) {
185
+ if (site.i18n.currentLocale === site.i18n.defaultLocale) {
186
+ await writeFile(join(outDir, 'robots.txt'), robotsTxt(localeSitemapUrls(site)));
187
+ }
188
+ else if (!existsSync(join(defaultLocaleOutDir(outDir, site.i18n), 'robots.txt'))) {
189
+ report.warn(`robots.txt is written only by the default locale build (${site.i18n.defaultLocale}); build that locale to include it.`);
190
+ }
191
+ }
192
+ if (options.rss.enabled && !siteOwns(site, 'rss.xml')) {
193
+ if (outputs.feedItems.length === 0) {
194
+ report.info('No content is available for the RSS feed, so rss.xml is not written.');
195
+ }
196
+ else {
197
+ await writeFile(join(outDir, 'rss.xml'), await renderFeed(site, outputs.feedItems));
198
+ }
199
+ }
200
+ }
@@ -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,8 @@
1
+ import type { LoadContext } from '@docusaurus/types';
2
+ import type { ResolvedHagilightOptions } from './options.js';
3
+ export declare const PLUGIN_NAME = "@hagicode/hagilight-docusaurus";
4
+ export interface HagilightGlobalData {
5
+ version: string;
6
+ options: ResolvedHagilightOptions;
7
+ i18n: LoadContext['i18n'];
8
+ }
@@ -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, HagilightDocusaurusOptions, ResolvedHagilightOptions, RssOptions, SeoOptions, SeoOrganizationOptions, } from './options.js';
7
+ export declare function hagilightDocusaurus(options?: HagilightDocusaurusOptions): PluginModule;
8
+ export default hagilightDocusaurus;