@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.
package/README.md CHANGED
@@ -1,3 +1,186 @@
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.posthog` | built-in key, enabled | `{ enabled, apiKey }`. Loads PostHog in the global head in production builds only. |
54
+ | `analytics.fiftyOneLa` | built-in ID, enabled | `{ enabled, siteId }`. Loads 51LA in production builds only. |
55
+ | `promoto` | `{ enabled: true }` | Shows the promotion banner and its styles. |
56
+ | `languageSelector` | `{ enabled: true }` | Controls the locale configuration warnings. It does not render a dropdown. Add `{ type: 'localeDropdown' }` to `themeConfig.navbar.items` for the switcher. |
57
+ | `seo` | `{ enabled: true }` | Emits sharing metadata and Organization and WebSite JSON-LD. Sub-options: `image`, `organization.name`, `organization.url`, `organization.logo`. |
58
+ | `sitemap` | `{ enabled: true }` | Writes a fallback `sitemap.xml` in production builds (see below). |
59
+ | `robots` | `{ enabled: true }` | Writes `robots.txt` in the default-locale build (see below). |
60
+ | `rss` | `{ enabled: true, includeDocs: true, includeBlog: true }` | Writes `rss.xml` for each locale that has feed items. `includeDocs` and `includeBlog` choose the sources. |
61
+ | `noIndex` | `false` | Stops this plugin from writing `sitemap.xml` and `robots.txt`. |
62
+
63
+ ### Footer avatar
64
+
65
+ `footer` takes `avatar` and `randomAvatar` next to `enabled`. Both default to `true`. `avatar: false` removes the decorative avatar from the brand block and its request; `randomAvatar: false` keeps the vendored default and ships no pool list and no picker script. The avatar and its pool come from `@hagicode/hagilight-core` (`assets/footer`), and the build emits the AVIF and WebP files as ordinary assets rather than inlining them into every page. A site whose `img-src` policy blocks `https://img-gen.hagicode.com` keeps the default image, and the pool refresh procedure is in the core README.
66
+
67
+ ```js
68
+ hagilightDocusaurus({ footer: { avatar: true, randomAvatar: false } });
69
+ ```
70
+
71
+ ### Analytics defaults
72
+
73
+ The built-in analytics IDs (`G-EN03FMT2Q4`, 51LA `L6b88a5yK4h2Xnci`, and PostHog `phc_qiVVihsL925ovhPYNdLJWz4k7aoKVMwYCevgZPE6PER5`) 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`.
74
+
75
+ 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.
76
+
77
+ PostHog needs no configuration or consumer project key when registering `hagilightDocusaurus()`. It uses `phc_qiVVihsL925ovhPYNdLJWz4k7aoKVMwYCevgZPE6PER5`, API host `https://us.i.posthog.com`, and SDK defaults `2026-05-30`. Its single inline initialization shares the global head with Google Analytics on every production HTML page, including localized docs, blogs and not-found pages. No PostHog loader is emitted outside production.
78
+
79
+ | PostHog input | Behavior |
80
+ | --- | --- |
81
+ | Plugin options omitted, `analytics: {}`, or `posthog` omitted | Enabled with the supplied default key |
82
+ | `posthog: {}` | Disabled, like an empty Google Analytics provider |
83
+ | `posthog: { apiKey: 'phc_Custom123' }` | Enabled with the custom key |
84
+ | `posthog: { enabled: true, apiKey: 'phc_Custom123' }` | Enabled with the custom key |
85
+ | `posthog: { enabled: true }` | Configuration error: `apiKey` required |
86
+ | `posthog: { enabled: false }` | Disabled independently of Google Analytics and 51LA |
87
+
88
+ Keys must match `^phc_[A-Za-z0-9]+$`; enabled providers reject missing or malformed keys. Provider options must be objects and `enabled` must be boolean.
89
+
90
+ ```js
91
+ hagilightDocusaurus(); // shared analytics defaults
92
+ hagilightDocusaurus({ analytics: { posthog: { apiKey: 'phc_Custom123' } } });
93
+ hagilightDocusaurus({ analytics: { posthog: { enabled: false } } });
94
+ hagilightDocusaurus({
95
+ analytics: {
96
+ googleAnalytics: { enabled: false },
97
+ fiftyOneLa: { enabled: false },
98
+ posthog: { enabled: false },
99
+ },
100
+ }); // all analytics disabled
101
+ ```
102
+
103
+ Each provider is independent: disabling Google Analytics and 51LA alone leaves PostHog enabled. Choose one PostHog initialization owner and opt out here if the site already supplies it. The loader initializes once per document, and the supplied SDK defaults own navigation tracking; Hagilight adds no PostHog capture calls or route listeners. The existing analytics client module remains registered only for Google Analytics and 51LA, preserving their navigation behavior.
104
+
105
+ ### SEO
106
+
107
+ - `seo.image` is an absolute HTTP(S) URL or a site-root path. When it is unset, `themeConfig.image` is the fallback sharing image.
108
+ - `seo.organization.url` and `seo.organization.logo` must be absolute HTTP(S) URLs.
109
+ - The Organization and WebSite JSON-LD is still emitted when a site replaces `SiteMetadata`.
110
+
111
+ ## Build line
112
+
113
+ The Hagilight footer shows `Branch:`, `Commit:`, and `Built:` after the attribution. The values are resolved from the site directory: `HAGILIGHT_BUILD_BRANCH` and `HAGILIGHT_BUILD_COMMIT`, then Git, then `unknown`; the core README gives the order. The line belongs to the Hagilight footer, so it is absent when `footer` is disabled. For a detached-HEAD CI checkout, set `HAGILIGHT_BUILD_BRANCH` in the build step as shown in the Astro README.
114
+
115
+ Docusaurus evaluates the plugin again for each locale it builds, so each locale resolves its own `Built:` time. Pages within one locale share that time, but two locales built in the same run can differ by a few seconds.
116
+
117
+ ## Locale catalog
118
+
119
+ `@hagicode/hagilight-docusaurus/locales` exports the locale table used by the plugin:
120
+
121
+ ```js
122
+ import { i18n, locales, localeConfigs, defaultLocale } from '@hagicode/hagilight-docusaurus/locales';
123
+ ```
124
+
125
+ - `i18n` is a complete Docusaurus `i18n` config (`defaultLocale`, `locales`, `path: 'i18n'`, `localeConfigs`).
126
+ - `locales` and `localeConfigs` are the pieces `i18n` is built from, for sites that assemble their own `i18n` object.
127
+
128
+ 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`.
129
+
130
+ ## Discovery output
131
+
132
+ ### sitemap.xml
133
+
134
+ Written in production builds only, into each locale's output directory, when:
135
+
136
+ - `sitemap` is enabled, and
137
+ - `noIndex` is not set, and
138
+ - the official `docusaurus-plugin-sitemap` is not in the loaded plugins.
139
+
140
+ 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.
141
+
142
+ ### robots.txt
143
+
144
+ Written only by the default-locale build. It contains `User-agent: *`, `Allow: /`, and one absolute `Sitemap:` line for every configured locale.
145
+
146
+ - A site-owned `static/robots.txt` takes precedence, and this plugin writes nothing.
147
+ - A build of a non-default locale alone prints a warning when the default-locale build has not written `robots.txt`.
148
+ - Build every locale (run `docusaurus build` without `--locale`) so `robots.txt` lists all sitemaps.
149
+
150
+ ### rss.xml
151
+
152
+ Written per locale, into that locale's output directory, when feed items exist. Drafts and unlisted entries are excluded.
153
+
154
+ - A site-owned `static/rss.xml` takes precedence.
155
+ - The `<link rel="alternate">` head tag is added when feed items exist or the site owns `rss.xml`.
156
+ - With no items, the build logs an info message and writes nothing.
157
+
158
+ ### .well-known/hagilight.metadata.json
159
+
160
+ Written by the default-locale build, as the last discovery step. It lists every configured locale, the discovery files this build wrote, and build facts. The schema is in `@hagicode/hagilight-core`'s README.
161
+
162
+ - `metadata: false` opts out. `metadata: { name, description }` overrides the site name, which defaults to `title`, and the description, which defaults to `tagline`.
163
+ - Analytics flags are `true` only in production builds that load the provider. Non-production builds report `false`.
164
+ - A build of a non-default locale writes nothing and warns when the default-locale build has not written the file, the same rule as `robots.txt`.
165
+ - A site-owned `static/.well-known/hagilight.metadata.json` takes precedence, and this plugin keeps it untouched.
166
+ - GitHub Pages needs a `.nojekyll` file in the published root, or the `.well-known` directory is dropped.
167
+
168
+ ## Configuration warnings
169
+
170
+ The plugin prints a `[@hagicode/hagilight-docusaurus]` warning during the build when:
171
+
172
+ - Several locales are configured but `themeConfig.navbar.items` has no `localeDropdown` item, and `languageSelector` is enabled.
173
+ - 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`.
174
+ - `src/theme/Footer` exists in the site while `footer` is enabled. Remove the site component or disable `footer`.
175
+ - `src/theme/SiteMetadata` exists while `seo` is enabled. Remove it or disable `seo`.
176
+ - Neither `themeConfig.image` nor `seo.image` is set while `seo` is enabled.
177
+ - `themeConfig.footer` is set while the Hagilight footer is enabled. Its links are ignored.
178
+ - Google Analytics is configured twice, through `themeConfig.gtag` or the official gtag plugin, and through this plugin.
179
+
180
+ ## Plugin ordering
181
+
182
+ 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.
183
+
184
+ ## Demo
185
+
186
+ 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,8 @@
1
+ import type { HtmlTagObject } from '@docusaurus/types';
2
+ export declare function posthogHeadTags(apiKey: string): HtmlTagObject[];
3
+ export declare const DEFAULT_GOOGLE_ANALYTICS_ID = "G-EN03FMT2Q4";
4
+ export declare const DEFAULT_FIFTY_ONE_LA_ID = "L6b88a5yK4h2Xnci";
5
+ export declare const GOOGLE_ANALYTICS_ID_PATTERN: RegExp;
6
+ export declare const FIFTY_ONE_LA_ID_PATTERN: RegExp;
7
+ export declare function googleAnalyticsHeadTags(measurementId: string): HtmlTagObject[];
8
+ export declare function fiftyOneLaHeadTags(siteId: string): HtmlTagObject[];
@@ -0,0 +1,37 @@
1
+ import { posthogInitScript } from '@hagicode/hagilight-core/posthog';
2
+ export function posthogHeadTags(apiKey) {
3
+ return [{ tagName: 'script', innerHTML: posthogInitScript(apiKey) }];
4
+ }
5
+ export const DEFAULT_GOOGLE_ANALYTICS_ID = 'G-EN03FMT2Q4';
6
+ export const DEFAULT_FIFTY_ONE_LA_ID = 'L6b88a5yK4h2Xnci';
7
+ export const GOOGLE_ANALYTICS_ID_PATTERN = /^G-[A-Z0-9]+$/u;
8
+ export const FIFTY_ONE_LA_ID_PATTERN = /^[A-Za-z0-9_-]+$/u;
9
+ export function googleAnalyticsHeadTags(measurementId) {
10
+ return [
11
+ {
12
+ tagName: 'script',
13
+ attributes: { async: true, src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}` },
14
+ },
15
+ {
16
+ tagName: 'script',
17
+ innerHTML: [
18
+ 'window.dataLayer = window.dataLayer || [];',
19
+ 'function gtag(){dataLayer.push(arguments);}',
20
+ "gtag('js', new Date());",
21
+ `gtag('config', ${JSON.stringify(measurementId)});`,
22
+ ].join('\n'),
23
+ },
24
+ ];
25
+ }
26
+ export function fiftyOneLaHeadTags(siteId) {
27
+ return [
28
+ {
29
+ tagName: 'script',
30
+ attributes: { charset: 'UTF-8', id: 'LA_COLLECT', src: 'https://sdk.51.la/js-sdk-pro.min.js' },
31
+ },
32
+ {
33
+ tagName: 'script',
34
+ innerHTML: `LA.init(${JSON.stringify({ id: siteId, ck: siteId, autoTrack: true, hashMode: true, screenRecord: true })});`,
35
+ },
36
+ ];
37
+ }
@@ -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__progress\" data-promoto-progress aria-hidden=\"true\"></span>\n <span class=\"hagilight-promoto__status\" data-promoto-status aria-live=\"polite\"></span>\n </section>\n";
@@ -0,0 +1,25 @@
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__progress" data-promoto-progress aria-hidden="true"></span>
23
+ <span class="hagilight-promoto__status" data-promoto-status aria-live="polite"></span>
24
+ </section>
25
+ `;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Centers the fixed card in the main content column instead of the viewport: it
3
+ * starts after the doc sidebar and ends before the desktop table of contents.
4
+ * Infima's layout cannot be expressed in CSS alone (collapsible sidebar, container
5
+ * max-width, TOC column), so the two insets are measured and handed to promoto.css.
6
+ */
7
+ export declare function syncPromotoInsets(doc: Document, banner: HTMLElement): void;
8
+ export declare function mountPromotoBanner(doc: Document, locale: string): void;
9
+ export declare function onRouteDidUpdate(): void;
@@ -0,0 +1,96 @@
1
+ import { definePromotoBannerElement, recordSiteVisit } 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
+ /** Infima switches to the single-column mobile layout below this width. */
7
+ const DESKTOP_MIN_WIDTH = 997;
8
+ const INSET_START = '--hagilight-promoto-inset-start';
9
+ const INSET_END = '--hagilight-promoto-inset-end';
10
+ let stopWatchingLayout;
11
+ function widest(elements) {
12
+ return elements
13
+ .map((element) => element.getBoundingClientRect())
14
+ .filter((rect) => rect.width > 0)
15
+ .sort((a, b) => b.width - a.width)[0];
16
+ }
17
+ /**
18
+ * Centers the fixed card in the main content column instead of the viewport: it
19
+ * starts after the doc sidebar and ends before the desktop table of contents.
20
+ * Infima's layout cannot be expressed in CSS alone (collapsible sidebar, container
21
+ * max-width, TOC column), so the two insets are measured and handed to promoto.css.
22
+ */
23
+ export function syncPromotoInsets(doc, banner) {
24
+ const viewport = doc.documentElement.clientWidth;
25
+ let start = 0;
26
+ let end = 0;
27
+ if (viewport >= DESKTOP_MIN_WIDTH) {
28
+ const main = doc.querySelector('main');
29
+ const columns = main ? Array.from(main.querySelectorAll(':scope > * > .row > .col, :scope > .row > .col')) : [];
30
+ const content = widest(columns) ?? main?.getBoundingClientRect();
31
+ if (content && content.width > 0) {
32
+ const sidebar = doc.querySelector('.theme-doc-sidebar-container')?.getBoundingClientRect();
33
+ start = Math.max(content.left, sidebar && sidebar.width > 0 ? sidebar.right : 0);
34
+ end = Math.max(0, viewport - content.right);
35
+ }
36
+ }
37
+ banner.style.setProperty(INSET_START, `${Math.max(0, Math.round(start))}px`);
38
+ banner.style.setProperty(INSET_END, `${Math.max(0, Math.round(end))}px`);
39
+ }
40
+ function watchPromotoLayout(doc, banner) {
41
+ stopWatchingLayout?.();
42
+ stopWatchingLayout = undefined;
43
+ const win = doc.defaultView;
44
+ if (!win || !banner.style || typeof win.ResizeObserver !== 'function')
45
+ return;
46
+ let frame = 0;
47
+ const schedule = () => {
48
+ if (frame)
49
+ return;
50
+ frame = win.requestAnimationFrame(() => {
51
+ frame = 0;
52
+ syncPromotoInsets(doc, banner);
53
+ });
54
+ };
55
+ const observer = new win.ResizeObserver(schedule);
56
+ const main = doc.querySelector('main');
57
+ if (main)
58
+ observer.observe(main);
59
+ observer.observe(doc.documentElement);
60
+ syncPromotoInsets(doc, banner);
61
+ stopWatchingLayout = () => {
62
+ observer.disconnect();
63
+ if (frame)
64
+ win.cancelAnimationFrame(frame);
65
+ };
66
+ }
67
+ function readSiteId(doc) {
68
+ const content = doc.querySelector('meta[name="hagilight-site-id"]')?.getAttribute('content')?.trim();
69
+ return content || undefined;
70
+ }
71
+ export function mountPromotoBanner(doc, locale) {
72
+ const footer = doc.querySelector('footer');
73
+ const previous = mounts.get(doc);
74
+ if (previous?.banner.isConnected && previous.footer === footer && previous.locale === locale) {
75
+ // The page layout may have been swapped under the same footer (docs to blog), so re-measure it.
76
+ watchPromotoLayout(doc, previous.banner);
77
+ return;
78
+ }
79
+ doc.querySelectorAll(ELEMENT_NAME).forEach((existing) => existing.remove());
80
+ const banner = doc.createElement(ELEMENT_NAME);
81
+ banner.className = 'hagilight-promoto';
82
+ banner.dataset.locale = locale;
83
+ const siteId = readSiteId(doc);
84
+ if (siteId)
85
+ banner.dataset.siteId = siteId;
86
+ banner.hidden = true;
87
+ banner.innerHTML = PROMOTO_SHELL_MARKUP;
88
+ doc.body.append(banner);
89
+ mounts.set(doc, { banner, footer, locale });
90
+ watchPromotoLayout(doc, banner);
91
+ }
92
+ export function onRouteDidUpdate() {
93
+ definePromotoBannerElement();
94
+ mountPromotoBanner(document, document.documentElement.lang || DEFAULT_LOCALE);
95
+ recordSiteVisit();
96
+ }
@@ -0,0 +1,2 @@
1
+ /** Picks the footer avatar once per page view, on the first load and after each client-side route change. */
2
+ export declare function onRouteDidUpdate(): void;
@@ -0,0 +1,5 @@
1
+ import { mountRandomImages } from '@hagicode/hagilight-core/random-image';
2
+ /** Picks the footer avatar once per page view, on the first load and after each client-side route change. */
3
+ export function onRouteDidUpdate() {
4
+ void mountRandomImages(document);
5
+ }
@@ -0,0 +1,42 @@
1
+ import type { HtmlTagObject, LoadContext } from '@docusaurus/types';
2
+ import type { BuildInfo } from '@hagicode/hagilight-core/build-info';
3
+ import { type RssFeedItem } from '@hagicode/hagilight-core/rss';
4
+ import type { ResolvedHagilightOptions, ResolvedRssOptions } from './options.js';
5
+ type Warn = (message: string) => void;
6
+ export interface DiscoveryReporter {
7
+ warn: Warn;
8
+ info: Warn;
9
+ }
10
+ export interface DiscoverySite {
11
+ title: string;
12
+ tagline: string;
13
+ url: string;
14
+ baseUrl: string;
15
+ siteDir: string;
16
+ i18n: LoadContext['i18n'];
17
+ }
18
+ export interface DiscoveryOutputs {
19
+ options: ResolvedHagilightOptions;
20
+ site: DiscoverySite;
21
+ /** Version of this package, recorded as the metadata generator. */
22
+ generatorVersion: string;
23
+ buildInfo: BuildInfo;
24
+ outDir: string;
25
+ production: boolean;
26
+ plugins: readonly {
27
+ name: string;
28
+ }[];
29
+ routesPaths: readonly string[];
30
+ routesBuildMetadata: Readonly<Record<string, {
31
+ noIndex: boolean;
32
+ } | undefined>>;
33
+ feedItems: readonly RssFeedItem[];
34
+ report: DiscoveryReporter;
35
+ }
36
+ export declare function sitemapUrls(routesPaths: readonly string[], routesBuildMetadata: DiscoveryOutputs['routesBuildMetadata'], siteUrl: string, localeBaseUrl: string): string[];
37
+ export declare function sitemapXml(urls: readonly string[]): string;
38
+ export declare function robotsTxt(sitemapUrlList: readonly string[]): string;
39
+ export declare function collectFeedItems(allContent: unknown, site: DiscoverySite, rss: ResolvedRssOptions, report: DiscoveryReporter): RssFeedItem[];
40
+ export declare function feedHeadTags(site: DiscoverySite, rss: ResolvedRssOptions, feedItemCount: number): HtmlTagObject[];
41
+ export declare function writeDiscoveryOutputs(outputs: DiscoveryOutputs): Promise<void>;
42
+ export {};