@hagicode/hagilight-docusaurus 0.20.1-dev.196.1.d67f4e4 → 0.20.1-dev.199.1.c17b214

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
@@ -109,6 +109,40 @@ Docusaurus sends `not_found_viewed` from its 404 page, `code_copied` from its co
109
109
  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. The head registers the site context (`site_framework` `docusaurus`, the plugin version, and `site_id` from `links.siteId` or the site URL's hostname) once. On every route update, including the first page load, the plugin refreshes only `locale` and `page_type` through `posthog.register`, with `page_type` derived from the path, so the 404 page reports a path-derived type rather than `other`. The analytics client module is bundled whenever PostHog is enabled, even when Google Analytics and 51LA are disabled, so marked links are captured as `cta_click` without Google Analytics. Google Analytics and 51LA keep their navigation behavior.
110
110
 
111
111
 
112
+ ### Regional consent
113
+
114
+ > Legal notice: this is a product configuration, not legal advice. The default country set, Brazil's inclusion and the default hiding of "Cookie settings" for not-required visitors need confirmation by counsel, and site privacy notices may need matching updates. The `features.consentMode` metadata reports `globalRequired` or `regional`.
115
+
116
+ Regional consent starts analytics without a click for visitors whose country does not require consent. It is off unless `analytics.consent.regions` is set. Without it every visitor sees the popup first, and the output is unchanged.
117
+
118
+ | Option | Default | Effect |
119
+ | --- | --- | --- |
120
+ | `regions` | absent (global consent) | `'default'` for the built-in country set, or an array of upper-case ISO 3166-1 alpha-2 codes that replaces it. Setting it enables regional mode. |
121
+ | `countryCode` | none | One upper-case ISO code that overrides detection. Needs `regions`. |
122
+ | `regionHook` | none | Name of a function on `window`, such as `"siteRegion"`. It returns `'required'`, `'notRequired'`, `'unknown'`, a boolean, or a promise of one of these. Needs `regions`. |
123
+ | `settingsEntry` | `'requiredOnly'` | `'always'` shows the footer "Cookie settings" entry to every visitor. Needs `regions`. |
124
+ | `sessionReplay` | `'optIn'` | `'projectDefault'` lets replay follow the PostHog project. `'off'` removes the replay entry. Needs `regions`. |
125
+
126
+ ```js
127
+ hagilightDocusaurus({
128
+ analytics: { consent: { regions: 'default', settingsEntry: 'requiredOnly', sessionReplay: 'optIn' } },
129
+ });
130
+ ```
131
+
132
+ Invalid values fail at configuration time with a `TypeError` that names the option. The messages come from the core validator, so they match the Starlight and Astro integrations. A regional-only option without `regions` fails, and so does `regions` with `enabled: false`.
133
+
134
+ **Default country set.** `'default'` requires consent for the EU27, the EEA (IS, LI, NO), GB, CH and BR. This is a product configuration, not a legal determination. A list replaces the whole set.
135
+
136
+ **Sources.** The decision uses the first answer from: `countryCode`, `regionHook`, the same-origin `/cdn-cgi/trace` endpoint (only its `loc` line is read, with a 1200 ms timeout), and a coarse time-zone fallback. The whole decision has a 1800 ms deadline. It is resolved once per document, so client-side navigation does not resolve it again, and each provider starts once.
137
+
138
+ **Fail-closed.** `unknown` requires consent: no country, an unrecognized hook result, a timeout, an invalid `countryCode`, or an uncertain time zone. The popup appears, and no provider loads until the visitor accepts.
139
+
140
+ **Cloudflare trace and `www`.** `/cdn-cgi/trace` answers only for hostnames proxied through Cloudflare. `www.hagicode.com` returned no `loc` when verified, so on a `www` host the time-zone fallback decides unless `countryCode` or `regionHook` is set.
141
+
142
+ **Session replay.** With `sessionReplay: 'optIn'`, a not-required visitor gets replay off and an "Allow session replay" footer entry. Replay stays off until the visitor allows it. The settings and replay entries belong to the Hagilight footer, so `footer: { enabled: false }` removes them.
143
+
144
+ Google Consent Mode and cookieless pings stay off, and no `gtag` consent default is added. Turning regional mode on changes what GA4 and PostHog collect, so annotate the deployment date in GA4.
145
+
112
146
  ### SEO
113
147
 
114
148
  - `seo.image` is an absolute HTTP(S) URL or a site-root path. When it is unset, `themeConfig.image` is the fallback sharing image.
@@ -194,4 +228,4 @@ The demo site for this package lives in the monorepo at `examples/demo-docusauru
194
228
 
195
229
  ### Consent
196
230
 
197
- `analytics.consent` (`enabled`, `privacyUrl`, `policyVersion`) mirrors the Starlight option. With consent on, providers leave inert descriptors in the head, the client module mounts the popup and gate, PostHog's click sink installs only after a grant, and the swizzled footer shows the localized "Cookie settings" entry. The popup styles ship in `consent.css`. `enabled: false` restores immediate loading.
231
+ `analytics.consent` (`enabled`, `privacyUrl`, `policyVersion`) mirrors the Starlight option. With consent on, providers leave inert descriptors in the head, the client module mounts the popup and gate, PostHog's click sink installs only after a grant, and the swizzled footer shows the localized "Cookie settings" entry. The popup styles ship in `consent.css`. `enabled: false` restores immediate loading. Regional options (`regions` and the four related options) are under [Regional consent](#regional-consent).
package/dist/discovery.js CHANGED
@@ -221,7 +221,12 @@ async function writeSiteMetadataFile(outputs) {
221
221
  fiftyOneLa: production && options.fiftyOneLaId !== undefined,
222
222
  posthog: production && options.posthogApiKey !== undefined,
223
223
  },
224
- features: { seo: options.seo.enabled, promoto: options.promoto, consent: options.consent.enabled },
224
+ features: {
225
+ seo: options.seo.enabled,
226
+ promoto: options.promoto,
227
+ consent: options.consent.enabled,
228
+ consentMode: options.consent.enabled ? (options.consent.regional ? 'regional' : 'globalRequired') : undefined,
229
+ },
225
230
  });
226
231
  writeSiteMetadata({
227
232
  outDir,
package/dist/footer.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { resolveSiteLinks, type SiteLinksOptions } from '@hagicode/hagilight-core/links';
2
+ import type { ResolvedConsentOptions } from './options.js';
2
3
  export interface FooterLinksInput {
3
4
  locale: string;
4
5
  baseUrl: string;
@@ -7,3 +8,12 @@ export interface FooterLinksInput {
7
8
  }
8
9
  export type FooterLinks = ReturnType<typeof resolveSiteLinks>;
9
10
  export declare function resolveFooterLinks({ locale, baseUrl, links, rssEnabled }: FooterLinksInput): FooterLinks;
11
+ export interface FooterConsentEntries {
12
+ /** The "Cookie settings" label. Absent when consent is off, so no entry renders. */
13
+ consentLabel: string | undefined;
14
+ /** A regional site: the settings entry renders hidden until the consent popup reveals it. */
15
+ regional: boolean;
16
+ /** The session replay entry label, for a regional site with replay opt-in. */
17
+ replayLabel: string | undefined;
18
+ }
19
+ export declare function resolveFooterConsent(consent: ResolvedConsentOptions, locale: string): FooterConsentEntries;
package/dist/footer.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { resolveConsentCopy } from '@hagicode/hagilight-core/consent-copy';
1
2
  import { resolveSiteLinks } from '@hagicode/hagilight-core/links';
2
3
  export function resolveFooterLinks({ locale, baseUrl, links, rssEnabled }) {
3
4
  const sitemap = links.overrides?.sitemap;
@@ -10,3 +11,14 @@ export function resolveFooterLinks({ locale, baseUrl, links, rssEnabled }) {
10
11
  },
11
12
  });
12
13
  }
14
+ export function resolveFooterConsent(consent, locale) {
15
+ if (!consent.enabled)
16
+ return { consentLabel: undefined, regional: false, replayLabel: undefined };
17
+ const copy = resolveConsentCopy(locale);
18
+ const regional = consent.regional !== undefined;
19
+ return {
20
+ consentLabel: copy.footerEntry,
21
+ regional,
22
+ replayLabel: regional && consent.regional?.sessionReplay === 'optIn' ? copy.replayEntry : undefined,
23
+ };
24
+ }
package/dist/index.js CHANGED
@@ -43,6 +43,7 @@ function analyticsHeadTags(options, site) {
43
43
  providers: options.consent.providers,
44
44
  privacyUrl: options.consent.privacyUrl,
45
45
  policyVersion: options.consent.policyVersion,
46
+ regional: options.consent.regional,
46
47
  })] : []),
47
48
  ];
48
49
  }
package/dist/options.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { SiteLinksOptions } from '@hagicode/hagilight-core/links';
2
2
  import { type ResolvedFooterAvatarOptions } from '@hagicode/hagilight-core/footer-avatar';
3
- import { type ConsentPopupOptions } from '@hagicode/hagilight-core/consent-markup';
3
+ import { type ConsentPopupOptions, type RegionalPopupOptions } from '@hagicode/hagilight-core/consent-markup';
4
4
  import { type SiteMetadataOption } from '@hagicode/hagilight-core/site-metadata';
5
5
  export interface FeatureToggle {
6
6
  enabled?: boolean;
@@ -24,12 +24,24 @@ export interface ConsentOptions {
24
24
  privacyUrl?: string;
25
25
  /** Defaults to `1`. Changing it asks every visitor again. */
26
26
  policyVersion?: string;
27
+ /** `default` applies the built-in country set; a list of ISO 3166-1 alpha-2 codes replaces it. Setting it enables regional consent. */
28
+ regions?: 'default' | readonly string[];
29
+ /** One ISO 3166-1 alpha-2 code for every visitor. Needs `regions`. */
30
+ countryCode?: string;
31
+ /** The name of a function defined on `window`, such as "siteRegion". Needs `regions`. */
32
+ regionHook?: string;
33
+ /** `requiredOnly` (default) shows the settings entry only where consent is required. `always` shows it everywhere. Needs `regions`. */
34
+ settingsEntry?: 'requiredOnly' | 'always';
35
+ /** `optIn` (default) offers session replay as a separate choice. `projectDefault` or `off` change that. Needs `regions`. */
36
+ sessionReplay?: 'optIn' | 'projectDefault' | 'off';
27
37
  }
28
38
  export interface ResolvedConsentOptions {
29
39
  enabled: boolean;
30
40
  privacyUrl: string | undefined;
31
41
  policyVersion: string;
32
42
  providers: ConsentPopupOptions['providers'];
43
+ /** Set only when a regional option is given; its absence keeps the global consent output unchanged. */
44
+ regional: RegionalPopupOptions | undefined;
33
45
  }
34
46
  export interface PostHogOptions extends FeatureToggle {
35
47
  apiKey?: string;
package/dist/options.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { resolveFooterAvatarOptions } from '@hagicode/hagilight-core/footer-avatar';
2
2
  import { DEFAULT_POSTHOG_API_KEY, POSTHOG_API_KEY_PATTERN } from '@hagicode/hagilight-core/posthog';
3
- import { validatePolicyVersion, validatePrivacyUrl, } from '@hagicode/hagilight-core/consent-markup';
3
+ import { resolveRegionalOptions, validatePolicyVersion, validatePrivacyUrl, } from '@hagicode/hagilight-core/consent-markup';
4
4
  import { isValidSeoImageReference, resolveAbsoluteHttpUrl } from '@hagicode/hagilight-core/seo';
5
5
  import { resolveSiteMetadataOption } from '@hagicode/hagilight-core/site-metadata';
6
6
  import { DEFAULT_FIFTY_ONE_LA_ID, DEFAULT_GOOGLE_ANALYTICS_ID, FIFTY_ONE_LA_ID_PATTERN, GOOGLE_ANALYTICS_ID_PATTERN, } from './analytics.js';
@@ -122,10 +122,13 @@ function resolveRss(value) {
122
122
  function resolveConsent(config, providers) {
123
123
  const record = config === undefined ? {} : objectOption(config, 'analytics.consent');
124
124
  const requested = booleanOption(record.enabled, 'analytics.consent.enabled');
125
+ const enabled = providers.length > 0 && (requested ?? true);
125
126
  return {
126
- enabled: providers.length > 0 && (requested ?? true),
127
+ enabled,
127
128
  privacyUrl: validatePrivacyUrl(record.privacyUrl),
128
129
  policyVersion: validatePolicyVersion(record.policyVersion),
130
+ // The core validator names each option, so the messages match the Starlight and Astro integrations.
131
+ regional: resolveRegionalOptions(record, record.enabled !== false),
129
132
  providers,
130
133
  };
131
134
  }
@@ -19,5 +19,9 @@ export interface FooterViewProps {
19
19
  siteUrl?: string;
20
20
  /** The localized "Cookie settings" label. Set only when the consent popup is mounted. */
21
21
  consentLabel?: string;
22
+ /** A regional site: the "Cookie settings" entry renders hidden and the consent popup reveals it. */
23
+ regional?: boolean;
24
+ /** The session replay entry label. Set only for a regional site with replay opt-in, alongside `consentLabel`. */
25
+ replayLabel?: string;
22
26
  }
23
- export declare function FooterView({ links, version, year, buildInfo, avatar, siteId, siteUrl, consentLabel }: FooterViewProps): ReactElement;
27
+ export declare function FooterView({ links, version, year, buildInfo, avatar, siteId, siteUrl, consentLabel, regional, replayLabel }: FooterViewProps): ReactElement;
@@ -30,8 +30,8 @@ function FooterBrand({ locale, links, avatar, siteId, siteUrl }) {
30
30
  });
31
31
  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-fan": 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: brandHref(primary), 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: brandHref(secondary), 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))) })] }));
32
32
  }
33
- export function FooterView({ links, version, year, buildInfo, avatar, siteId, siteUrl, consentLabel }) {
33
+ export function FooterView({ links, version, year, buildInfo, avatar, siteId, siteUrl, consentLabel, regional, replayLabel }) {
34
34
  const { labels } = links;
35
35
  const buildItems = buildInfoItems(buildInfo);
36
- return (_jsxs("footer", { className: "hagilight-footer", children: [_jsx(FooterBrand, { locale: links.locale, links: links.quick, avatar: avatar, siteId: siteId, siteUrl: siteUrl }), _jsx("div", { className: "hagilight-footer__sections", children: _jsx("div", { className: "hagilight-cols", children: links.columns.map((column, index) => (_jsxs("nav", { className: "hagilight-col", "data-column": column.id, style: { '--hagilight-col-i': index }, "aria-labelledby": `hagilight-col-${column.id}`, children: [_jsx("h2", { id: `hagilight-col-${column.id}`, children: labels.columns[column.id] }), _jsx("ul", { children: column.entries.map((entry) => (_jsx("li", { children: _jsx(ColumnLink, { entry: entry }) }, entry.id))) })] }, column.id))) }) }), _jsxs("div", { className: "hagilight-footer__base", children: [links.feeds.length > 0 && (_jsx("nav", { className: "hagilight-footer__feeds", "aria-label": labels.navigation.feeds, children: links.feeds.map((entry) => (_jsx(ColumnLink, { entry: entry }, entry.id))) })), (links.filings.length > 0 || consentLabel) && (_jsxs("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))), consentLabel && (_jsx("button", { type: "button", className: "hagilight-footer__consent", "data-consent-preferences": true, children: consentLabel }))] }))] }), _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)))] })] }));
36
+ return (_jsxs("footer", { className: "hagilight-footer", children: [_jsx(FooterBrand, { locale: links.locale, links: links.quick, avatar: avatar, siteId: siteId, siteUrl: siteUrl }), _jsx("div", { className: "hagilight-footer__sections", children: _jsx("div", { className: "hagilight-cols", children: links.columns.map((column, index) => (_jsxs("nav", { className: "hagilight-col", "data-column": column.id, style: { '--hagilight-col-i': index }, "aria-labelledby": `hagilight-col-${column.id}`, children: [_jsx("h2", { id: `hagilight-col-${column.id}`, children: labels.columns[column.id] }), _jsx("ul", { children: column.entries.map((entry) => (_jsx("li", { children: _jsx(ColumnLink, { entry: entry }) }, entry.id))) })] }, column.id))) }) }), _jsxs("div", { className: "hagilight-footer__base", children: [links.feeds.length > 0 && (_jsx("nav", { className: "hagilight-footer__feeds", "aria-label": labels.navigation.feeds, children: links.feeds.map((entry) => (_jsx(ColumnLink, { entry: entry }, entry.id))) })), (links.filings.length > 0 || consentLabel) && (_jsxs("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))), consentLabel && (_jsx("button", { type: "button", className: "hagilight-footer__consent", "data-consent-preferences": true, "data-consent-entry": regional ? 'settings' : undefined, hidden: regional, children: consentLabel })), consentLabel && replayLabel && (_jsx("button", { type: "button", className: "hagilight-footer__consent", "data-consent-preferences": true, "data-consent-mode": "replay", "data-consent-entry": "replay", hidden: true, children: replayLabel }))] }))] }), _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)))] })] }));
37
37
  }
@@ -2,8 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import FooterDefaults from '@theme-init/Footer';
3
3
  import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
4
4
  import { usePluginData } from '@docusaurus/useGlobalData';
5
- import { resolveFooterLinks } from '../../footer.js';
6
- import { resolveConsentCopy } from '@hagicode/hagilight-core/consent-copy';
5
+ import { resolveFooterConsent, resolveFooterLinks } from '../../footer.js';
7
6
  import { PLUGIN_NAME } from '../../global-data.js';
8
7
  import { FooterView } from './FooterView.js';
9
8
  import avatarAvif from '@hagicode/hagilight-core/assets/footer/avatar.avif';
@@ -13,6 +12,7 @@ export default function Footer() {
13
12
  const { version, options, buildInfo, footerAvatarPool } = usePluginData(PLUGIN_NAME);
14
13
  if (!options.footer)
15
14
  return _jsx(FooterDefaults, {});
15
+ const consent = resolveFooterConsent(options.consent, i18n.currentLocale);
16
16
  const avatar = options.footerAvatar.avatar
17
17
  ? { avif: avatarAvif, webp: avatarWebp, randomPool: footerAvatarPool }
18
18
  : undefined;
@@ -22,5 +22,5 @@ export default function Footer() {
22
22
  links: options.links,
23
23
  rssEnabled: options.rss.enabled,
24
24
  });
25
- return (_jsx(FooterView, { links: links, version: version, year: new Date().getFullYear(), buildInfo: buildInfo, avatar: avatar, siteId: options.links.siteId, siteUrl: options.links.siteUrl ?? siteConfig.url, consentLabel: options.consent.enabled ? resolveConsentCopy(i18n.currentLocale).footerEntry : undefined }));
25
+ return (_jsx(FooterView, { links: links, version: version, year: new Date().getFullYear(), buildInfo: buildInfo, avatar: avatar, siteId: options.links.siteId, siteUrl: options.links.siteUrl ?? siteConfig.url, consentLabel: consent.consentLabel, regional: consent.regional, replayLabel: consent.replayLabel }));
26
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hagicode/hagilight-docusaurus",
3
- "version": "0.20.1-dev.196.1.d67f4e4",
3
+ "version": "0.20.1-dev.199.1.c17b214",
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": {
@@ -39,7 +39,7 @@
39
39
  ],
40
40
  "dependencies": {
41
41
  "@docusaurus/types": "^3.9.2",
42
- "@hagicode/hagilight-core": "0.20.1-dev.196.1.d67f4e4"
42
+ "@hagicode/hagilight-core": "0.20.1-dev.199.1.c17b214"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@docusaurus/core": "^3.9.2",