@hagicode/hagilight-docusaurus 0.18.0 → 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/README.md +13 -2
- package/consent.css +383 -0
- package/dist/analytics.d.ts +7 -1
- package/dist/analytics.js +16 -3
- package/dist/client/analytics.d.ts +17 -0
- package/dist/client/analytics.js +44 -2
- package/dist/client/consent.d.ts +1 -0
- package/dist/client/consent.js +4 -0
- package/dist/client/posthog-interactions.d.ts +1 -0
- package/dist/client/posthog-interactions.js +13 -0
- package/dist/discovery.js +1 -1
- package/dist/index.js +45 -8
- package/dist/options.d.ts +21 -0
- package/dist/options.js +48 -3
- package/dist/theme/Footer/FooterView.d.ts +7 -1
- package/dist/theme/Footer/FooterView.js +21 -4
- package/dist/theme/Footer/index.js +2 -1
- package/dist/theme/NotFound/Content/index.d.ts +7 -0
- package/dist/theme/NotFound/Content/index.js +22 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ The built-in analytics IDs (`G-EN03FMT2Q4`, 51LA `L6b88a5yK4h2Xnci`, and PostHog
|
|
|
74
74
|
|
|
75
75
|
An analytics object enables its provider when it contains an ID. Set `enabled` explicitly to override that. `enabled: true` with no ID is a configuration error.
|
|
76
76
|
|
|
77
|
-
PostHog needs no configuration or consumer project key when registering `hagilightDocusaurus()`. It uses `phc_qiVVihsL925ovhPYNdLJWz4k7aoKVMwYCevgZPE6PER5`, API host `https://us.
|
|
77
|
+
PostHog needs no configuration or consumer project key when registering `hagilightDocusaurus()`. 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`. Its single inline initialization shares the global head with Google Analytics on every production HTML page, including localized docs, blogs and not-found pages. No PostHog loader is emitted outside production.
|
|
78
78
|
|
|
79
79
|
| PostHog input | Behavior |
|
|
80
80
|
| --- | --- |
|
|
@@ -84,6 +84,8 @@ PostHog needs no configuration or consumer project key when registering `hagilig
|
|
|
84
84
|
| `posthog: { enabled: true, apiKey: 'phc_Custom123' }` | Enabled with the custom key |
|
|
85
85
|
| `posthog: { enabled: true }` | Configuration error: `apiKey` required |
|
|
86
86
|
| `posthog: { enabled: false }` | Disabled independently of Google Analytics and 51LA |
|
|
87
|
+
| `posthog: { searchQuery: 'zero-results' }` | Enabled with the default key; search text is sent only for searches with no results (Docusaurus has no PostHog search events, so this has no effect here) |
|
|
88
|
+
| `posthog: { searchQuery: 'off' }` | Enabled with the default key; no search text (the default) |
|
|
87
89
|
|
|
88
90
|
Keys must match `^phc_[A-Za-z0-9]+$`; enabled providers reject missing or malformed keys. Provider options must be objects and `enabled` must be boolean.
|
|
89
91
|
|
|
@@ -100,7 +102,12 @@ hagilightDocusaurus({
|
|
|
100
102
|
}); // all analytics disabled
|
|
101
103
|
```
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
`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, while `{}` still disables it.
|
|
106
|
+
|
|
107
|
+
Docusaurus sends `not_found_viewed` from its 404 page, `code_copied` from its code blocks, and `locale_changed` from the locale dropdown. The interaction observers are installed whenever PostHog is enabled, even when Google Analytics and 51LA are disabled. Platform gaps: there is no `search_used` or `search_result_clicked`, because Docusaurus search is not Pagefind; `article_promotion_impression`, `theme_changed`, `content_width_changed` and `locale_redirected` are Starlight-only or root-transit-only and are not sent here.
|
|
108
|
+
|
|
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
|
+
|
|
104
111
|
|
|
105
112
|
### SEO
|
|
106
113
|
|
|
@@ -184,3 +191,7 @@ The sitemap check looks at every loaded plugin, so the position of `hagilightDoc
|
|
|
184
191
|
## Demo
|
|
185
192
|
|
|
186
193
|
The demo site for this package lives in the monorepo at `examples/demo-docusaurus-web` and is published to https://docusaurus.hagicode.com/.
|
|
194
|
+
|
|
195
|
+
### Consent
|
|
196
|
+
|
|
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.
|
package/consent.css
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/* Consent popup (hagilight). Same rules as packages/core/CookieConsent.astro; only the tokens differ. */
|
|
2
|
+
/* Tokens follow Docusaurus --ifm-* variables and fall back to light values where a theme omits them. */
|
|
3
|
+
.hagilight-consent {
|
|
4
|
+
--hc-accent: var(--ifm-color-primary, #2563eb);
|
|
5
|
+
--hc-bg: var(--ifm-background-surface-color, #ffffff);
|
|
6
|
+
--hc-text: var(--ifm-font-color-base, #1f2937);
|
|
7
|
+
--hc-muted: var(--ifm-color-emphasis-700, color-mix(in srgb, var(--hc-text) 72%, transparent));
|
|
8
|
+
--hc-border: var(--ifm-color-emphasis-300, #cbd5e1);
|
|
9
|
+
--hc-action-bg: var(--ifm-color-primary-dark, var(--ifm-color-primary, #2563eb));
|
|
10
|
+
--hc-action-fg: var(--ifm-color-white, #ffffff);
|
|
11
|
+
--hc-focus: var(--ifm-color-primary-darker, var(--ifm-color-primary, #2563eb));
|
|
12
|
+
/* Derived: a surface tinted toward the brand, hairlines from the theme border, motion curves. */
|
|
13
|
+
--hc-surface: color-mix(in srgb, var(--hc-accent) 5%, var(--hc-bg));
|
|
14
|
+
--hc-hairline: color-mix(in srgb, var(--hc-border) 70%, transparent);
|
|
15
|
+
--hc-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
16
|
+
--hc-spring: cubic-bezier(0.22, 1, 0.36, 1);
|
|
17
|
+
|
|
18
|
+
position: fixed;
|
|
19
|
+
z-index: 45;
|
|
20
|
+
inset-block-end: max(1rem, env(safe-area-inset-bottom, 0px));
|
|
21
|
+
inset-inline-end: max(1rem, env(safe-area-inset-right, 0px));
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
width: min(24rem, calc(100vw - 2rem - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px)));
|
|
24
|
+
max-height: calc(100dvh - 2rem);
|
|
25
|
+
overflow-y: auto;
|
|
26
|
+
overscroll-behavior: contain;
|
|
27
|
+
scrollbar-width: thin;
|
|
28
|
+
scrollbar-color: color-mix(in srgb, var(--hc-text) 28%, transparent) transparent;
|
|
29
|
+
padding: 1.25rem 1.25rem 0.5rem;
|
|
30
|
+
border: 0;
|
|
31
|
+
border-radius: 0.875rem;
|
|
32
|
+
background: var(--hc-surface);
|
|
33
|
+
color: var(--hc-text);
|
|
34
|
+
/* Elevation is declared once, as a shadow: a tight contact shadow under a long, soft one. */
|
|
35
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 0.08), 0 18px 44px -14px rgb(0 0 0 / 0.4);
|
|
36
|
+
font-size: 0.875rem;
|
|
37
|
+
line-height: 1.5;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* A damped spring where linear() exists (the same curve as the promotion banner), a long ease-out elsewhere. */
|
|
41
|
+
@supports (animation-timing-function: linear(0, 1)) {
|
|
42
|
+
.hagilight-consent {
|
|
43
|
+
--hc-spring: linear(0, 0.066, 0.22, 0.409, 0.597, 0.761, 0.89, 0.983, 1.042, 1.073, 1.083, 1.079, 1.067, 1.052, 1.036, 1.022, 1.011, 1.002, 0.997, 0.994, 0.993, 0.993, 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Below about 36rem the panel attaches to the bottom edge, full width, with its top corners rounded. */
|
|
48
|
+
@media (max-width: 36rem) {
|
|
49
|
+
.hagilight-consent {
|
|
50
|
+
inset-inline: 0;
|
|
51
|
+
inset-block-end: 0;
|
|
52
|
+
width: auto;
|
|
53
|
+
max-width: none;
|
|
54
|
+
max-height: min(85dvh, calc(100dvh - 1rem));
|
|
55
|
+
border-radius: 0.875rem 0.875rem 0 0;
|
|
56
|
+
padding: 1.125rem 1rem max(0.25rem, env(safe-area-inset-bottom, 0px));
|
|
57
|
+
box-shadow: 0 -1px 2px rgb(0 0 0 / 0.06), 0 -14px 36px -14px rgb(0 0 0 / 0.4);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.hagilight-consent[hidden],
|
|
62
|
+
.hagilight-consent [hidden] {
|
|
63
|
+
display: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* While the panel fades out its buttons no longer take clicks. */
|
|
67
|
+
.hagilight-consent[data-leaving] {
|
|
68
|
+
pointer-events: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.hagilight-consent ::selection {
|
|
72
|
+
background: color-mix(in srgb, var(--hc-accent) 28%, transparent);
|
|
73
|
+
color: inherit;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* The one authored moment: the panel springs up, then its parts settle in order. */
|
|
77
|
+
.hagilight-consent:not([hidden]) {
|
|
78
|
+
animation: hagilight-consent-rise 0.7s var(--hc-spring) backwards;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.hagilight-consent:not([hidden]) .hagilight-consent__title,
|
|
82
|
+
.hagilight-consent:not([hidden]) .hagilight-consent__intro,
|
|
83
|
+
.hagilight-consent:not([hidden]) .hagilight-consent__actions,
|
|
84
|
+
.hagilight-consent:not([hidden]) .hagilight-consent__toggle {
|
|
85
|
+
animation: hagilight-consent-settle 0.55s var(--hc-ease) backwards;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.hagilight-consent:not([hidden]) .hagilight-consent__title { animation-delay: 0.1s; }
|
|
89
|
+
.hagilight-consent:not([hidden]) .hagilight-consent__intro { animation-delay: 0.16s; }
|
|
90
|
+
.hagilight-consent:not([hidden]) .hagilight-consent__actions { animation-delay: 0.22s; }
|
|
91
|
+
.hagilight-consent:not([hidden]) .hagilight-consent__toggle { animation-delay: 0.28s; }
|
|
92
|
+
|
|
93
|
+
@keyframes hagilight-consent-rise {
|
|
94
|
+
from { opacity: 0; transform: translateY(1.5rem) scale(0.97); }
|
|
95
|
+
to { opacity: 1; transform: none; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@keyframes hagilight-consent-settle {
|
|
99
|
+
from { opacity: 0; transform: translateY(0.5rem); }
|
|
100
|
+
to { opacity: 1; transform: none; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.hagilight-consent__title {
|
|
104
|
+
margin: 0;
|
|
105
|
+
color: var(--hc-text);
|
|
106
|
+
font-size: 1.0625rem;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
line-height: 1.3;
|
|
109
|
+
text-wrap: balance;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.hagilight-consent__intro {
|
|
113
|
+
margin: 0.375rem 0 0;
|
|
114
|
+
color: var(--hc-muted);
|
|
115
|
+
line-height: 1.55;
|
|
116
|
+
text-wrap: pretty;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* The current choice only shows once there is one, as a small status chip beside the actions. */
|
|
120
|
+
.hagilight-consent__choice {
|
|
121
|
+
display: inline-flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 0.4375rem;
|
|
124
|
+
margin: 0.75rem 0 0;
|
|
125
|
+
padding: 0.1875rem 0.625rem 0.1875rem 0.5rem;
|
|
126
|
+
border-radius: 999px;
|
|
127
|
+
background: color-mix(in srgb, var(--hc-text) 7%, transparent);
|
|
128
|
+
color: var(--hc-text);
|
|
129
|
+
font-size: 0.75rem;
|
|
130
|
+
line-height: 1.4;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.hagilight-consent[data-state='undecided'] .hagilight-consent__choice {
|
|
134
|
+
display: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.hagilight-consent__choice::before {
|
|
138
|
+
content: '';
|
|
139
|
+
flex: none;
|
|
140
|
+
width: 0.4375rem;
|
|
141
|
+
height: 0.4375rem;
|
|
142
|
+
border-radius: 50%;
|
|
143
|
+
background: var(--hc-muted);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.hagilight-consent[data-state='granted'] .hagilight-consent__choice::before {
|
|
147
|
+
background: var(--hc-accent);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.hagilight-consent__actions {
|
|
151
|
+
display: grid;
|
|
152
|
+
grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
|
|
153
|
+
gap: 0.5rem;
|
|
154
|
+
margin-top: 1rem;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Accept and Reject share every property, so neither reads as the primary choice. Both rest as a tint and fill on hover. */
|
|
158
|
+
.hagilight-consent__button {
|
|
159
|
+
min-height: 2.75rem;
|
|
160
|
+
margin: 0;
|
|
161
|
+
padding: 0.5rem 0.875rem;
|
|
162
|
+
border: 1px solid color-mix(in srgb, var(--hc-action-bg) 55%, transparent);
|
|
163
|
+
border-radius: 0.625rem;
|
|
164
|
+
background: color-mix(in srgb, var(--hc-action-bg) 10%, transparent);
|
|
165
|
+
color: var(--hc-text);
|
|
166
|
+
font: inherit;
|
|
167
|
+
font-size: 0.875rem;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
line-height: 1.25;
|
|
170
|
+
text-wrap: balance;
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
transition: background-color 0.18s var(--hc-ease), border-color 0.18s var(--hc-ease), color 0.18s var(--hc-ease), transform 0.18s var(--hc-ease);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@media (hover: hover) {
|
|
176
|
+
.hagilight-consent__button:hover {
|
|
177
|
+
border-color: var(--hc-action-bg);
|
|
178
|
+
background: var(--hc-action-bg);
|
|
179
|
+
color: var(--hc-action-fg);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.hagilight-consent__button:active {
|
|
184
|
+
transform: scale(0.98);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* A full-width disclosure row: the label on the left, a chevron that turns as the details open. */
|
|
188
|
+
.hagilight-consent__toggle {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
justify-content: space-between;
|
|
192
|
+
gap: 0.75rem;
|
|
193
|
+
box-sizing: border-box;
|
|
194
|
+
width: 100%;
|
|
195
|
+
min-height: 2.5rem;
|
|
196
|
+
margin: 0.75rem 0 0;
|
|
197
|
+
padding: 0.5rem 0;
|
|
198
|
+
border: 0;
|
|
199
|
+
border-top: 1px solid var(--hc-hairline);
|
|
200
|
+
border-radius: 0.25rem;
|
|
201
|
+
background: none;
|
|
202
|
+
color: var(--hc-muted);
|
|
203
|
+
font: inherit;
|
|
204
|
+
font-size: 0.8125rem;
|
|
205
|
+
font-weight: 500;
|
|
206
|
+
text-align: start;
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
transition: color 0.18s var(--hc-ease);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.hagilight-consent__toggle::after {
|
|
212
|
+
content: '';
|
|
213
|
+
flex: none;
|
|
214
|
+
width: 0.4375rem;
|
|
215
|
+
height: 0.4375rem;
|
|
216
|
+
border: solid currentColor;
|
|
217
|
+
border-width: 0 1.5px 1.5px 0;
|
|
218
|
+
transform: translateY(-0.125rem) rotate(45deg);
|
|
219
|
+
transition: transform 0.3s var(--hc-ease);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.hagilight-consent__toggle[aria-expanded='true']::after {
|
|
223
|
+
transform: translateY(0.125rem) rotate(225deg);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@media (hover: hover) {
|
|
227
|
+
.hagilight-consent__toggle:hover {
|
|
228
|
+
color: var(--hc-text);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.hagilight-consent__button:focus-visible,
|
|
233
|
+
.hagilight-consent__toggle:focus-visible,
|
|
234
|
+
.hagilight-consent a:focus-visible {
|
|
235
|
+
outline: 2px solid var(--hc-focus);
|
|
236
|
+
outline-offset: 2px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.hagilight-consent__details {
|
|
240
|
+
box-sizing: border-box;
|
|
241
|
+
margin: 0;
|
|
242
|
+
padding: 0 0 0.25rem;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* Categories are open sections divided by hairlines, not boxes inside a box. */
|
|
246
|
+
.hagilight-consent__category {
|
|
247
|
+
margin: 0;
|
|
248
|
+
padding: 0.875rem 0;
|
|
249
|
+
border-top: 1px solid var(--hc-hairline);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.hagilight-consent__category:first-child {
|
|
253
|
+
padding-top: 0.5rem;
|
|
254
|
+
border-top: 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.hagilight-consent__category h3 {
|
|
258
|
+
display: flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
gap: 0.5rem;
|
|
261
|
+
margin: 0;
|
|
262
|
+
color: var(--hc-text);
|
|
263
|
+
font-size: 0.8125rem;
|
|
264
|
+
font-weight: 600;
|
|
265
|
+
line-height: 1.3;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* A filled dot is on, a hollow one is off; the analytics dot follows the visitor's choice. */
|
|
269
|
+
.hagilight-consent__category h3::before {
|
|
270
|
+
content: '';
|
|
271
|
+
flex: none;
|
|
272
|
+
box-sizing: border-box;
|
|
273
|
+
width: 0.5rem;
|
|
274
|
+
height: 0.5rem;
|
|
275
|
+
border: 1.5px solid var(--hc-muted);
|
|
276
|
+
border-radius: 50%;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.hagilight-consent__category--necessary h3::before,
|
|
280
|
+
.hagilight-consent[data-state='granted'] .hagilight-consent__category--analytics h3::before {
|
|
281
|
+
border-color: var(--hc-accent);
|
|
282
|
+
background: var(--hc-accent);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.hagilight-consent__category p,
|
|
286
|
+
.hagilight-consent__category ul {
|
|
287
|
+
margin: 0.375rem 0 0;
|
|
288
|
+
padding-inline-start: 1rem;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.hagilight-consent__category p,
|
|
292
|
+
.hagilight-consent__withdraw {
|
|
293
|
+
color: var(--hc-muted);
|
|
294
|
+
font-size: 0.8125rem;
|
|
295
|
+
line-height: 1.5;
|
|
296
|
+
text-wrap: pretty;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.hagilight-consent ul {
|
|
300
|
+
list-style: none;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Storage names are code: monospace, with the kind pushed to the far edge. */
|
|
304
|
+
.hagilight-consent li {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: baseline;
|
|
307
|
+
justify-content: space-between;
|
|
308
|
+
gap: 0.75rem;
|
|
309
|
+
margin: 0;
|
|
310
|
+
padding: 0.1875rem 0;
|
|
311
|
+
color: var(--hc-muted);
|
|
312
|
+
font-size: 0.75rem;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.hagilight-consent li span {
|
|
316
|
+
flex: 0 1 auto;
|
|
317
|
+
text-align: end;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.hagilight-consent code {
|
|
321
|
+
color: var(--hc-text);
|
|
322
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
323
|
+
font-size: 0.75rem;
|
|
324
|
+
overflow-wrap: anywhere;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.hagilight-consent__withdraw {
|
|
328
|
+
margin: 0;
|
|
329
|
+
padding: 0.875rem 0 0;
|
|
330
|
+
border-top: 1px solid var(--hc-hairline);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.hagilight-consent a {
|
|
334
|
+
color: var(--hc-text);
|
|
335
|
+
font-weight: 600;
|
|
336
|
+
text-decoration-thickness: 1px;
|
|
337
|
+
text-underline-offset: 0.2em;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.hagilight-consent a:hover {
|
|
341
|
+
text-decoration-thickness: 2px;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.hagilight-consent__privacy {
|
|
345
|
+
margin: 0.5rem 0 0.5rem;
|
|
346
|
+
font-size: 0.8125rem;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* Forced colors drop the shadow and the fills, so the panel and the dots get system-colored edges. */
|
|
350
|
+
@media (forced-colors: active) {
|
|
351
|
+
.hagilight-consent {
|
|
352
|
+
border: 1px solid CanvasText;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.hagilight-consent__button {
|
|
356
|
+
border-color: ButtonText;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.hagilight-consent__category--necessary h3::before,
|
|
360
|
+
.hagilight-consent[data-state='granted'] .hagilight-consent__category--analytics h3::before {
|
|
361
|
+
border-color: CanvasText;
|
|
362
|
+
background: CanvasText;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
@media (prefers-reduced-motion: reduce) {
|
|
367
|
+
.hagilight-consent,
|
|
368
|
+
.hagilight-consent * {
|
|
369
|
+
animation: none !important;
|
|
370
|
+
transition: none !important;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.hagilight-footer__consent {
|
|
375
|
+
margin: 0;
|
|
376
|
+
padding: 0;
|
|
377
|
+
border: 0;
|
|
378
|
+
background: none;
|
|
379
|
+
color: inherit;
|
|
380
|
+
font: inherit;
|
|
381
|
+
text-decoration: underline;
|
|
382
|
+
cursor: pointer;
|
|
383
|
+
}
|
package/dist/analytics.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { HtmlTagObject } from '@docusaurus/types';
|
|
2
|
-
|
|
2
|
+
import { type ConsentPopupOptions } from '@hagicode/hagilight-core/consent-markup';
|
|
3
|
+
import { type PostHogInitOptions, type PostHogSiteContext } from '@hagicode/hagilight-core/posthog';
|
|
4
|
+
export declare function posthogHeadTags(apiKey: string, context?: PostHogSiteContext, options?: PostHogInitOptions): HtmlTagObject[];
|
|
5
|
+
/** Inert data for the consent gate: a JSON script that never executes, so no provider loads before a grant. */
|
|
6
|
+
export declare function consentDescriptorTag(provider: string, payload: Record<string, string>): HtmlTagObject;
|
|
7
|
+
/** The popup's config descriptor, read by the client module once. It holds no IDs. */
|
|
8
|
+
export declare function consentConfigTag(config: ConsentPopupOptions): HtmlTagObject;
|
|
3
9
|
export declare const DEFAULT_GOOGLE_ANALYTICS_ID = "G-EN03FMT2Q4";
|
|
4
10
|
export declare const DEFAULT_FIFTY_ONE_LA_ID = "L6b88a5yK4h2Xnci";
|
|
5
11
|
export declare const GOOGLE_ANALYTICS_ID_PATTERN: RegExp;
|
package/dist/analytics.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
+
import { consentDescriptor, CONSENT_DESCRIPTOR_ID } from '@hagicode/hagilight-core/consent-markup';
|
|
1
2
|
import { posthogInitScript } from '@hagicode/hagilight-core/posthog';
|
|
2
|
-
export function posthogHeadTags(apiKey) {
|
|
3
|
-
return [{ tagName: 'script', innerHTML: posthogInitScript(apiKey) }];
|
|
3
|
+
export function posthogHeadTags(apiKey, context, options) {
|
|
4
|
+
return [{ tagName: 'script', innerHTML: posthogInitScript(apiKey, context, options) }];
|
|
5
|
+
}
|
|
6
|
+
/** Inert data for the consent gate: a JSON script that never executes, so no provider loads before a grant. */
|
|
7
|
+
export function consentDescriptorTag(provider, payload) {
|
|
8
|
+
return {
|
|
9
|
+
tagName: 'script',
|
|
10
|
+
attributes: { type: 'application/json', 'data-hagilight-analytics': provider },
|
|
11
|
+
innerHTML: JSON.stringify(payload).replace(/</gu, '\\u003c'),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** The popup's config descriptor, read by the client module once. It holds no IDs. */
|
|
15
|
+
export function consentConfigTag(config) {
|
|
16
|
+
return { tagName: 'script', attributes: { type: 'application/json', id: CONSENT_DESCRIPTOR_ID }, innerHTML: consentDescriptor(config) };
|
|
4
17
|
}
|
|
5
18
|
export const DEFAULT_GOOGLE_ANALYTICS_ID = 'G-EN03FMT2Q4';
|
|
6
19
|
export const DEFAULT_FIFTY_ONE_LA_ID = 'L6b88a5yK4h2Xnci';
|
|
@@ -31,7 +44,7 @@ export function fiftyOneLaHeadTags(siteId) {
|
|
|
31
44
|
},
|
|
32
45
|
{
|
|
33
46
|
tagName: 'script',
|
|
34
|
-
innerHTML: `LA.init(${JSON.stringify({ id: siteId, ck: siteId, autoTrack: true, hashMode: true
|
|
47
|
+
innerHTML: `LA.init(${JSON.stringify({ id: siteId, ck: siteId, autoTrack: true, hashMode: true })});`,
|
|
35
48
|
},
|
|
36
49
|
];
|
|
37
50
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PostHogClient } from '@hagicode/hagilight-core/posthog-events';
|
|
1
2
|
export interface RouteLocation {
|
|
2
3
|
pathname: string;
|
|
3
4
|
search: string;
|
|
@@ -18,6 +19,22 @@ export interface RouteReportDeps {
|
|
|
18
19
|
schedule: (task: () => void) => void;
|
|
19
20
|
}
|
|
20
21
|
export declare function installClickTracking(target?: Pick<EventTarget, 'addEventListener'>): boolean;
|
|
22
|
+
export interface PostHogPageContext {
|
|
23
|
+
/** The document language, sent as `locale` when non-empty. */
|
|
24
|
+
lang: string;
|
|
25
|
+
/** The site base path, for example `/docs/`. */
|
|
26
|
+
base: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Refreshes only `locale` and `page_type` as super properties. Route updates are the only time this
|
|
30
|
+
* module calls PostHog, and it never initializes the SDK or captures an event.
|
|
31
|
+
*/
|
|
32
|
+
export declare function refreshPostHogPageProperties(update: RouteDidUpdate, posthog: PostHogClient | undefined, page: PostHogPageContext): boolean;
|
|
21
33
|
export declare function reportPageView(gtag: unknown, page: PageSnapshot): boolean;
|
|
22
34
|
export declare function handleRouteDidUpdate(update: RouteDidUpdate, deps: RouteReportDeps): void;
|
|
35
|
+
/**
|
|
36
|
+
* Navigation reports wait for the grant when consent is on: the page then carries the consent descriptor, and
|
|
37
|
+
* only a granted visitor may send a page_view. Without consent the page reports as before.
|
|
38
|
+
*/
|
|
39
|
+
export declare function navigationReportsAllowed(doc?: Pick<Document, 'getElementById'>): boolean;
|
|
23
40
|
export declare function onRouteDidUpdate(update: RouteDidUpdate): void;
|
package/dist/client/analytics.js
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { installGaEventTracking } from '@hagicode/hagilight-core/analytics-events';
|
|
2
|
+
import { getConsent } from '@hagicode/hagilight-core/consent';
|
|
3
|
+
import { CONSENT_DESCRIPTOR_ID } from '@hagicode/hagilight-core/consent-markup';
|
|
4
|
+
import { classifyPagePath, installPostHogClickTracking, } from '@hagicode/hagilight-core/posthog-events';
|
|
2
5
|
export function installClickTracking(target) {
|
|
3
|
-
|
|
6
|
+
const installed = installGaEventTracking(target);
|
|
7
|
+
// The PostHog sink is installed only when the head script initialized PostHog, so a site that disables
|
|
8
|
+
// PostHog gets no PostHog listener even though this module is bundled for Google Analytics.
|
|
9
|
+
if (globalThis.__hagilightPosthogInitialized === true) {
|
|
10
|
+
installPostHogClickTracking(target);
|
|
11
|
+
}
|
|
12
|
+
return installed;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Refreshes only `locale` and `page_type` as super properties. Route updates are the only time this
|
|
16
|
+
* module calls PostHog, and it never initializes the SDK or captures an event.
|
|
17
|
+
*/
|
|
18
|
+
export function refreshPostHogPageProperties(update, posthog, page) {
|
|
19
|
+
if (typeof posthog?.register !== 'function')
|
|
20
|
+
return false;
|
|
21
|
+
const pageType = classifyPagePath(update.location.pathname, { base: page.base, locales: page.lang ? [page.lang] : [] });
|
|
22
|
+
const properties = page.lang ? { locale: page.lang, page_type: pageType } : { page_type: pageType };
|
|
23
|
+
posthog.register(properties);
|
|
24
|
+
return true;
|
|
4
25
|
}
|
|
5
26
|
export function reportPageView(gtag, page) {
|
|
6
27
|
if (typeof gtag !== 'function')
|
|
@@ -15,8 +36,17 @@ export function handleRouteDidUpdate(update, deps) {
|
|
|
15
36
|
reportPageView(deps.getGtag(), deps.readPage());
|
|
16
37
|
});
|
|
17
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Navigation reports wait for the grant when consent is on: the page then carries the consent descriptor, and
|
|
41
|
+
* only a granted visitor may send a page_view. Without consent the page reports as before.
|
|
42
|
+
*/
|
|
43
|
+
export function navigationReportsAllowed(doc = document) {
|
|
44
|
+
if (!doc.getElementById(CONSENT_DESCRIPTOR_ID))
|
|
45
|
+
return true;
|
|
46
|
+
return getConsent().analytics === 'granted';
|
|
47
|
+
}
|
|
18
48
|
const browserDeps = {
|
|
19
|
-
getGtag: () => globalThis.gtag,
|
|
49
|
+
getGtag: () => (navigationReportsAllowed() ? globalThis.gtag : undefined),
|
|
20
50
|
readPage: () => ({
|
|
21
51
|
title: document.title,
|
|
22
52
|
location: window.location.href,
|
|
@@ -26,8 +56,20 @@ const browserDeps = {
|
|
|
26
56
|
window.setTimeout(task, 0);
|
|
27
57
|
},
|
|
28
58
|
};
|
|
59
|
+
function browserPageContext() {
|
|
60
|
+
return {
|
|
61
|
+
lang: document.documentElement.lang,
|
|
62
|
+
base: document.querySelector('meta[name="hagilight-base-path"]')?.getAttribute('content') || '/',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
29
65
|
export function onRouteDidUpdate(update) {
|
|
30
66
|
handleRouteDidUpdate(update, browserDeps);
|
|
67
|
+
try {
|
|
68
|
+
refreshPostHogPageProperties(update, globalThis.posthog, browserPageContext());
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Analytics must never break route handling.
|
|
72
|
+
}
|
|
31
73
|
}
|
|
32
74
|
if (typeof document !== 'undefined')
|
|
33
75
|
installClickTracking(document);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function installPostHogInteractionObservers(): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { installPostHogInteractions } from '@hagicode/hagilight-core/posthog-interactions';
|
|
2
|
+
/*
|
|
3
|
+
* Installs the shared PostHog interaction observers (search, result clicks, code copy, locale links).
|
|
4
|
+
* Runs only when the head script initialized PostHog. Observers are delegated on the document, so
|
|
5
|
+
* Docusaurus client navigation needs no rebinding and this module adds no route listener.
|
|
6
|
+
*/
|
|
7
|
+
export function installPostHogInteractionObservers() {
|
|
8
|
+
if (globalThis.__hagilightPosthogInitialized !== true)
|
|
9
|
+
return false;
|
|
10
|
+
return installPostHogInteractions(document);
|
|
11
|
+
}
|
|
12
|
+
if (typeof document !== 'undefined')
|
|
13
|
+
installPostHogInteractionObservers();
|
package/dist/discovery.js
CHANGED
|
@@ -221,7 +221,7 @@ 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 },
|
|
224
|
+
features: { seo: options.seo.enabled, promoto: options.promoto, consent: options.consent.enabled },
|
|
225
225
|
});
|
|
226
226
|
writeSiteMetadata({
|
|
227
227
|
outDir,
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
3
3
|
import { resolveBuildInfo } from '@hagicode/hagilight-core/build-info-resolver';
|
|
4
4
|
import footerAvatarManifest from '@hagicode/hagilight-core/assets/footer/manifest.json' with { type: 'json' };
|
|
5
5
|
import { randomPoolFromManifest } from '@hagicode/hagilight-core/random-image';
|
|
6
|
-
import { fiftyOneLaHeadTags, googleAnalyticsHeadTags, posthogHeadTags } from './analytics.js';
|
|
6
|
+
import { consentConfigTag, consentDescriptorTag, fiftyOneLaHeadTags, googleAnalyticsHeadTags, posthogHeadTags } from './analytics.js';
|
|
7
|
+
import { posthogInitScript } from '@hagicode/hagilight-core/posthog';
|
|
7
8
|
import { collectFeedItems, feedHeadTags, writeDiscoveryOutputs } from './discovery.js';
|
|
8
9
|
import { PLUGIN_NAME } from './global-data.js';
|
|
9
10
|
import { resolveOptions } from './options.js';
|
|
@@ -14,15 +15,45 @@ const PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', impor
|
|
|
14
15
|
function packageFile(relativePath) {
|
|
15
16
|
return fileURLToPath(new URL(relativePath, import.meta.url));
|
|
16
17
|
}
|
|
17
|
-
function analyticsHeadTags(options) {
|
|
18
|
+
function analyticsHeadTags(options, site) {
|
|
18
19
|
if (process.env.NODE_ENV !== 'production')
|
|
19
20
|
return [];
|
|
21
|
+
const consent = options.consent.enabled;
|
|
22
|
+
const posthogContext = {
|
|
23
|
+
siteId: options.links.siteId?.trim() || hostnameOf(site.url),
|
|
24
|
+
framework: 'docusaurus',
|
|
25
|
+
version: PACKAGE_VERSION,
|
|
26
|
+
};
|
|
20
27
|
return [
|
|
21
|
-
|
|
22
|
-
...(options.
|
|
23
|
-
|
|
28
|
+
// With consent on, each provider leaves inert data for the gate instead of an executable loader.
|
|
29
|
+
...(options.googleAnalyticsId === undefined ? [] : consent
|
|
30
|
+
? [consentDescriptorTag('googleAnalytics', { measurementId: options.googleAnalyticsId })]
|
|
31
|
+
: googleAnalyticsHeadTags(options.googleAnalyticsId)),
|
|
32
|
+
...(options.fiftyOneLaId === undefined ? [] : consent
|
|
33
|
+
? [consentDescriptorTag('fiftyOneLa', { siteId: options.fiftyOneLaId })]
|
|
34
|
+
: fiftyOneLaHeadTags(options.fiftyOneLaId)),
|
|
35
|
+
...(options.posthogApiKey === undefined ? [] : [
|
|
36
|
+
// The client reads this to classify routes for `page_type`; it is emitted only with PostHog.
|
|
37
|
+
{ tagName: 'meta', attributes: { name: 'hagilight-base-path', content: site.baseUrl } },
|
|
38
|
+
...(consent
|
|
39
|
+
? [consentDescriptorTag('posthog', { apiKey: options.posthogApiKey, loader: posthogInitScript(options.posthogApiKey, posthogContext, { searchQuery: options.posthogSearchQuery }) })]
|
|
40
|
+
: posthogHeadTags(options.posthogApiKey, posthogContext, { searchQuery: options.posthogSearchQuery })),
|
|
41
|
+
]),
|
|
42
|
+
...(consent ? [consentConfigTag({
|
|
43
|
+
providers: options.consent.providers,
|
|
44
|
+
privacyUrl: options.consent.privacyUrl,
|
|
45
|
+
policyVersion: options.consent.policyVersion,
|
|
46
|
+
})] : []),
|
|
24
47
|
];
|
|
25
48
|
}
|
|
49
|
+
function hostnameOf(url) {
|
|
50
|
+
try {
|
|
51
|
+
return new URL(url).hostname || undefined;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
26
57
|
/** Exposes the footer site identifier to the client banner, which tags its call to action with it. */
|
|
27
58
|
function promotoSiteIdHeadTags(options) {
|
|
28
59
|
const siteId = options.links.siteId?.trim();
|
|
@@ -33,12 +64,17 @@ function promotoSiteIdHeadTags(options) {
|
|
|
33
64
|
/** The footer avatar pair in the core package; matched by path so the rule holds in any install layout. */
|
|
34
65
|
const FOOTER_AVATAR_ASSET = /[\\/]footer[\\/]avatar\.(?:avif|webp)$/u;
|
|
35
66
|
function clientModules(options) {
|
|
36
|
-
|
|
67
|
+
// The analytics client also installs the PostHog click sink, so PostHog alone is enough to bundle it.
|
|
68
|
+
const analyticsEnabled = options.googleAnalyticsId !== undefined || options.fiftyOneLaId !== undefined || options.posthogApiKey !== undefined;
|
|
37
69
|
const footerAvatarPicker = options.footer && options.footerAvatar.randomAvatar;
|
|
38
70
|
return [
|
|
39
71
|
...(analyticsEnabled ? [packageFile('./client/analytics.js')] : []),
|
|
72
|
+
...(options.consent.enabled ? [packageFile('./client/consent.js')] : []),
|
|
73
|
+
// Interaction observers are independent of Google Analytics and 51LA, so PostHog alone registers them.
|
|
74
|
+
...(options.posthogApiKey === undefined ? [] : [packageFile('./client/posthog-interactions.js')]),
|
|
40
75
|
...(options.promoto ? [packageFile('./client/promoto.js'), packageFile('../promoto.css')] : []),
|
|
41
76
|
...(options.footer ? [packageFile('../styles.css')] : []),
|
|
77
|
+
...(options.consent.enabled ? [packageFile('../consent.css')] : []),
|
|
42
78
|
...(footerAvatarPicker ? [packageFile('./client/random-image.js')] : []),
|
|
43
79
|
];
|
|
44
80
|
}
|
|
@@ -55,7 +91,8 @@ function createPlugin(context, options) {
|
|
|
55
91
|
? randomPoolFromManifest(footerAvatarManifest.pool)
|
|
56
92
|
: undefined,
|
|
57
93
|
};
|
|
58
|
-
|
|
94
|
+
// The PostHog 404 wrapper lives in the theme, so PostHog alone also enables the theme path.
|
|
95
|
+
const themeEnabled = options.footer || options.seo.enabled || options.posthogApiKey !== undefined;
|
|
59
96
|
const site = {
|
|
60
97
|
title: context.siteConfig.title,
|
|
61
98
|
tagline: context.siteConfig.tagline,
|
|
@@ -109,7 +146,7 @@ function createPlugin(context, options) {
|
|
|
109
146
|
injectHtmlTags() {
|
|
110
147
|
return {
|
|
111
148
|
headTags: [
|
|
112
|
-
...analyticsHeadTags(options),
|
|
149
|
+
...analyticsHeadTags(options, site),
|
|
113
150
|
...seoHeadTags(options.seo, site),
|
|
114
151
|
...feedHeadTags(site, options.rss, feedItems.length),
|
|
115
152
|
...promotoSiteIdHeadTags(options),
|
package/dist/options.d.ts
CHANGED
|
@@ -1,5 +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
4
|
import { type SiteMetadataOption } from '@hagicode/hagilight-core/site-metadata';
|
|
4
5
|
export interface FeatureToggle {
|
|
5
6
|
enabled?: boolean;
|
|
@@ -14,10 +15,28 @@ export interface AnalyticsOptions {
|
|
|
14
15
|
googleAnalytics?: GoogleAnalyticsOptions;
|
|
15
16
|
fiftyOneLa?: FiftyOneLaOptions;
|
|
16
17
|
posthog?: PostHogOptions;
|
|
18
|
+
consent?: ConsentOptions;
|
|
19
|
+
}
|
|
20
|
+
export interface ConsentOptions {
|
|
21
|
+
/** Defaults to on when any analytics provider is enabled. `false` restores immediate loading. */
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
/** An absolute HTTP(S) URL or a site-relative path. Omitted means the details have no privacy link. */
|
|
24
|
+
privacyUrl?: string;
|
|
25
|
+
/** Defaults to `1`. Changing it asks every visitor again. */
|
|
26
|
+
policyVersion?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ResolvedConsentOptions {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
privacyUrl: string | undefined;
|
|
31
|
+
policyVersion: string;
|
|
32
|
+
providers: ConsentPopupOptions['providers'];
|
|
17
33
|
}
|
|
18
34
|
export interface PostHogOptions extends FeatureToggle {
|
|
19
35
|
apiKey?: string;
|
|
36
|
+
/** `zero-results` sends search text only for searches with no results. Defaults to `off`; never changes enablement. */
|
|
37
|
+
searchQuery?: PostHogSearchQueryOption;
|
|
20
38
|
}
|
|
39
|
+
export type PostHogSearchQueryOption = 'off' | 'zero-results';
|
|
21
40
|
export interface SeoOrganizationOptions {
|
|
22
41
|
name?: string;
|
|
23
42
|
url?: string;
|
|
@@ -78,6 +97,8 @@ export interface ResolvedHagilightOptions {
|
|
|
78
97
|
googleAnalyticsId: string | undefined;
|
|
79
98
|
fiftyOneLaId: string | undefined;
|
|
80
99
|
posthogApiKey: string | undefined;
|
|
100
|
+
consent: ResolvedConsentOptions;
|
|
101
|
+
posthogSearchQuery: PostHogSearchQueryOption;
|
|
81
102
|
promoto: boolean;
|
|
82
103
|
languageSelector: boolean;
|
|
83
104
|
seo: ResolvedSeoOptions;
|
package/dist/options.js
CHANGED
|
@@ -1,5 +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
4
|
import { isValidSeoImageReference, resolveAbsoluteHttpUrl } from '@hagicode/hagilight-core/seo';
|
|
4
5
|
import { resolveSiteMetadataOption } from '@hagicode/hagilight-core/site-metadata';
|
|
5
6
|
import { DEFAULT_FIFTY_ONE_LA_ID, DEFAULT_GOOGLE_ANALYTICS_ID, FIFTY_ONE_LA_ID_PATTERN, GOOGLE_ANALYTICS_ID_PATTERN, } from './analytics.js';
|
|
@@ -40,6 +41,26 @@ function featureEnabled(value, path) {
|
|
|
40
41
|
return true;
|
|
41
42
|
return booleanOption(objectOption(value, path).enabled, `${path}.enabled`) ?? true;
|
|
42
43
|
}
|
|
44
|
+
function resolvePostHogSearchQuery(value) {
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
return 'off';
|
|
47
|
+
const mode = objectOption(value, 'analytics.posthog').searchQuery;
|
|
48
|
+
if (mode === undefined || mode === 'off')
|
|
49
|
+
return 'off';
|
|
50
|
+
if (mode === 'zero-results')
|
|
51
|
+
return mode;
|
|
52
|
+
throw invalid('analytics.posthog.searchQuery', 'must be one of: off, zero-results');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Removes `searchQuery` before provider resolution, because it never changes whether PostHog is enabled.
|
|
56
|
+
* An object that held only `searchQuery` therefore counts as omitted, so the default key stays on.
|
|
57
|
+
*/
|
|
58
|
+
function providerOptionsWithoutSearchQuery(value) {
|
|
59
|
+
if (value === undefined || typeof value !== 'object' || value === null || !Object.hasOwn(value, 'searchQuery'))
|
|
60
|
+
return value;
|
|
61
|
+
const { searchQuery: _searchQuery, ...rest } = value;
|
|
62
|
+
return Object.keys(rest).length === 0 ? undefined : rest;
|
|
63
|
+
}
|
|
43
64
|
function resolveProvider(value, path, idKey, pattern, defaultId) {
|
|
44
65
|
if (value === undefined)
|
|
45
66
|
return defaultId;
|
|
@@ -94,16 +115,40 @@ function resolveRss(value) {
|
|
|
94
115
|
includeBlog: booleanOption(group.includeBlog, 'rss.includeBlog') ?? true,
|
|
95
116
|
};
|
|
96
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Consent is on by default whenever a provider is enabled; `enabled: false` restores immediate loading. Invalid values
|
|
120
|
+
* throw from the core validators, which name the option.
|
|
121
|
+
*/
|
|
122
|
+
function resolveConsent(config, providers) {
|
|
123
|
+
const record = config === undefined ? {} : objectOption(config, 'analytics.consent');
|
|
124
|
+
const requested = booleanOption(record.enabled, 'analytics.consent.enabled');
|
|
125
|
+
return {
|
|
126
|
+
enabled: providers.length > 0 && (requested ?? true),
|
|
127
|
+
privacyUrl: validatePrivacyUrl(record.privacyUrl),
|
|
128
|
+
policyVersion: validatePolicyVersion(record.policyVersion),
|
|
129
|
+
providers,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
97
132
|
export function resolveOptions(input) {
|
|
98
133
|
const options = input === undefined ? {} : objectOption(input, 'options');
|
|
99
134
|
const analytics = options.analytics === undefined ? {} : objectOption(options.analytics, 'analytics');
|
|
135
|
+
const googleAnalyticsId = resolveProvider(analytics.googleAnalytics, 'analytics.googleAnalytics', 'measurementId', GOOGLE_ANALYTICS_ID_PATTERN, DEFAULT_GOOGLE_ANALYTICS_ID);
|
|
136
|
+
const fiftyOneLaId = resolveProvider(analytics.fiftyOneLa, 'analytics.fiftyOneLa', 'siteId', FIFTY_ONE_LA_ID_PATTERN, DEFAULT_FIFTY_ONE_LA_ID);
|
|
137
|
+
const posthogSearchQuery = resolvePostHogSearchQuery(analytics.posthog);
|
|
138
|
+
const posthogApiKey = resolveProvider(providerOptionsWithoutSearchQuery(analytics.posthog), 'analytics.posthog', 'apiKey', POSTHOG_API_KEY_PATTERN, DEFAULT_POSTHOG_API_KEY);
|
|
100
139
|
return {
|
|
101
140
|
footer: featureEnabled(options.footer, 'footer'),
|
|
102
141
|
footerAvatar: resolveFooterAvatar(options.footer),
|
|
103
142
|
links: options.links === undefined ? {} : objectOption(options.links, 'links'),
|
|
104
|
-
googleAnalyticsId
|
|
105
|
-
fiftyOneLaId
|
|
106
|
-
posthogApiKey
|
|
143
|
+
googleAnalyticsId,
|
|
144
|
+
fiftyOneLaId,
|
|
145
|
+
posthogApiKey,
|
|
146
|
+
posthogSearchQuery,
|
|
147
|
+
consent: resolveConsent(analytics.consent, [
|
|
148
|
+
...(googleAnalyticsId === undefined ? [] : ['googleAnalytics']),
|
|
149
|
+
...(fiftyOneLaId === undefined ? [] : ['fiftyOneLa']),
|
|
150
|
+
...(posthogApiKey === undefined ? [] : ['posthog']),
|
|
151
|
+
]),
|
|
107
152
|
promoto: featureEnabled(options.promoto, 'promoto'),
|
|
108
153
|
languageSelector: featureEnabled(options.languageSelector, 'languageSelector'),
|
|
109
154
|
seo: resolveSeo(options.seo),
|
|
@@ -13,5 +13,11 @@ export interface FooterViewProps {
|
|
|
13
13
|
year: number;
|
|
14
14
|
buildInfo?: BuildInfo;
|
|
15
15
|
avatar?: FooterAvatar;
|
|
16
|
+
/** Identifies the hosting site in campaign tags; without it the brand links stay untagged. */
|
|
17
|
+
siteId?: string;
|
|
18
|
+
/** The hosting site's URL, used only to recognise same-origin destinations that must stay untagged. */
|
|
19
|
+
siteUrl?: string;
|
|
20
|
+
/** The localized "Cookie settings" label. Set only when the consent popup is mounted. */
|
|
21
|
+
consentLabel?: string;
|
|
16
22
|
}
|
|
17
|
-
export declare function FooterView({ links, version, year, buildInfo, avatar }: FooterViewProps): ReactElement;
|
|
23
|
+
export declare function FooterView({ links, version, year, buildInfo, avatar, siteId, siteUrl, consentLabel }: FooterViewProps): ReactElement;
|
|
@@ -5,16 +5,33 @@ import { buildInfoItems } from '@hagicode/hagilight-core/build-info';
|
|
|
5
5
|
import { pickFooterBrandLinks, resolveFooterBrand } from '@hagicode/hagilight-core/footer-brand';
|
|
6
6
|
import { footerAvatarImageAttributes } from '@hagicode/hagilight-core/footer-avatar';
|
|
7
7
|
import { randomPoolAttribute } from '@hagicode/hagilight-core/random-image';
|
|
8
|
+
import { withCampaignUtm } from '@hagicode/hagilight-core/links';
|
|
8
9
|
function LinkSection({ heading, navLabel, links, tagged }) {
|
|
9
10
|
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))) }) })] }));
|
|
10
11
|
}
|
|
11
|
-
function FooterBrand({ locale, links, avatar }) {
|
|
12
|
+
function FooterBrand({ locale, links, avatar, siteId, siteUrl }) {
|
|
12
13
|
const brand = resolveFooterBrand(locale);
|
|
13
14
|
const { primary, secondary } = pickFooterBrandLinks(links);
|
|
14
|
-
|
|
15
|
+
const origin = (() => {
|
|
16
|
+
try {
|
|
17
|
+
return siteUrl ? new URL(siteUrl).origin : undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
// Re-tagged from the untagged destination, so the brand block's content reads `footer_brand`.
|
|
24
|
+
const brandHref = (link) => withCampaignUtm(link.untaggedHref ?? link.href, {
|
|
25
|
+
siteId,
|
|
26
|
+
medium: 'footer',
|
|
27
|
+
campaign: link.id,
|
|
28
|
+
content: 'footer_brand',
|
|
29
|
+
origin,
|
|
30
|
+
});
|
|
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))) })] }));
|
|
15
32
|
}
|
|
16
|
-
export function FooterView({ links, version, year, buildInfo, avatar }) {
|
|
33
|
+
export function FooterView({ links, version, year, buildInfo, avatar, siteId, siteUrl, consentLabel }) {
|
|
17
34
|
const { labels } = links;
|
|
18
35
|
const buildItems = buildInfoItems(buildInfo);
|
|
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 && (
|
|
36
|
+
return (_jsxs("footer", { className: "hagilight-footer", children: [_jsx(FooterBrand, { locale: links.locale, links: links.quick, avatar: avatar, siteId: siteId, siteUrl: siteUrl }), _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 || 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)))] })] }));
|
|
20
37
|
}
|
|
@@ -3,6 +3,7 @@ import FooterDefaults from '@theme-init/Footer';
|
|
|
3
3
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
4
4
|
import { usePluginData } from '@docusaurus/useGlobalData';
|
|
5
5
|
import { resolveFooterLinks } from '../../footer.js';
|
|
6
|
+
import { resolveConsentCopy } from '@hagicode/hagilight-core/consent-copy';
|
|
6
7
|
import { PLUGIN_NAME } from '../../global-data.js';
|
|
7
8
|
import { FooterView } from './FooterView.js';
|
|
8
9
|
import avatarAvif from '@hagicode/hagilight-core/assets/footer/avatar.avif';
|
|
@@ -21,5 +22,5 @@ export default function Footer() {
|
|
|
21
22
|
links: options.links,
|
|
22
23
|
rssEnabled: options.rss.enabled,
|
|
23
24
|
});
|
|
24
|
-
return (_jsx(FooterView, { links: links, version: version, year: new Date().getFullYear(), buildInfo: buildInfo, avatar: avatar }));
|
|
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
26
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import Content from '@theme-init/NotFound/Content';
|
|
2
|
+
import { type ComponentProps, type ReactElement } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* The Docusaurus 404 content, with one `not_found_viewed` event per view. The wrapper remounts on each
|
|
5
|
+
* client navigation to the 404 route, so each view reports once. Without PostHog the capture is silent.
|
|
6
|
+
*/
|
|
7
|
+
export default function NotFoundContent(props: ComponentProps<typeof Content>): ReactElement;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import Content from '@theme-init/NotFound/Content';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { capturePostHogEvent, referrerDomain } from '@hagicode/hagilight-core/posthog-events';
|
|
5
|
+
/**
|
|
6
|
+
* The Docusaurus 404 content, with one `not_found_viewed` event per view. The wrapper remounts on each
|
|
7
|
+
* client navigation to the 404 route, so each view reports once. Without PostHog the capture is silent.
|
|
8
|
+
*/
|
|
9
|
+
export default function NotFoundContent(props) {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
try {
|
|
12
|
+
capturePostHogEvent('not_found_viewed', {
|
|
13
|
+
path: window.location.pathname,
|
|
14
|
+
referrer_domain: referrerDomain(document.referrer),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Analytics must never break the 404 page.
|
|
19
|
+
}
|
|
20
|
+
}, []);
|
|
21
|
+
return _jsx(Content, { ...props });
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hagicode/hagilight-docusaurus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1-dev.184.1.830a4c6",
|
|
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": {
|
|
@@ -33,12 +33,13 @@
|
|
|
33
33
|
"README.md",
|
|
34
34
|
"styles.css",
|
|
35
35
|
"promoto.css",
|
|
36
|
+
"consent.css",
|
|
36
37
|
"dist/**/*.js",
|
|
37
38
|
"dist/**/*.d.ts"
|
|
38
39
|
],
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@docusaurus/types": "^3.9.2",
|
|
41
|
-
"@hagicode/hagilight-core": "0.
|
|
42
|
+
"@hagicode/hagilight-core": "0.19.1-dev.184.1.830a4c6"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
45
|
"@docusaurus/core": "^3.9.2",
|