@hagicode/hagilight-docusaurus 0.14.1-dev.146.1.e98244c → 0.14.1-dev.150.1.68cde77

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
@@ -60,6 +60,14 @@ Every option is optional. Feature toggles take an object with `enabled`, which d
60
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
61
  | `noIndex` | `false` | Stops this plugin from writing `sitemap.xml` and `robots.txt`. |
62
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
+
63
71
  ### Analytics defaults
64
72
 
65
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`.
@@ -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
+ }
@@ -7,4 +7,6 @@ export interface HagilightGlobalData {
7
7
  options: ResolvedHagilightOptions;
8
8
  i18n: LoadContext['i18n'];
9
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[];
10
12
  }
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
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';
4
6
  import { fiftyOneLaHeadTags, googleAnalyticsHeadTags, posthogHeadTags } from './analytics.js';
5
7
  import { collectFeedItems, feedHeadTags, writeDiscoveryOutputs } from './discovery.js';
6
8
  import { PLUGIN_NAME } from './global-data.js';
@@ -28,12 +30,16 @@ function promotoSiteIdHeadTags(options) {
28
30
  return [];
29
31
  return [{ tagName: 'meta', attributes: { name: 'hagilight-site-id', content: siteId } }];
30
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;
31
35
  function clientModules(options) {
32
36
  const analyticsEnabled = options.googleAnalyticsId !== undefined || options.fiftyOneLaId !== undefined;
37
+ const footerAvatarPicker = options.footer && options.footerAvatar.randomAvatar;
33
38
  return [
34
39
  ...(analyticsEnabled ? [packageFile('./client/analytics.js')] : []),
35
40
  ...(options.promoto ? [packageFile('./client/promoto.js'), packageFile('../promoto.css')] : []),
36
41
  ...(options.footer ? [packageFile('../styles.css')] : []),
42
+ ...(footerAvatarPicker ? [packageFile('./client/random-image.js')] : []),
37
43
  ];
38
44
  }
39
45
  function createPlugin(context, options) {
@@ -45,6 +51,9 @@ function createPlugin(context, options) {
45
51
  options,
46
52
  i18n: context.i18n,
47
53
  buildInfo: resolveBuildInfo({ root: context.siteDir }),
54
+ footerAvatarPool: options.footer && options.footerAvatar.randomAvatar
55
+ ? randomPoolFromManifest(footerAvatarManifest.pool)
56
+ : undefined,
48
57
  };
49
58
  const themeEnabled = options.footer || options.seo.enabled;
50
59
  const site = {
@@ -64,6 +73,18 @@ function createPlugin(context, options) {
64
73
  name: PLUGIN_NAME,
65
74
  ...(themeEnabled ? { getThemePath: () => packageFile('./theme') } : {}),
66
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
+ },
67
88
  contentLoaded({ actions }) {
68
89
  actions.setGlobalData(globalData);
69
90
  },
package/dist/options.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { SiteLinksOptions } from '@hagicode/hagilight-core/links';
2
+ import { type ResolvedFooterAvatarOptions } from '@hagicode/hagilight-core/footer-avatar';
2
3
  import { type SiteMetadataOption } from '@hagicode/hagilight-core/site-metadata';
3
4
  export interface FeatureToggle {
4
5
  enabled?: boolean;
@@ -36,6 +37,12 @@ export interface MetadataOptions extends FeatureToggle {
36
37
  /** Site description. Defaults to `siteConfig.tagline`. */
37
38
  description?: string;
38
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
+ }
39
46
  export interface HagilightDocusaurusOptions {
40
47
  /**
41
48
  * Write `.well-known/hagilight.metadata.json` at the end of the build. Defaults to `true`;
@@ -43,7 +50,7 @@ export interface HagilightDocusaurusOptions {
43
50
  * `static/.well-known/` is kept. Only the default locale build writes the file.
44
51
  */
45
52
  metadata?: boolean | MetadataOptions;
46
- footer?: FeatureToggle;
53
+ footer?: FooterOptions;
47
54
  links?: SiteLinksOptions;
48
55
  analytics?: AnalyticsOptions;
49
56
  promoto?: FeatureToggle;
@@ -66,6 +73,7 @@ export interface ResolvedRssOptions {
66
73
  }
67
74
  export interface ResolvedHagilightOptions {
68
75
  footer: boolean;
76
+ footerAvatar: ResolvedFooterAvatarOptions;
69
77
  links: SiteLinksOptions;
70
78
  googleAnalyticsId: string | undefined;
71
79
  fiftyOneLaId: string | undefined;
package/dist/options.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { resolveFooterAvatarOptions } from '@hagicode/hagilight-core/footer-avatar';
1
2
  import { DEFAULT_POSTHOG_API_KEY, POSTHOG_API_KEY_PATTERN } from '@hagicode/hagilight-core/posthog';
2
3
  import { isValidSeoImageReference, resolveAbsoluteHttpUrl } from '@hagicode/hagilight-core/seo';
3
4
  import { resolveSiteMetadataOption } from '@hagicode/hagilight-core/site-metadata';
@@ -25,6 +26,15 @@ function nonEmptyStringOption(value, path) {
25
26
  throw invalid(path, 'must be a non-empty string');
26
27
  return value;
27
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
+ }
28
38
  function featureEnabled(value, path) {
29
39
  if (value === undefined)
30
40
  return true;
@@ -89,6 +99,7 @@ export function resolveOptions(input) {
89
99
  const analytics = options.analytics === undefined ? {} : objectOption(options.analytics, 'analytics');
90
100
  return {
91
101
  footer: featureEnabled(options.footer, 'footer'),
102
+ footerAvatar: resolveFooterAvatar(options.footer),
92
103
  links: options.links === undefined ? {} : objectOption(options.links, 'links'),
93
104
  googleAnalyticsId: resolveProvider(analytics.googleAnalytics, 'analytics.googleAnalytics', 'measurementId', GOOGLE_ANALYTICS_ID_PATTERN, DEFAULT_GOOGLE_ANALYTICS_ID),
94
105
  fiftyOneLaId: resolveProvider(analytics.fiftyOneLa, 'analytics.fiftyOneLa', 'siteId', FIFTY_ONE_LA_ID_PATTERN, DEFAULT_FIFTY_ONE_LA_ID),
@@ -1,10 +1,17 @@
1
1
  import { type ReactElement } from 'react';
2
2
  import { type BuildInfo } from '@hagicode/hagilight-core/build-info';
3
3
  import type { FooterLinks } from '../../footer.js';
4
+ /** The decorative avatar: the vendored default pair and, when the random option is on, the baked pool. */
5
+ export interface FooterAvatar {
6
+ avif: string;
7
+ webp: string;
8
+ randomPool?: readonly string[];
9
+ }
4
10
  export interface FooterViewProps {
5
11
  links: FooterLinks;
6
12
  version: string;
7
13
  year: number;
8
14
  buildInfo?: BuildInfo;
15
+ avatar?: FooterAvatar;
9
16
  }
10
- export declare function FooterView({ links, version, year, buildInfo }: FooterViewProps): ReactElement;
17
+ export declare function FooterView({ links, version, year, buildInfo, avatar }: FooterViewProps): ReactElement;
@@ -3,16 +3,18 @@ import { Fragment } from 'react';
3
3
  import { siteLinkGaAttributes } from '@hagicode/hagilight-core/analytics-events';
4
4
  import { buildInfoItems } from '@hagicode/hagilight-core/build-info';
5
5
  import { pickFooterBrandLinks, resolveFooterBrand } from '@hagicode/hagilight-core/footer-brand';
6
+ import { footerAvatarImageAttributes } from '@hagicode/hagilight-core/footer-avatar';
7
+ import { randomPoolAttribute } from '@hagicode/hagilight-core/random-image';
6
8
  function LinkSection({ heading, navLabel, links, tagged }) {
7
9
  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))) }) })] }));
8
10
  }
9
- function FooterBrand({ locale, links }) {
11
+ function FooterBrand({ locale, links, avatar }) {
10
12
  const brand = resolveFooterBrand(locale);
11
13
  const { primary, secondary } = pickFooterBrandLinks(links);
12
- return (_jsxs("section", { className: "hagilight-brand", lang: brand.locale, "aria-labelledby": "hagilight-brand-title", children: [_jsxs("div", { className: "hagilight-brand__lead", children: [_jsx("h2", { className: "hagilight-brand__headline", id: "hagilight-brand-title", children: brand.headline }), _jsxs("div", { className: "hagilight-brand__body", children: [_jsx("p", { className: "hagilight-brand__summary", children: brand.summary }), (primary || secondary) && (_jsxs("div", { className: "hagilight-brand__actions", children: [primary && (_jsxs("a", { className: "hagilight-brand__primary", href: primary.href, target: primary.target, rel: primary.rel, "aria-label": primary.ariaLabel, ...siteLinkGaAttributes(primary, 'footer_brand'), children: [_jsx("span", { children: primary.label }), _jsx("svg", { className: "hagilight-brand__arrow", viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M2.5 8h10M8.5 3.5 13 8l-4.5 4.5", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), secondary && (_jsx("a", { className: "hagilight-brand__secondary", href: secondary.href, target: secondary.target, rel: secondary.rel, "aria-label": secondary.ariaLabel, ...siteLinkGaAttributes(secondary, 'footer_brand'), children: secondary.label }))] }))] })] }), _jsx("dl", { className: "hagilight-brand__pillars", children: brand.pillars.map((pillar, index) => (_jsxs("div", { className: "hagilight-brand__pillar", "data-pillar": pillar.id, style: { '--hagilight-brand-i': index }, children: [_jsx("dt", { children: pillar.label }), _jsx("dd", { children: pillar.description })] }, pillar.id))) })] }));
14
+ return (_jsxs("section", { className: "hagilight-brand", lang: brand.locale, "aria-labelledby": "hagilight-brand-title", children: [_jsxs("div", { className: "hagilight-brand__lead", children: [_jsxs("div", { className: "hagilight-brand__title", children: [avatar && (_jsxs("picture", { className: "hagilight-brand__avatar", "data-random-pool": avatar.randomPool && randomPoolAttribute(avatar.randomPool), children: [_jsx("source", { type: "image/avif", srcSet: avatar.avif }), _jsx("source", { type: "image/webp", srcSet: avatar.webp }), _jsx("img", { src: avatar.webp, ...footerAvatarImageAttributes() })] })), _jsx("h2", { className: "hagilight-brand__headline", id: "hagilight-brand-title", children: brand.headline })] }), _jsxs("div", { className: "hagilight-brand__body", children: [_jsx("p", { className: "hagilight-brand__summary", children: brand.summary }), (primary || secondary) && (_jsxs("div", { className: "hagilight-brand__actions", children: [primary && (_jsxs("a", { className: "hagilight-brand__primary", href: primary.href, target: primary.target, rel: primary.rel, "aria-label": primary.ariaLabel, ...siteLinkGaAttributes(primary, 'footer_brand'), children: [_jsx("span", { children: primary.label }), _jsx("svg", { className: "hagilight-brand__arrow", viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M2.5 8h10M8.5 3.5 13 8l-4.5 4.5", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), secondary && (_jsx("a", { className: "hagilight-brand__secondary", href: secondary.href, target: secondary.target, rel: secondary.rel, "aria-label": secondary.ariaLabel, ...siteLinkGaAttributes(secondary, 'footer_brand'), children: secondary.label }))] }))] })] }), _jsx("dl", { className: "hagilight-brand__pillars", children: brand.pillars.map((pillar, index) => (_jsxs("div", { className: "hagilight-brand__pillar", "data-pillar": pillar.id, style: { '--hagilight-brand-i': index }, children: [_jsx("dt", { children: pillar.label }), _jsx("dd", { children: pillar.description })] }, pillar.id))) })] }));
13
15
  }
14
- export function FooterView({ links, version, year, buildInfo }) {
16
+ export function FooterView({ links, version, year, buildInfo, avatar }) {
15
17
  const { labels } = links;
16
18
  const buildItems = buildInfoItems(buildInfo);
17
- return (_jsxs("footer", { className: "hagilight-footer", children: [_jsx(FooterBrand, { locale: links.locale, links: links.quick }), _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] }), buildItems.map((item) => (_jsxs(Fragment, { children: [' | ', _jsxs("span", { className: "hagilight-footer__build", "data-build-field": item.field, title: item.title, "data-build-commit": item.title, children: [item.label, ": ", item.datetime ? _jsx("time", { dateTime: item.datetime, children: item.text }) : item.text] })] }, item.field)))] })] }));
19
+ return (_jsxs("footer", { className: "hagilight-footer", children: [_jsx(FooterBrand, { locale: links.locale, links: links.quick, avatar: avatar }), _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] }), buildItems.map((item) => (_jsxs(Fragment, { children: [' | ', _jsxs("span", { className: "hagilight-footer__build", "data-build-field": item.field, title: item.title, "data-build-commit": item.title, children: [item.label, ": ", item.datetime ? _jsx("time", { dateTime: item.datetime, children: item.text }) : item.text] })] }, item.field)))] })] }));
18
20
  }
@@ -5,16 +5,21 @@ import { usePluginData } from '@docusaurus/useGlobalData';
5
5
  import { resolveFooterLinks } from '../../footer.js';
6
6
  import { PLUGIN_NAME } from '../../global-data.js';
7
7
  import { FooterView } from './FooterView.js';
8
+ import avatarAvif from '@hagicode/hagilight-core/assets/footer/avatar.avif';
9
+ import avatarWebp from '@hagicode/hagilight-core/assets/footer/avatar.webp';
8
10
  export default function Footer() {
9
11
  const { i18n, siteConfig } = useDocusaurusContext();
10
- const { version, options, buildInfo } = usePluginData(PLUGIN_NAME);
12
+ const { version, options, buildInfo, footerAvatarPool } = usePluginData(PLUGIN_NAME);
11
13
  if (!options.footer)
12
14
  return _jsx(FooterDefaults, {});
15
+ const avatar = options.footerAvatar.avatar
16
+ ? { avif: avatarAvif, webp: avatarWebp, randomPool: footerAvatarPool }
17
+ : undefined;
13
18
  const links = resolveFooterLinks({
14
19
  locale: i18n.currentLocale,
15
20
  baseUrl: siteConfig.baseUrl,
16
21
  links: options.links,
17
22
  rssEnabled: options.rss.enabled,
18
23
  });
19
- return (_jsx(FooterView, { links: links, version: version, year: new Date().getFullYear(), buildInfo: buildInfo }));
24
+ return (_jsx(FooterView, { links: links, version: version, year: new Date().getFullYear(), buildInfo: buildInfo, avatar: avatar }));
20
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hagicode/hagilight-docusaurus",
3
- "version": "0.14.1-dev.146.1.e98244c",
3
+ "version": "0.14.1-dev.150.1.68cde77",
4
4
  "type": "module",
5
5
  "description": "Docusaurus plugin for HagiCode site features: footer, analytics, promote card, locales, SEO, sitemap, robots.txt, and RSS",
6
6
  "repository": {
@@ -38,7 +38,7 @@
38
38
  ],
39
39
  "dependencies": {
40
40
  "@docusaurus/types": "^3.9.2",
41
- "@hagicode/hagilight-core": "0.14.1-dev.146.1.e98244c"
41
+ "@hagicode/hagilight-core": "0.14.1-dev.150.1.68cde77"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@docusaurus/core": "^3.9.2",
package/styles.css CHANGED
@@ -112,6 +112,36 @@
112
112
  gap: 1.5rem 3.5rem;
113
113
  }
114
114
 
115
+ .hagilight-footer .hagilight-brand__title {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 1.25rem;
119
+ min-width: 0;
120
+ }
121
+
122
+ /* The decorative avatar keeps a fixed 64 px box, so a random swap never moves the headline. */
123
+ .hagilight-footer .hagilight-brand__avatar {
124
+ display: block;
125
+ flex: none;
126
+ width: 64px;
127
+ height: 64px;
128
+ }
129
+
130
+ .hagilight-footer .hagilight-brand__avatar img {
131
+ display: block;
132
+ width: 100%;
133
+ height: 100%;
134
+ border-radius: 0.5rem;
135
+ object-fit: cover;
136
+ }
137
+
138
+ /* Hidden in a brand block narrower than 360 px, where the headline needs the full width. */
139
+ @container hagilight-brand (max-width: 359.98px) {
140
+ .hagilight-footer .hagilight-brand__avatar {
141
+ display: none;
142
+ }
143
+ }
144
+
115
145
  .hagilight-footer .hagilight-brand__headline {
116
146
  margin: 0;
117
147
  color: var(--_heading);