@hagicode/hagilight-starlight 0.18.0 → 0.19.1-dev.185.1.a85e2a8

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import { gaEventAttributes } from '@hagicode/hagilight-core/analytics-events';
3
- import { resolveSiteLinks } from '@hagicode/hagilight-core/links';
3
+ import { resolveSiteLinks, withCampaignUtm } from '@hagicode/hagilight-core/links';
4
4
  import { getArticlePromotionCopy } from './dist/article-promotion.js';
5
5
  import { randomPoolAttribute, randomPoolFromManifest } from '@hagicode/hagilight-core/random-image';
6
6
  import {
@@ -19,16 +19,26 @@ interface Props {
19
19
  lang?: string;
20
20
  /** Deal the illustration as a fan of random cat heads, like the footer avatar. Defaults to `true`. */
21
21
  randomAvatar?: boolean;
22
+ /** Identifies the hosting site in campaign tags; without it the call-to-action links stay untagged. */
23
+ siteId?: string;
22
24
  }
23
25
 
24
- const { lang, randomAvatar = true } = Astro.props;
26
+ const { lang, randomAvatar = true, siteId } = Astro.props;
25
27
  if (typeof randomAvatar !== 'boolean') throw new TypeError('Hagilight hagicodePromotion randomAvatar option must be a boolean.');
26
28
  const copy = getArticlePromotionCopy(lang);
27
29
  const siteLinks = resolveSiteLinks(lang);
28
30
  const linkHref = (id: string, fallback: string) =>
29
31
  [...siteLinks.header, ...siteLinks.quick].find((link) => link.id === id)?.href ?? fallback;
30
- const homeHref = linkHref('home', 'https://www.hagicode.com/');
31
- const docsHref = linkHref('productDocs', 'https://docs.hagicode.com/');
32
+ // The campaign is the link's stable id; the Store badge is third-party and is not tagged.
33
+ const taggedHref = (id: string, fallback: string) => withCampaignUtm(linkHref(id, fallback), {
34
+ siteId,
35
+ medium: 'article',
36
+ campaign: id,
37
+ content: 'article_promotion',
38
+ origin: Astro.site?.origin,
39
+ });
40
+ const homeHref = taggedHref('home', 'https://www.hagicode.com/');
41
+ const docsHref = taggedHref('productDocs', 'https://docs.hagicode.com/');
32
42
  const storeHref = linkHref('microsoftStore', 'https://apps.microsoft.com/detail/9N3PM0N3SVDW');
33
43
  const downloadsHref = linkHref('downloadClient', 'https://www.hagicode.com/');
34
44
  const storeProductId = resolveMicrosoftStoreProductId(storeHref);
@@ -19,12 +19,14 @@ const labels = getContentWidthLabels(lang);
19
19
  setContentWidth,
20
20
  synchronizeContentWidthFromStorage,
21
21
  } from './dist/content-width.js';
22
+ import { capturePostHogEvent } from '@hagicode/hagilight-core/posthog-events';
22
23
 
23
24
  const root = document.documentElement;
24
25
  restoreContentWidth(root);
25
26
  document.querySelectorAll('[data-hagilight-content-width-choice]').forEach((button) => {
26
27
  button.addEventListener('click', () => {
27
28
  setContentWidth(button.dataset.hagilightContentWidthChoice, root);
29
+ capturePostHogEvent('content_width_changed', { choice: button.dataset.hagilightContentWidthChoice });
28
30
  });
29
31
  });
30
32
  window.addEventListener('storage', (event) => synchronizeContentWidthFromStorage(event, root));
package/Footer.astro CHANGED
@@ -5,6 +5,7 @@ import DefaultFooter from '@astrojs/starlight/components/Footer.astro';
5
5
  import { siteLinkGaAttributes } from '@hagicode/hagilight-core/analytics-events';
6
6
  import { resolveSiteLinks, type SiteLinksOptions } from '@hagicode/hagilight-core/links';
7
7
  import { buildInfoItems, type BuildInfo } from '@hagicode/hagilight-core/build-info';
8
+ import { resolveConsentCopy } from '@hagicode/hagilight-core/consent-copy';
8
9
  import manifest from './package.json';
9
10
 
10
11
  interface Props {
@@ -15,6 +16,8 @@ interface Props {
15
16
  buildInfo?: BuildInfo;
16
17
  avatar?: boolean;
17
18
  randomAvatar?: boolean;
19
+ /** Adds the "Cookie settings" entry. Set only when the site mounts the consent popup. Defaults to `false`. */
20
+ consent?: boolean;
18
21
  }
19
22
 
20
23
  const {
@@ -25,7 +28,9 @@ const {
25
28
  buildInfo,
26
29
  avatar,
27
30
  randomAvatar,
31
+ consent = false,
28
32
  } = Astro.props;
33
+ const consentCopy = resolveConsentCopy(locale);
29
34
  const buildItems = buildInfoItems(buildInfo);
30
35
  if (typeof manifest.version !== 'string' || manifest.version.trim().length === 0) {
31
36
  throw new Error('@hagicode/hagilight-starlight requires a nonempty string version in its package.json.');
@@ -50,7 +55,7 @@ const resolvedLinks = resolveSiteLinks(currentLanguage ?? locale, {
50
55
 
51
56
  <DefaultFooter />
52
57
  <div class="hagilight-brand-host">
53
- <FooterBrand locale={resolvedLinks.locale} links={resolvedLinks.quick} avatar={avatar} randomAvatar={randomAvatar} />
58
+ <FooterBrand locale={resolvedLinks.locale} links={resolvedLinks.quick} avatar={avatar} randomAvatar={randomAvatar} siteId={links?.siteId} siteUrl={links?.siteUrl ?? Astro.site?.href} />
54
59
  </div>
55
60
  <div class="hagilight-site-links">
56
61
  {resolvedLinks.relatedSites.length > 0 && (
@@ -112,6 +117,9 @@ const resolvedLinks = resolveSiteLinks(currentLanguage ?? locale, {
112
117
  aria-label={link.ariaLabel}
113
118
  >{link.label}</a>
114
119
  ))}
120
+ {consent && (
121
+ <button type="button" class="hagilight-consent-entry" data-consent-preferences>{consentCopy.footerEntry}</button>
122
+ )}
115
123
  </nav>
116
124
  </div>
117
125
  <p class="hagilight-copyright">
@@ -144,6 +152,8 @@ const resolvedLinks = resolveSiteLinks(currentLanguage ?? locale, {
144
152
  margin: 0 auto 1rem;
145
153
  max-width: 72rem;
146
154
  padding-inline: 1rem;
155
+ /* Keeps the last links scrollable clear of the consent popup; 0 when no popup is visible. */
156
+ padding-block-end: var(--hagilight-consent-clearance, 0px);
147
157
  }
148
158
 
149
159
  .hagilight-site-links section {
@@ -174,6 +184,17 @@ const resolvedLinks = resolveSiteLinks(currentLanguage ?? locale, {
174
184
  color: var(--sl-color-white);
175
185
  }
176
186
 
187
+ .hagilight-consent-entry {
188
+ margin: 0;
189
+ padding: 0;
190
+ border: 0;
191
+ background: none;
192
+ color: inherit;
193
+ font: inherit;
194
+ text-decoration: underline;
195
+ cursor: pointer;
196
+ }
197
+
177
198
  .hagilight-filings {
178
199
  grid-column: 1 / -1;
179
200
  display: flex;
@@ -119,6 +119,7 @@ const navigationOptions = options.map((option) => ({
119
119
  getKeyboardTargetIndex,
120
120
  persistStarlightLocaleSelection,
121
121
  } from './dist/language-routing.js';
122
+ import { capturePostHogEvent } from '@hagicode/hagilight-core/posthog-events';
122
123
 
123
124
  class HagilightLanguageChooser extends HTMLElement {
124
125
  connectedCallback() {
@@ -176,6 +177,8 @@ const navigationOptions = options.map((option) => ({
176
177
  if (!locale || !href) return;
177
178
  closeDialog();
178
179
  if (option.getAttribute('aria-selected') === 'true') return;
180
+ // Sent before navigation: the capture uses sendBeacon, so it survives the page unloading.
181
+ capturePostHogEvent('locale_changed', { from: document.documentElement.lang, to: locale });
179
182
  persistStarlightLocaleSelection(locale);
180
183
  const target = new URL(href, window.location.origin);
181
184
  target.search = window.location.search;
@@ -14,6 +14,7 @@ const {
14
14
  aiDisclosures = { isAITranslation: false, isAIAuthor: false, sourceLocale: 'root' },
15
15
  hagicodePromotionEnabled = true,
16
16
  hagicodePromotionRandomAvatar = true,
17
+ siteId,
17
18
  } = Astro.props;
18
19
  const route = Astro.locals.starlightRoute;
19
20
  const isDocsEntry = route.entry.id !== '404' && !route.entry.id.endsWith('/404');
@@ -42,4 +43,4 @@ if (showTranslation) {
42
43
  {flags.isAIAuthor && <AIDisclosureNotice kind="author" lang={route.lang} />}
43
44
  <DefaultMarkdownContent><slot /></DefaultMarkdownContent>
44
45
  {showTranslation && <AIDisclosureNotice kind="translation" lang={route.lang} sourceHref={sourceHref} />}
45
- {showHagicodePromotion && <ArticlePromotion lang={route.lang} randomAvatar={hagicodePromotionRandomAvatar} />}
46
+ {showHagicodePromotion && <ArticlePromotion lang={route.lang} randomAvatar={hagicodePromotionRandomAvatar} siteId={siteId} />}
@@ -19,7 +19,7 @@ const homeLabel = getNotFoundHomeLabel(route.lang);
19
19
 
20
20
  {
21
21
  isNotFound ? (
22
- <div class="not-found-hero">
22
+ <div class="not-found-hero" data-hagilight-not-found>
23
23
  <DefaultHero />
24
24
  <div class="recovery-action">
25
25
  <a class="sl-link-button primary" href={homeHref}>{homeLabel}</a>
@@ -30,6 +30,18 @@ const homeLabel = getNotFoundHomeLabel(route.lang);
30
30
  )
31
31
  }
32
32
 
33
+ <script>
34
+ import { capturePostHogEvent, referrerDomain } from '@hagicode/hagilight-core/posthog-events';
35
+
36
+ // Once per page view: the script runs once per load and only the not-found hero carries the marker.
37
+ if (document.querySelector('[data-hagilight-not-found]')) {
38
+ capturePostHogEvent('not_found_viewed', {
39
+ path: window.location.pathname,
40
+ referrer_domain: referrerDomain(document.referrer),
41
+ });
42
+ }
43
+ </script>
44
+
33
45
  <style>
34
46
  .not-found-hero {
35
47
  display: flex;
@@ -1,7 +1,14 @@
1
1
  ---
2
2
  import PromotoBanner from '@hagicode/hagilight-core/PromotoBanner';
3
3
  import Footer from './Footer.astro';
4
+
5
+ interface Props {
6
+ /** Identifies the hosting site in campaign tags for the banner's call to action. */
7
+ siteId?: string;
8
+ }
9
+
10
+ const { siteId } = Astro.props;
4
11
  ---
5
12
 
6
13
  <Footer />
7
- <PromotoBanner />
14
+ <PromotoBanner siteId={siteId} />
package/README.md CHANGED
@@ -31,7 +31,7 @@ The plugin also exposes `@hagicode/hagilight-starlight/schema` for optional SEO
31
31
 
32
32
  ### Analytics
33
33
 
34
- `hagilight()` enables PostHog without analytics configuration, an enabled flag, or a consumer project key. It uses `phc_qiVVihsL925ovhPYNdLJWz4k7aoKVMwYCevgZPE6PER5`, API host `https://us.i.posthog.com`, and SDK defaults `2026-05-30`. Google Analytics and PostHog render together in the generated global Head; 51LA stays in the Footer. PostHog covers all production pages, including localized and 404 pages, independently of Google's existing 404 exclusion. Development pages emit no PostHog loader.
34
+ `hagilight()` enables PostHog without analytics configuration, an enabled flag, or a consumer project key. It uses `phc_qiVVihsL925ovhPYNdLJWz4k7aoKVMwYCevgZPE6PER5`, API host `https://t.hagicode.com` (managed proxy), UI host `https://us.posthog.com`, person profiles `identified_only`, and SDK defaults `2026-05-30`. Sites with a strict script or connect policy must allow `t.hagicode.com`. Google Analytics and PostHog render together in the generated global Head; 51LA stays in the Footer. PostHog covers all production pages, including localized and 404 pages, independently of Google's existing 404 exclusion. Development pages emit no PostHog loader.
35
35
 
36
36
  PostHog options follow the Google Analytics provider convention:
37
37
 
@@ -43,6 +43,8 @@ PostHog options follow the Google Analytics provider convention:
43
43
  | `posthog: { enabled: true, apiKey: 'phc_Custom123' }` | Enabled with the custom key |
44
44
  | `posthog: { enabled: true }` | Configuration error: `apiKey` required |
45
45
  | `posthog: { enabled: false }` | Disabled independently of Google Analytics and 51LA |
46
+ | `posthog: { searchQuery: 'zero-results' }` | Enabled with the default key; search text is sent only for searches with no results |
47
+ | `posthog: { searchQuery: 'off' }` | Enabled with the default key; no search text in any event (the default) |
46
48
 
47
49
  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.
48
50
 
@@ -64,8 +66,14 @@ hagilight({
64
66
  });
65
67
  ```
66
68
 
69
+ `searchQuery` accepts `off` (default) or `zero-results`; any other value fails configuration with an error naming `analytics.posthog.searchQuery`. It never changes whether PostHog is enabled: an object that holds only `searchQuery` counts as omitted, so the default key stays on, while `{}` still disables it. Under `zero-results`, `search_used` carries `query` only when the search returned nothing and the text passes the secret and path filter; see the core README for the exact rules.
70
+
71
+ The Starlight plugin emits every core event: `not_found_viewed` (404 page), `search_used`, `search_result_clicked` (Pagefind search), `code_copied`, `article_promotion_impression` (end-of-article card), `locale_changed` (language chooser), `locale_redirected` (root transit), and the optional `theme_changed` and `content_width_changed` (theme picker and content width toggle). Docusaurus has no Pagefind search, so it sends no search events, and it has no article promotion impression or theme and width events.
72
+
67
73
  The shared component initializes once per document; SDK defaults own PostHog navigation behavior. Choose one initialization owner and disable this provider when the site already initializes PostHog. Google Analytics and 51LA keep their configured behavior when PostHog is disabled.
68
74
 
75
+ Each page registers its site context: `site_framework` `starlight`, the plugin version, `site_id` from `links.siteId` or the site URL's hostname, and a per-page `page_type` (splash pages are `home`, the not-found page is `404`, and other routes are derived from the path, defaulting to `other`). Marked links are captured as `cta_click` on every page that initializes PostHog, including the not-found page, whether or not Google Analytics is enabled.
76
+
69
77
  ### Theme picker
70
78
 
71
79
  By default the plugin replaces Starlight's light/dark toggle with a theme picker offering nine choices: the default theme plus the Ocean, Sakura, and Forest themes in light and dark, and a "Default" choice that follows the visitor's system color scheme and renders the Forest theme. First-time visitors, and visitors with an invalid saved choice, therefore see Forest; the other themes appear only after an explicit selection, which is remembered in `localStorage`.
@@ -171,8 +179,14 @@ The copy and assets mirror `repos/web` and `repos/docs` in the HagiCode monorepo
171
179
  - **Options:** `hagicodePromotion: { randomAvatar: false }` keeps the default illustration and loads no picker; `footer: { avatar: false }` removes the footer avatar, and `footer: { randomAvatar: false }` keeps its default. A site whose `img-src` policy blocks `https://img-gen.hagicode.com` keeps the defaults.
172
180
  - **Badge:** the product id is parsed from the Store URL and the badge language map lives in `src/windows-download.ts`. Re-check both against `repos/docs/src/components/MicrosoftStoreBadge.tsx` and the official site when the Store listing or the supported languages change.
173
181
 
182
+ Direct `/PromotoFooter` takes an optional `siteId` prop and passes it to its banner, so the banner's call to action carries campaign tags. The plugin footer does this automatically from `links.siteId`.
183
+
174
184
  The plugin footer and direct `/Footer` and `/PromotoFooter` components append
175
185
  the literal `Powered By hagilight-starlight@<version>`. The version comes from
176
186
  this package's own manifest at build time, not from Astro or Starlight.
177
187
 
178
188
  See the [repository documentation](https://github.com/HagiCode-org/hagilight#readme) for schema setup and feature options.
189
+
190
+ ### Consent
191
+
192
+ `analytics.consent` (`enabled`, `privacyUrl`, `policyVersion`) is on by default whenever a provider is enabled. The plugin mounts the popup through its generated Footer and Head, on every page including 404, and passes `consent` to the Google Analytics, 51LA and PostHog components. `enabled: false` restores immediate loading; invalid `privacyUrl` or `policyVersion` values fail with the option named. See the core README, "Analytics consent", for the storage names and rules.
package/ThemeSelect.astro CHANGED
@@ -78,6 +78,7 @@ const groupOptionLabel = (prefix: string, choice: ThemeChoice): string =>
78
78
 
79
79
  <script>
80
80
  import { applyThemeChoice, parseThemeChoice, readStoredThemeChoice, type ThemeChoice } from './dist/themes.js';
81
+ import { capturePostHogEvent } from '@hagicode/hagilight-core/posthog-events';
81
82
 
82
83
  const modeClass = (choice: ThemeChoice): string =>
83
84
  choice.endsWith('-light') ? 'mode-light' : choice.endsWith('-dark') ? 'mode-dark' : 'mode-auto';
@@ -102,6 +103,7 @@ const groupOptionLabel = (prefix: string, choice: ThemeChoice): string =>
102
103
  select.addEventListener('change', () => {
103
104
  const choice = parseThemeChoice(select.value);
104
105
  applyThemeChoice(choice);
106
+ capturePostHogEvent('theme_changed', { theme: choice });
105
107
  sync(choice);
106
108
  });
107
109
 
package/dist/index.d.ts CHANGED
@@ -31,12 +31,25 @@ export interface HagilightAnalyticsOptions {
31
31
  googleAnalytics?: HagilightGoogleAnalyticsOptions;
32
32
  fiftyOneLa?: HagilightFiftyOneLaOptions;
33
33
  posthog?: HagilightPostHogOptions;
34
+ consent?: HagilightConsentOptions;
35
+ }
36
+ export interface HagilightConsentOptions {
37
+ /** Defaults to on when any analytics provider is enabled. `false` restores immediate loading. */
38
+ enabled?: boolean;
39
+ /** An absolute HTTP(S) URL or a site-relative path. Omitted means the details have no privacy link. */
40
+ privacyUrl?: string;
41
+ /** Defaults to `1`. Changing it asks every visitor again. 1 to 32 letters, digits, dots, underscores or hyphens. */
42
+ policyVersion?: string;
34
43
  }
35
44
  export interface HagilightPostHogOptions {
36
45
  /** Defaults to true when the provider options are omitted, otherwise to whether apiKey is set. */
37
46
  enabled?: boolean;
38
47
  apiKey?: string;
48
+ /** `zero-results` sends the search text only for searches with no results. Defaults to `off`; never changes enablement. */
49
+ searchQuery?: PostHogSearchQueryOption;
39
50
  }
51
+ declare const POSTHOG_SEARCH_QUERY_OPTIONS: readonly ["off", "zero-results"];
52
+ export type PostHogSearchQueryOption = (typeof POSTHOG_SEARCH_QUERY_OPTIONS)[number];
40
53
  export interface HagilightSeoOptions {
41
54
  /** Replace Starlight's Head with Hagilight SEO metadata. Defaults to `true`; requires Astro `site`. */
42
55
  enabled?: boolean;
@@ -103,3 +116,4 @@ export interface HagilightStarlightOptions {
103
116
  }
104
117
  /** Starlight plugin that applies HagiCode header, footer, content, SEO, RSS, and analytics customizations. */
105
118
  export default function hagilight(options?: HagilightStarlightOptions): StarlightPlugin;
119
+ export {};
package/dist/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
2
3
  import { fileURLToPath } from 'node:url';
3
4
  import { resolveFaviconHeadEntry } from '@hagicode/hagilight-core/favicon';
4
5
  import { resolveBuildInfo } from '@hagicode/hagilight-core/build-info-resolver';
5
6
  import { resolveFooterAvatarOptions } from '@hagicode/hagilight-core/footer-avatar';
6
7
  import { checkContentImageRegistration, registerContentImages } from '@hagicode/hagilight-core/content-images';
7
8
  import { DEFAULT_POSTHOG_API_KEY, POSTHOG_API_KEY_PATTERN } from '@hagicode/hagilight-core/posthog';
9
+ import { validatePolicyVersion, validatePrivacyUrl, } from '@hagicode/hagilight-core/consent-markup';
8
10
  import { resolveRssLocales } from '@hagicode/hagilight-core/rss';
9
11
  import { RSS_OWNER, registerStarlightRssOwner, } from '@hagicode/hagilight-core/rss-ownership';
10
12
  import { isValidSeoImageReference, resolveSeoImageUrl } from '@hagicode/hagilight-core/seo';
@@ -14,7 +16,9 @@ import { resolveRssOptions } from './rss-utils.js';
14
16
  import { resolveSeoLocales } from './seo-utils.js';
15
17
  import { createStarlightMetadataFinalizer } from './site-metadata.js';
16
18
  import { THEME_BOOTSTRAP } from './themes.js';
19
+ const POSTHOG_SEARCH_QUERY_OPTIONS = ['off', 'zero-results'];
17
20
  const packageRoot = new URL('../', import.meta.url);
21
+ const PACKAGE_VERSION = JSON.parse(readFileSync(new URL('package.json', packageRoot), 'utf8')).version;
18
22
  const packageFile = (name) => fileURLToPath(new URL(name, packageRoot));
19
23
  const coreExport = (name) => fileURLToPath(import.meta.resolve(`@hagicode/hagilight-core/${name}`));
20
24
  function isRecord(value) {
@@ -53,6 +57,37 @@ const contentWidthHeadScript = `(() => {
53
57
  }
54
58
  document.documentElement.dataset.hagilightContentWidth = mode;
55
59
  })();`;
60
+ /**
61
+ * Consent is on by default whenever a provider is enabled, and `enabled: false` restores immediate loading. Invalid
62
+ * values throw from the core validators, which name the option.
63
+ */
64
+ function resolveConsentOptions(config, providers) {
65
+ const record = optionalRecord(config, 'analytics consent');
66
+ const requested = optionalBoolean(record, 'enabled', 'analytics consent enabled');
67
+ return {
68
+ enabled: providers.length > 0 && (requested ?? true),
69
+ privacyUrl: validatePrivacyUrl(record?.privacyUrl),
70
+ policyVersion: validatePolicyVersion(record?.policyVersion),
71
+ providers,
72
+ };
73
+ }
74
+ function resolvePostHogSearchQuery(value) {
75
+ if (value === undefined)
76
+ return 'off';
77
+ if (POSTHOG_SEARCH_QUERY_OPTIONS.includes(value))
78
+ return value;
79
+ throw new TypeError('Hagilight analytics.posthog.searchQuery must be one of: off, zero-results.');
80
+ }
81
+ /**
82
+ * Removes `searchQuery` before provider resolution, because it never changes whether PostHog is enabled.
83
+ * An object that held only `searchQuery` therefore counts as omitted, so the default key stays on.
84
+ */
85
+ function providerOptionsWithoutSearchQuery(config) {
86
+ if (!isRecord(config) || !Object.hasOwn(config, 'searchQuery'))
87
+ return config;
88
+ const { searchQuery: _searchQuery, ...rest } = config;
89
+ return Object.keys(rest).length === 0 ? undefined : rest;
90
+ }
56
91
  function resolveProvider(config, { name, idKey, pattern, defaultId }) {
57
92
  if (config !== undefined && !isRecord(config)) {
58
93
  throw new TypeError(`Hagilight ${name} analytics options must be an object.`);
@@ -240,13 +275,22 @@ import options from '${optionsId}';
240
275
  const analyticsImport = serializedOptions.fiftyOneLaId
241
276
  ? `import Analytics51LA from ${JSON.stringify(coreExport('Analytics51LA'))};`
242
277
  : '';
278
+ // Consent-off sites keep their generated markup unchanged; the attribute appears only when the popup is mounted.
279
+ const consentAttribute = serializedOptions.consentEnabled ? ' consent={true}' : '';
243
280
  const analyticsRender = serializedOptions.fiftyOneLaId
244
- ? '<Analytics51LA siteId={options.fiftyOneLaId} />'
281
+ ? `<Analytics51LA siteId={options.fiftyOneLaId}${consentAttribute} />`
282
+ : '';
283
+ const consentImport = serializedOptions.consentEnabled
284
+ ? `import CookieConsent from ${JSON.stringify(coreExport('CookieConsent'))};`
285
+ : '';
286
+ const consentRender = serializedOptions.consentEnabled
287
+ ? '<CookieConsent providers={options.consentProviders} privacyUrl={options.consentPrivacyUrl} policyVersion={options.consentPolicyVersion} locale={Astro.locals?.starlightRoute?.locale} />'
245
288
  : '';
246
289
  const footerSource = `---
247
290
  import Footer from ${JSON.stringify(footerPath)};
248
291
  ${promotionImport}
249
292
  ${analyticsImport}
293
+ ${consentImport}
250
294
  import options from '${optionsId}';
251
295
  ---
252
296
 
@@ -258,9 +302,11 @@ import options from '${optionsId}';
258
302
  buildInfo={options.buildInfo}
259
303
  avatar={options.footerAvatar.avatar}
260
304
  randomAvatar={options.footerAvatar.randomAvatar}
261
- />
305
+ ${consentAttribute ? ` consent={true}
306
+ ` : ''}/>
262
307
  ${promotionRender}
263
308
  ${analyticsRender}
309
+ ${consentRender}
264
310
  `;
265
311
  const pageTitleSource = `---
266
312
  import PageTitle from ${JSON.stringify(pageTitlePath)};
@@ -277,6 +323,7 @@ import options from '${optionsId}';
277
323
  aiDisclosures={options.aiDisclosures}
278
324
  hagicodePromotionEnabled={options.hagicodePromotionEnabled}
279
325
  hagicodePromotionRandomAvatar={options.hagicodePromotionRandomAvatar}
326
+ siteId={options.links?.siteId}
280
327
  ><slot /></MarkdownContent>
281
328
  `;
282
329
  const headSource = `---
@@ -284,14 +331,22 @@ import DefaultHead from '@astrojs/starlight/components/Head.astro';
284
331
  import SEOHead from ${JSON.stringify(seoHeadPath)};
285
332
  import GoogleAnalytics from ${JSON.stringify(coreExport('GoogleAnalytics'))};
286
333
  ${serializedOptions.posthogApiKey ? `import PostHog from ${JSON.stringify(coreExport('PostHog'))};` : ''}
334
+ import { classifyPagePath } from ${JSON.stringify(coreExport('posthog-events'))};
287
335
  import options from '${optionsId}';
288
336
  const isNotFound = /(?:^|\\/)404(?:\\.html)?\\/?$/u.test(Astro.url.pathname);
337
+ const route = Astro.locals.starlightRoute;
338
+ const pageType = isNotFound
339
+ ? '404'
340
+ : route?.entry?.data?.template === 'splash'
341
+ ? 'home'
342
+ : classifyPagePath(Astro.url.pathname, { base: import.meta.env.BASE_URL, locales: route?.locale ? [route.locale] : [] });
289
343
  ---
290
344
 
291
345
  {options.seoEnabled ? <SEOHead options={options} /> : <DefaultHead />}
292
- ${serializedOptions.posthogApiKey ? '<PostHog apiKey={options.posthogApiKey} />' : ''}
346
+ ${serializedOptions.posthogApiKey ? `<PostHog apiKey={options.posthogApiKey} framework="starlight" version={${JSON.stringify(PACKAGE_VERSION)}} siteId={options.links?.siteId} pageType={pageType}${serializedOptions.consentEnabled ? ' consent={true}' : ''} searchQuery={options.posthogSearchQuery} />` : ''}
347
+
293
348
  {!isNotFound && options.googleAnalyticsMeasurementId && (
294
- <GoogleAnalytics measurementId={options.googleAnalyticsMeasurementId} />
349
+ <GoogleAnalytics measurementId={options.googleAnalyticsMeasurementId}${serializedOptions.consentEnabled ? ' consent={true}' : ''} />
295
350
  )}
296
351
  `;
297
352
  const modules = new Map([
@@ -403,14 +458,22 @@ export default function hagilight(options = {}) {
403
458
  pattern: FIFTY_ONE_LA_ID_PATTERN,
404
459
  defaultId: DEFAULT_51LA_ID,
405
460
  });
406
- const posthog = optionalRecord(analytics.posthog, 'PostHog analytics');
461
+ const posthogSearchQuery = resolvePostHogSearchQuery(optionalRecord(analytics.posthog, 'PostHog analytics')?.searchQuery);
462
+ const posthogProviderOptions = providerOptionsWithoutSearchQuery(analytics.posthog);
463
+ const posthog = optionalRecord(posthogProviderOptions, 'PostHog analytics');
407
464
  optionalBoolean(posthog, 'enabled', 'PostHog analytics enabled');
408
- const posthogApiKey = resolveProvider(analytics.posthog, {
465
+ const posthogApiKey = resolveProvider(posthogProviderOptions, {
409
466
  name: 'PostHog',
410
467
  idKey: 'apiKey',
411
468
  pattern: POSTHOG_API_KEY_PATTERN,
412
469
  defaultId: DEFAULT_POSTHOG_API_KEY,
413
470
  });
471
+ const consentProviders = [
472
+ ...(googleAnalyticsMeasurementId ? ['googleAnalytics'] : []),
473
+ ...(fiftyOneLaId ? ['fiftyOneLa'] : []),
474
+ ...(posthogApiKey ? ['posthog'] : []),
475
+ ];
476
+ const consent = resolveConsentOptions(analytics.consent, consentProviders);
414
477
  const promoto = optionalRecord(input.promoto, 'promoto');
415
478
  const promotoEnabled = optionalBoolean(promoto, 'enabled', 'promoto enabled') !== false;
416
479
  const contentImages = optionalRecord(input.contentImages, 'contentImages');
@@ -442,6 +505,11 @@ export default function hagilight(options = {}) {
442
505
  googleAnalyticsMeasurementId,
443
506
  fiftyOneLaId,
444
507
  posthogApiKey,
508
+ consentEnabled: consent.enabled,
509
+ consentProviders: consent.providers,
510
+ consentPrivacyUrl: consent.privacyUrl,
511
+ consentPolicyVersion: consent.policyVersion,
512
+ posthogSearchQuery,
445
513
  aiDisclosures,
446
514
  seoEnabled: seo.enabled,
447
515
  seo,
@@ -544,7 +612,7 @@ export default function hagilight(options = {}) {
544
612
  fiftyOneLa: serializedOptions.fiftyOneLaId !== undefined,
545
613
  posthog: serializedOptions.posthogApiKey !== undefined,
546
614
  },
547
- features: { seo: seo.enabled, promoto: promotoEnabled },
615
+ features: { seo: seo.enabled, promoto: promotoEnabled, consent: consent.enabled },
548
616
  }, unregisterSiteMetadataOwner));
549
617
  updateConfig({
550
618
  ...(config.logo === undefined ? { logo: { src: defaultLogo, alt: 'HagiCode' } } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hagicode/hagilight-starlight",
3
- "version": "0.18.0",
3
+ "version": "0.19.1-dev.185.1.a85e2a8",
4
4
  "type": "module",
5
5
  "description": "Starlight plugin and components for HagiCode documentation",
6
6
  "repository": {
@@ -53,7 +53,7 @@
53
53
  "assets/showcase/*"
54
54
  ],
55
55
  "dependencies": {
56
- "@hagicode/hagilight-core": "0.18.0"
56
+ "@hagicode/hagilight-core": "0.19.1-dev.185.1.a85e2a8"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@astrojs/starlight": "^0.42.4",