@hagicode/hagilight-starlight 0.17.1-dev.175.1.77872ad → 0.19.1-dev.184.1.830a4c6
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/ArticlePromotion.astro +14 -4
- package/ContentLayoutToggle.astro +2 -0
- package/Footer.astro +22 -1
- package/LanguageChooser.astro +3 -0
- package/MarkdownContent.astro +2 -1
- package/NotFoundHero.astro +13 -1
- package/PromotoFooter.astro +8 -1
- package/README.md +15 -1
- package/ThemeSelect.astro +2 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +75 -7
- package/package.json +2 -2
package/ArticlePromotion.astro
CHANGED
|
@@ -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
|
-
|
|
31
|
-
const
|
|
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;
|
package/LanguageChooser.astro
CHANGED
|
@@ -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;
|
package/MarkdownContent.astro
CHANGED
|
@@ -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} />}
|
package/NotFoundHero.astro
CHANGED
|
@@ -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;
|
package/PromotoFooter.astro
CHANGED
|
@@ -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.
|
|
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
|
-
?
|
|
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 ?
|
|
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
|
|
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(
|
|
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.
|
|
3
|
+
"version": "0.19.1-dev.184.1.830a4c6",
|
|
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.
|
|
56
|
+
"@hagicode/hagilight-core": "0.19.1-dev.184.1.830a4c6"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@astrojs/starlight": "^0.42.4",
|