keksi 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ texts in English, Finnish and Swedish.
6
6
  keksi loads the trackers itself, and only after consent. You don't add their script tags: you put their ids in the
7
7
  config, so there's nothing to block.
8
8
 
9
- - Google Analytics 4, Google Ads, Meta Pixel and YouTube. The short list is deliberate.
9
+ - Google Analytics 4, Google Ads, Meta Pixel, PostHog and YouTube. The short list is deliberate.
10
10
  - About 5 KB gzipped, inline. No extra requests, no dependencies.
11
11
  - Rendered on the server. Works on static pages, with no layout shift.
12
12
  - The cookie policy table is generated from the same data.
@@ -25,6 +25,7 @@ export default defineConfig({
25
25
  ga: 'G-XXXXXXX',
26
26
  googleAds: 'AW-123456789',
27
27
  meta: '1234567890',
28
+ posthog: { key: 'phc_XXXX' }, // EU cloud by default; set host for US or a proxy
28
29
  youtube: true,
29
30
  policyUrl: '/evasteet',
30
31
  });
@@ -82,16 +83,18 @@ string.
82
83
 
83
84
  ## Tracking events
84
85
 
85
- `gtag()` and `fbq()` are always defined, so you can call them anytime:
86
+ `gtag()`, `fbq()` and `posthog` are always defined, so you can call them anytime:
86
87
 
87
88
  ```ts
88
89
  gtag('event', 'generate_lead');
89
90
  fbq('track', 'Lead');
91
+ posthog.capture('signup');
90
92
  ```
91
93
 
92
94
  - Calls made before consent are queued. They're sent only if the visitor consents on that page.
93
95
  - Google gets Consent Mode v2 signals (basic mode).
94
- - GA4 and Meta Pixel track single-page navigation themselves.
96
+ - GA4, Meta Pixel and PostHog track single-page navigation themselves.
97
+ - PostHog counts as analytics. With the US host, the settings say data is transferred to the US.
95
98
 
96
99
  ## YouTube
97
100
 
package/dist/cli.js CHANGED
@@ -4,6 +4,9 @@
4
4
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { parseArgs } from "node:util";
6
6
 
7
+ // src/glob.ts
8
+ var glob = (pattern, name) => new RegExp(`^${pattern.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(name);
9
+
7
10
  // src/providers.ts
8
11
  var GOOGLE = { company: "Google Ireland Limited", privacyPolicy: "https://policies.google.com/privacy", transfer: "dpf" };
9
12
  var META = { company: "Meta Platforms Ireland Limited", privacyPolicy: "https://www.facebook.com/privacy/policy/", transfer: "dpf" };
@@ -64,6 +67,29 @@ var providers = [
64
67
  ],
65
68
  hosts: ["facebook.net", "facebook.com"]
66
69
  },
70
+ {
71
+ id: "posthog",
72
+ name: "PostHog",
73
+ company: "PostHog Inc.",
74
+ privacyPolicy: "https://posthog.com/privacy",
75
+ category: "analytics",
76
+ purpose: {
77
+ fi: "Tuoteanalytiikka: sivujen katselut, klikkaukset ja k\xE4ytt\xF6tavat. Voi tallentaa istuntoja, jos istuntotallenteet ovat k\xE4yt\xF6ss\xE4.",
78
+ sv: "Produktanalys: sidvisningar, klick och anv\xE4ndningsm\xF6nster. Kan spela in sessioner om sessionsinspelning \xE4r aktiverad.",
79
+ en: "Product analytics: page views, clicks and usage patterns. Can record sessions if session replay is enabled."
80
+ },
81
+ cookies: [
82
+ { name: "ph_*_posthog", duration: "1y" },
83
+ { name: "ph_*_posthog_cpm", duration: "1y" },
84
+ { name: "ph_*_posthog", duration: "persistent", storage: "localStorage" },
85
+ { name: "ph_*_posthog__flags", duration: "persistent", storage: "localStorage" },
86
+ { name: "ph_*_posthog", duration: "session", storage: "sessionStorage" },
87
+ { name: "ph_*_primary_window_exists", duration: "session", storage: "sessionStorage" },
88
+ { name: "ph_*_session_registered_properties", duration: "session", storage: "sessionStorage" },
89
+ { name: "ph_*_window_id", duration: "session", storage: "sessionStorage" }
90
+ ],
91
+ hosts: ["posthog.com"]
92
+ },
67
93
  {
68
94
  id: "youtube",
69
95
  name: "YouTube",
@@ -119,7 +145,7 @@ var purposes = {
119
145
  };
120
146
  var trackerNames = providers.flatMap((p) => p.cookies.flatMap((c) => c.name.split(", ")));
121
147
  var trackerHosts = providers.flatMap((p) => p.hosts);
122
- var matches = (patterns, name) => patterns.some((p) => p.endsWith("*") ? name.startsWith(p.slice(0, -1)) : name === p);
148
+ var matches = (patterns, name) => patterns.some((p) => glob(p, name));
123
149
  function purposeOf(name) {
124
150
  const k = Object.keys(purposes).find((p) => matches([p], name));
125
151
  return k ? purposes[k] : void 0;
package/dist/glob.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** `*` matches any run of characters, e.g. `ph_*_posthog`. */
2
+ export declare const glob: (pattern: string, name: string) => boolean;
package/dist/i18n.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { Locale } from './types.js';
2
2
  declare const fi: {
3
3
  title: string;
4
4
  intro: string;
5
- transfer: string;
5
+ change: string;
6
6
  more: string;
7
7
  accept: string;
8
8
  reject: string;
@@ -22,6 +22,7 @@ declare const fi: {
22
22
  consentPurpose: string;
23
23
  privacyPolicy: string;
24
24
  dpf: string;
25
+ us: string;
25
26
  name: string;
26
27
  duration: string;
27
28
  purpose: string;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  var fi = {
3
3
  title: "Ev\xE4steet",
4
4
  intro: "K\xE4yt\xE4mme v\xE4ltt\xE4m\xE4tt\xF6mien ev\xE4steiden lis\xE4ksi seuraavia palveluita vain, jos annat siihen luvan:",
5
- transfer: "Palveluntarjoajat voivat siirt\xE4\xE4 tietoja Yhdysvaltoihin. Voit muuttaa valintaasi milloin tahansa.",
5
+ change: "Voit muuttaa valintaasi milloin tahansa.",
6
6
  more: "Lue lis\xE4\xE4",
7
7
  accept: "Hyv\xE4ksy kaikki",
8
8
  reject: "Hylk\xE4\xE4 kaikki",
@@ -22,6 +22,7 @@ var fi = {
22
22
  consentPurpose: "Tallentaa ev\xE4stevalintasi.",
23
23
  privacyPolicy: "Tietosuojaseloste",
24
24
  dpf: "Tietoja voidaan siirt\xE4\xE4 Yhdysvaltoihin EU:n ja Yhdysvaltojen v\xE4lisen tietosuojakehyksen (DPF) perusteella.",
25
+ us: "Tietoja siirret\xE4\xE4n Yhdysvaltoihin.",
25
26
  name: "Nimi",
26
27
  duration: "Kesto",
27
28
  purpose: "Tarkoitus",
@@ -39,7 +40,7 @@ var fi = {
39
40
  var sv = {
40
41
  title: "Kakor",
41
42
  intro: "Ut\xF6ver n\xF6dv\xE4ndiga kakor anv\xE4nder vi f\xF6ljande tj\xE4nster endast om du ger ditt samtycke:",
42
- transfer: "Tj\xE4nsteleverant\xF6rerna kan \xF6verf\xF6ra uppgifter till USA. Du kan \xE4ndra ditt val n\xE4r som helst.",
43
+ change: "Du kan \xE4ndra ditt val n\xE4r som helst.",
43
44
  more: "L\xE4s mer",
44
45
  accept: "Godk\xE4nn alla",
45
46
  reject: "Avvisa alla",
@@ -59,6 +60,7 @@ var sv = {
59
60
  consentPurpose: "Sparar ditt val av kakor.",
60
61
  privacyPolicy: "Dataskyddsbeskrivning",
61
62
  dpf: "Uppgifter kan \xF6verf\xF6ras till USA med st\xF6d av ramverket f\xF6r dataskydd mellan EU och USA (DPF).",
63
+ us: "Uppgifter \xF6verf\xF6rs till USA.",
62
64
  name: "Namn",
63
65
  duration: "Varaktighet",
64
66
  purpose: "Syfte",
@@ -76,7 +78,7 @@ var sv = {
76
78
  var en = {
77
79
  title: "Cookies",
78
80
  intro: "Besides necessary cookies, we use the following services only if you allow it:",
79
- transfer: "The providers may transfer data to the United States. You can change your choice at any time.",
81
+ change: "You can change your choice at any time.",
80
82
  more: "Read more",
81
83
  accept: "Accept all",
82
84
  reject: "Reject all",
@@ -96,6 +98,7 @@ var en = {
96
98
  consentPurpose: "Stores your cookie choice.",
97
99
  privacyPolicy: "Privacy policy",
98
100
  dpf: "Data may be transferred to the United States under the EU\u2013US Data Privacy Framework (DPF).",
101
+ us: "Data is transferred to the United States.",
99
102
  name: "Name",
100
103
  duration: "Duration",
101
104
  purpose: "Purpose",
@@ -172,6 +175,29 @@ var providers = [
172
175
  ],
173
176
  hosts: ["facebook.net", "facebook.com"]
174
177
  },
178
+ {
179
+ id: "posthog",
180
+ name: "PostHog",
181
+ company: "PostHog Inc.",
182
+ privacyPolicy: "https://posthog.com/privacy",
183
+ category: "analytics",
184
+ purpose: {
185
+ fi: "Tuoteanalytiikka: sivujen katselut, klikkaukset ja k\xE4ytt\xF6tavat. Voi tallentaa istuntoja, jos istuntotallenteet ovat k\xE4yt\xF6ss\xE4.",
186
+ sv: "Produktanalys: sidvisningar, klick och anv\xE4ndningsm\xF6nster. Kan spela in sessioner om sessionsinspelning \xE4r aktiverad.",
187
+ en: "Product analytics: page views, clicks and usage patterns. Can record sessions if session replay is enabled."
188
+ },
189
+ cookies: [
190
+ { name: "ph_*_posthog", duration: "1y" },
191
+ { name: "ph_*_posthog_cpm", duration: "1y" },
192
+ { name: "ph_*_posthog", duration: "persistent", storage: "localStorage" },
193
+ { name: "ph_*_posthog__flags", duration: "persistent", storage: "localStorage" },
194
+ { name: "ph_*_posthog", duration: "session", storage: "sessionStorage" },
195
+ { name: "ph_*_primary_window_exists", duration: "session", storage: "sessionStorage" },
196
+ { name: "ph_*_session_registered_properties", duration: "session", storage: "sessionStorage" },
197
+ { name: "ph_*_window_id", duration: "session", storage: "sessionStorage" }
198
+ ],
199
+ hosts: ["posthog.com"]
200
+ },
175
201
  {
176
202
  id: "youtube",
177
203
  name: "YouTube",
@@ -200,8 +226,11 @@ function defineConfig(config) {
200
226
  return config;
201
227
  }
202
228
  var list = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
229
+ var posthogHost = (config) => (config.posthog?.host ?? "https://eu.i.posthog.com").replace(/\/$/, "");
203
230
  function activeProviders(config) {
204
- return providers.filter((p) => p.id === "youtube" ? config.youtube : list(config[p.id]).length);
231
+ return providers.filter(
232
+ (p) => p.id === "youtube" ? config.youtube : p.id === "posthog" ? config.posthog?.key : list(config[p.id]).length
233
+ ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p);
205
234
  }
206
235
  function categoriesOf(config) {
207
236
  return ["analytics", "marketing"].filter((c) => activeProviders(config).some((p) => p.category === c));
@@ -226,6 +255,7 @@ function runtimeData(config, locale) {
226
255
  ga: list(config.ga),
227
256
  ads: list(config.googleAds),
228
257
  meta: list(config.meta),
258
+ ph: config.posthog?.key ? { key: config.posthog.key, host: posthogHost(config) } : void 0,
229
259
  yt: !!config.youtube,
230
260
  del: { analytics: del("analytics"), marketing: del("marketing") },
231
261
  t: { ytText: t.ytText, ytButton: t.ytButton, ytOpen: t.ytOpen }
@@ -234,34 +264,46 @@ function runtimeData(config, locale) {
234
264
 
235
265
  // src/styles.ts
236
266
  var css = [
237
- ":where(.keksi,.keksi-dialog){--_bg:var(--keksi-bg,#fff);--_fg:var(--keksi-fg,#1b1b1b);--_line:var(--keksi-line,#c8c8c8);--_r:var(--keksi-radius,6px);box-sizing:border-box;background:var(--_bg);color:var(--_fg);font:inherit;font-size:15px;line-height:1.5}",
238
- ":where(.keksi-policy){--_line:var(--keksi-line,#c8c8c8)}",
239
- ":where(.keksi){position:fixed;z-index:2147483000;inset:auto 1rem 1rem;max-width:34rem;max-height:calc(100vh - 2rem);overflow:auto;margin-inline:auto;padding:1.25rem;border:1px solid var(--_line);border-radius:var(--_r);box-shadow:0 4px 24px #0003}",
240
- ":where(.keksi,.keksi-dialog) :where(h2){font-size:1.125rem;margin:0 0 .5rem}",
241
- ":where(.keksi) :where(p,ul){margin:0 0 .75rem}",
242
- ":where(.keksi) :where(ul){padding-left:1.25rem}",
243
- ":where(.keksi,.keksi-dialog) :where(a){color:inherit}",
244
- ":where(.keksi-actions){display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1rem}",
245
- ":where(.keksi-btn){flex:1 1 auto;font:inherit;font-weight:600;padding:.6rem 1rem;border:1px solid var(--_fg);border-radius:var(--_r);background:var(--_bg);color:var(--_fg);cursor:pointer}",
267
+ ":where(.keksi,.keksi-policy){--_bg:var(--keksi-bg,#fff);--_fg:var(--keksi-fg,#1b1b1b);--_line:var(--keksi-line,#e3e3e3);--_soft:color-mix(in srgb,var(--_fg) 4%,var(--_bg));--_r:var(--keksi-radius,8px)}",
268
+ ":where(.keksi){box-sizing:border-box;background:var(--_bg);color:var(--_fg);font:inherit;font-size:14px;line-height:1.5;border:1px solid var(--_line);border-radius:var(--_r);box-shadow:0 8px 32px #0000002e}",
269
+ ":where(.keksi){position:fixed;z-index:2147483000;inset:auto 1rem 1rem;display:flex;flex-direction:column;max-width:34rem;max-height:calc(100vh - 2rem);margin-inline:auto;overflow:hidden}",
270
+ ":where(.keksi,.keksi-main,.keksi-settings)[hidden]{display:none}",
271
+ ":where(.keksi-main,.keksi-settings){display:flex;flex-direction:column;min-height:0;margin:0}",
272
+ ":where(.keksi:has(.keksi-settings:not([hidden]))){max-width:40rem}",
273
+ ":where(.keksi-settings .keksi-body){padding-block:0}",
274
+ ":where(.keksi-head){display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.875rem 1.25rem;border-bottom:1px solid var(--_line)}",
275
+ ":where(.keksi-head) :where(h2){font-size:1rem;font-weight:600;line-height:1.3;margin:0}",
276
+ ":where(.keksi-body){padding:1rem 1.25rem;overflow:auto}",
277
+ ":where(.keksi) :where(.keksi-body>*){margin:0 0 .75rem}",
278
+ ":where(.keksi) :where(.keksi-body>:last-child){margin-bottom:0}",
279
+ ":where(.keksi) :where(ul){list-style:none;padding:0}",
280
+ ":where(.keksi) :where(strong){font-weight:600}",
281
+ ":where(.keksi) :where(a){color:inherit}",
282
+ ".keksi a{text-decoration:underline}",
283
+ ":where(.keksi-actions){display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem 1.25rem;background:var(--_soft)}",
284
+ ":where(.keksi-btn){min-width:7.5rem;font:inherit;font-weight:600;padding:.5rem 1rem;border:1px solid var(--_fg);border-radius:calc(var(--_r) - 2px);background:var(--_bg);color:var(--_fg);cursor:pointer}",
246
285
  ":where(.keksi-btn:hover){background:var(--_fg);color:var(--_bg)}",
247
- ":where(.keksi,.keksi-dialog,keksi-youtube) :where(button,a,input,summary):focus-visible{outline:3px solid var(--keksi-focus,#2563eb);outline-offset:2px}",
248
- ":where(.keksi-dialog){width:min(40rem,calc(100% - 2rem));max-height:calc(100% - 2rem);padding:0;border:0;border-radius:var(--_r)}",
249
- ":where(.keksi-dialog)::backdrop{background:#0009}",
250
- ":where(.keksi-dialog form){padding:1.25rem}",
251
- ":where(.keksi-head,.keksi-cat-head){display:flex;align-items:center;justify-content:space-between;gap:1rem}",
252
- ":where(.keksi-close){font:inherit;font-size:1.5rem;line-height:1;padding:.25rem .5rem;border:0;background:none;color:inherit;cursor:pointer}",
253
- ":where(.keksi-cat){padding:.75rem 0;border-top:1px solid var(--_line)}",
254
- ":where(.keksi-cat-head) :where(h2,h3,h4,h5){font-size:1rem;margin:0}",
286
+ ":where(.keksi-text){margin-right:auto;padding:.5rem .25rem;font:inherit;border:0;background:none;color:inherit;text-decoration:underline;text-underline-offset:2px;cursor:pointer}",
287
+ "@media (max-width:30rem){:where(.keksi-btn){flex:1 1 auto}}",
288
+ ":where(.keksi,keksi-youtube) :where(button,a,input,summary):focus-visible{outline:3px solid var(--keksi-focus,#2563eb);outline-offset:2px}",
289
+ ":where(.keksi-close){font:inherit;font-size:1.375rem;line-height:1;margin:-.25rem -.5rem;padding:.25rem .5rem;border:0;background:none;color:inherit;cursor:pointer}",
290
+ ":where(.keksi-cat){padding:1rem 0;border-top:1px solid var(--_line)}",
291
+ ":where(.keksi-body>.keksi-cat:first-child){border-top:0}",
292
+ ":where(.keksi-cat-head){display:flex;align-items:center;justify-content:space-between;gap:1rem}",
293
+ ":where(.keksi-cat-head) :where(h2,h3,h4,h5){font-size:1rem;font-weight:600;margin:0}",
294
+ ":where(.keksi-cat-head) :where(span){font-size:.8125rem;opacity:.7}",
255
295
  ":where(.keksi-cat) :where(p){margin:.25rem 0}",
256
- ":where(.keksi-cat input[role=switch]){appearance:none;position:relative;flex:none;width:2.75rem;height:1.5rem;margin:0;border-radius:1rem;background:var(--_line);cursor:pointer}",
257
- ':where(.keksi-cat input[role=switch])::before{content:"";position:absolute;top:.2rem;left:.2rem;width:1.1rem;height:1.1rem;border-radius:50%;background:#fff;transition:transform .15s}',
296
+ ":where(.keksi-cat input[role=switch]){appearance:none;position:relative;flex:none;width:2.5rem;height:1.375rem;margin:0;border-radius:1rem;background:color-mix(in srgb,var(--_fg) 22%,var(--_bg));cursor:pointer}",
297
+ ':where(.keksi-cat input[role=switch])::before{content:"";position:absolute;top:.1875rem;left:.1875rem;width:1rem;height:1rem;border-radius:50%;background:#fff;transition:transform .15s}',
258
298
  ":where(.keksi-cat input[role=switch]:checked){background:var(--_fg)}",
259
- ":where(.keksi-cat input[role=switch]:checked)::before{transform:translateX(1.25rem)}",
299
+ ":where(.keksi-cat input[role=switch]:checked)::before{transform:translateX(1.125rem)}",
260
300
  ":where(.keksi-cat summary){margin-top:.5rem;font-weight:600;cursor:pointer}",
261
- ":where(.keksi-provider){margin:.75rem 0}",
262
- ":where(.keksi-provider) :where(h3,h4,h5,h6){font-size:.9375rem;margin:0}",
263
- ":where(.keksi-cat table){width:100%;border-collapse:collapse;font-size:.875rem;margin-top:.25rem}",
264
- ":where(.keksi-cat) :where(th,td){padding:.25rem .75rem .25rem 0;border-bottom:1px solid var(--_line);text-align:left;vertical-align:top}",
301
+ ":where(.keksi-provider){margin:.75rem 0;padding:.75rem .875rem;border-radius:calc(var(--_r) - 2px);background:var(--_soft)}",
302
+ ":where(.keksi-policy .keksi-provider){padding:0;background:none}",
303
+ ":where(.keksi-provider) :where(h3,h4,h5,h6){font-size:.9375rem;font-weight:600;margin:0}",
304
+ ":where(.keksi-cat table){width:100%;border-collapse:collapse;font-size:.8125rem;margin-top:.5rem}",
305
+ ":where(.keksi-cat) :where(th,td){padding:.3rem .75rem .3rem 0;border-bottom:1px solid var(--_line);text-align:left;vertical-align:top}",
306
+ ":where(.keksi-cat) :where(th){font-weight:600}",
265
307
  ":where(keksi-youtube){display:block;aspect-ratio:16/9;background:#111;color:#fff}",
266
308
  ":where(keksi-youtube iframe){display:block;width:100%;height:100%;border:0}",
267
309
  ":where(.keksi-yt){display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.75rem;height:100%;padding:1rem;box-sizing:border-box;text-align:center}",
@@ -282,7 +324,7 @@ function renderKeksi(config, { lang = "fi", nonce } = {}) {
282
324
  const btn = (action, label, type = "button") => `<button type="${type}" class="keksi-btn" data-keksi="${action}">${e(label)}</button>`;
283
325
  const uses = cats.map((c) => `<li><strong>${e(t[c])}:</strong> ${e(active.filter((p) => p.category === c).map((p) => p.name).join(", "))}</li>`).join("");
284
326
  const json = JSON.stringify(runtimeData(config, lang)).replace(/</g, "\\u003c");
285
- return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><h2 id="keksi-title">${e(t.title)}</h2><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.transfer)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p><div class="keksi-actions">${btn("accept", t.accept)}${btn("reject", t.reject)}<button type="button" class="keksi-btn" data-keksi-open>${e(t.settings)}</button></div></section><dialog id="keksi-dialog" class="keksi-dialog" aria-labelledby="keksi-dialog-title"><form method="dialog"><div class="keksi-head"><h2 id="keksi-dialog-title">${e(t.settingsTitle)}</h2><button class="keksi-close" aria-label="${e(t.close)}">\xD7</button></div>` + sections(config, lang, 3, true) + `<div class="keksi-actions">${btn("save", t.save, "submit")}${btn("accept", t.accept, "submit")}${btn("reject", t.reject, "submit")}</div></form></dialog><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var v=["analytics","marketing"],C={analytics:!1,marketing:!1};function g(...a){return arguments}function q(a,o=window){if(typeof o.keksi?.sync=="function")return o.keksi.sync(),o.keksi;let r=o.document,h=e=>r.getElementById(e),l=w(),A=!1,E=!1,b=new Set,$=g("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(a.ga.length||a.ads.length){let e=o.dataLayer??(o.dataLayer=[]);o.gtag=function(){e.push(arguments)},e.push($)}if(a.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),o.fbq=o._fbq=e}function w(){let e=r.cookie.split("; ").find(s=>s.startsWith(`${a.name}=`))?.slice(a.name.length+1),[n,,t,i]=e?.split(".")??[];return n!==a.v||!t||!i?null:{id:t,c:{analytics:i.includes("a"),marketing:i.includes("m")}}}function T(e){let n=l?.id??Array.from(o.crypto.getRandomValues(new Uint8Array(8)),i=>i.toString(16).padStart(2,"0")).join(""),t=(e.analytics?"a":"")+(e.marketing?"m":"")||"-";return r.cookie=`${a.name}=${a.v}.${Date.now().toString(36)}.${n}.${t}; Path=/; Max-Age=${a.days*86400}; SameSite=Lax`+(a.domain?`; Domain=${a.domain}`:"")+(o.location.protocol==="https:"?"; Secure":""),{id:n,c:e}}function S(e){let n=r.createElement("script");n.async=!0,n.src=e,r.head.append(n)}function _(e){let n=[...e.analytics?a.ga:[],...e.marketing?a.ads:[]].filter(t=>!b.has(t));if(n.length){let t=e.marketing?"granted":"denied",i=[g("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];b.size||i.push(g("js",new Date));for(let s of n)i.push(g("config",s)),b.add(s);if(A)for(let s of i)o.dataLayer.push(s);else{let s=o.dataLayer;s.splice(s.indexOf($)+1,0,...i),S(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(n[0])}`),A=!0}}if(e.marketing&&a.meta.length&&!E&&(E=!0,o.fbq.queue.unshift(...a.meta.map(t=>g("init",t)),g("track","PageView")),S("https://connect.facebook.net/en_US/fbevents.js")),e.marketing)for(let t of r.querySelectorAll("keksi-youtube"))t.show(!0)}function M(e){let n=e.flatMap(c=>a.del[c]),t=c=>n.some(u=>u.endsWith("*")?c.startsWith(u.slice(0,-1)):c===u),i=o.location.hostname.split("."),s=["",...i.map((c,u)=>`; Domain=${i.slice(u).join(".")}`)];for(let c of r.cookie.split("; ")){let u=c.split("=")[0];if(t(u))for(let y of s)r.cookie=`${u}=; Max-Age=0; Path=/${y}`}try{for(let c of Object.keys(o.localStorage))t(c)&&o.localStorage.removeItem(c)}catch{}}function f(e){let n=l?.c;l=T(e),k(),r.dispatchEvent(new o.CustomEvent("keksi:change",{detail:{...e}})),M(v.filter(t=>!e[t])),v.some(t=>n?.[t]&&!e[t])?o.location.reload():_(e)}function k(){let e=h("keksi-banner");e&&(e.hidden=!!l)}function x(){let e=h("keksi-dialog");if(e){for(let n of v){let t=e.querySelector(`[name=${n}]`);t&&(t.checked=!!l?.c[n])}e.showModal()}}r.addEventListener("click",e=>{let n=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!n)return;if(n.hasAttribute("data-keksi-open"))return e.preventDefault(),x();let t=n.getAttribute("data-keksi");if(t==="accept")f({analytics:!0,marketing:!0});else if(t==="reject")f(C);else if(t==="save"){let i=s=>!!h("keksi-dialog")?.querySelector(`[name=${s}]`)?.checked;f({analytics:i("analytics"),marketing:i("marketing")})}}),r.addEventListener("astro:after-swap",k);class j extends o.HTMLElement{connectedCallback(){this.show(!!l?.c.marketing)}show(n,t=!1){let i=encodeURIComponent(this.getAttribute("video")??""),s=this.getAttribute("label");if(n){if(this.querySelector("iframe"))return;let d=r.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${i}${t?"?autoplay=1":""}`,d.title=s??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let c=r.createElement("div");c.className="keksi-yt";let u=this.getAttribute("poster");u&&(c.style.background=`linear-gradient(#000a,#000a),center/cover url(${JSON.stringify(u)})`);let y=r.createElement("p");y.textContent=a.t.ytText;let m=r.createElement("button");m.type="button",m.textContent=a.t.ytButton,s&&m.setAttribute("aria-label",`${a.t.ytButton}: ${s}`),m.onclick=()=>this.show(!0,!0);let p=r.createElement("a");p.href=`https://www.youtube.com/watch?v=${i}`,p.target="_blank",p.rel="noopener",p.textContent=a.t.ytOpen,c.append(y,m,p),this.replaceChildren(c)}}a.yt&&!o.customElements.get("keksi-youtube")&&o.customElements.define("keksi-youtube",j);let L={get consent(){return l?{...l.c}:null},open:x,save:e=>f({...l?.c??C,...e}),acceptAll:()=>f({analytics:!0,marketing:!0}),rejectAll:()=>f(C),sync:k};return o.keksi=L,k(),l&&_(l.c),L}q(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
327
+ return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><div class="keksi-main"><div class="keksi-head"><h2 id="keksi-title">${e(t.title)}</h2></div><div class="keksi-body"><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.change)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p></div><div class="keksi-actions"><button type="button" class="keksi-text" data-keksi-open>${e(t.settings)}</button>${btn("reject", t.reject)}${btn("accept", t.accept)}</div></div><form id="keksi-settings" class="keksi-settings" aria-labelledby="keksi-settings-title" hidden><div class="keksi-head"><h2 id="keksi-settings-title">${e(t.settingsTitle)}</h2><button type="button" class="keksi-close" data-keksi="close" aria-label="${e(t.close)}">\xD7</button></div><div class="keksi-body">${sections(config, lang, 3, true)}</div><div class="keksi-actions">${btn("save", t.save)}${btn("reject", t.reject)}${btn("accept", t.accept)}</div></form></section><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var M=(o,s)=>new RegExp(`^${o.split("*").map(i=>i.replace(/[.+?^${}()|[\\]\\\\]/g,"\\\\$&")).join(".*")}$`).test(s);var $=["analytics","marketing"],C={analytics:!1,marketing:!1};function p(...o){return arguments}function j(o,s=window){if(typeof s.keksi?.sync=="function")return s.keksi.sync(),s.keksi;let i=s.document,f=e=>i.getElementById(e),u=q(),_=!1,A=!1,S=!1,v=new Set,L=p("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(o.ga.length||o.ads.length){let e=s.dataLayer??(s.dataLayer=[]);s.gtag=function(){e.push(arguments)},e.push(L)}if(o.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),s.fbq=s._fbq=e}if(o.ph){let e=Object.assign([],{_i:[],people:[]});for(let n of"capture identify alias reset register register_once unregister group setPersonProperties getFeatureFlag isFeatureEnabled onFeatureFlags reloadFeatureFlags get_distinct_id startSessionRecording stopSessionRecording captureException on".split(" "))e[n]=(...t)=>e.push([n,...t]);s.posthog=e}function q(){let e=i.cookie.split("; ").find(r=>r.startsWith(`${o.name}=`))?.slice(o.name.length+1),[n,,t,a]=e?.split(".")??[];return n!==o.v||!t||!a?null:{id:t,c:{analytics:a.includes("a"),marketing:a.includes("m")}}}function F(e){let n=u?.id??Array.from(s.crypto.getRandomValues(new Uint8Array(8)),a=>a.toString(16).padStart(2,"0")).join(""),t=(e.analytics?"a":"")+(e.marketing?"m":"")||"-";return i.cookie=`${o.name}=${o.v}.${Date.now().toString(36)}.${n}.${t}; Path=/; Max-Age=${o.days*86400}; SameSite=Lax`+(o.domain?`; Domain=${o.domain}`:"")+(s.location.protocol==="https:"?"; Secure":""),{id:n,c:e}}function E(e){let n=i.createElement("script");return n.async=!0,n.src=e,i.head.append(n),n}function w(e){let n=[...e.analytics?o.ga:[],...e.marketing?o.ads:[]].filter(t=>!v.has(t));if(n.length){let t=e.marketing?"granted":"denied",a=[p("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];v.size||a.push(p("js",new Date));for(let r of n)a.push(p("config",r)),v.add(r);if(_)for(let r of a)s.dataLayer.push(r);else{let r=s.dataLayer;r.splice(r.indexOf(L)+1,0,...a),E(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(n[0])}`),_=!0}}if(e.marketing&&o.meta.length&&!A&&(A=!0,s.fbq.queue.unshift(...o.meta.map(t=>p("init",t)),p("track","PageView")),E("https://connect.facebook.net/en_US/fbevents.js")),e.analytics&&o.ph&&!S&&(S=!0,s.posthog._i.push([o.ph.key,{api_host:o.ph.host,defaults:"2026-08-30"},"posthog"]),E(`${o.ph.host.replace(".i.posthog.com","-assets.i.posthog.com")}/static/array.js`).crossOrigin="anonymous"),e.marketing)for(let t of i.querySelectorAll("keksi-youtube"))t.show(!0)}function H(e){let n=e.flatMap(c=>o.del[c]),t=c=>n.some(l=>M(l,c)),a=s.location.hostname.split("."),r=["",...a.map((c,l)=>`; Domain=${a.slice(l).join(".")}`)];for(let c of i.cookie.split("; ")){let l=c.split("=")[0];if(t(l))for(let b of r)i.cookie=`${l}=; Max-Age=0; Path=/${b}`}try{for(let c of[s.localStorage,s.sessionStorage])for(let l of Object.keys(c))t(l)&&c.removeItem(l)}catch{}}function g(e){let n=u?.c;u=F(e),y(),i.dispatchEvent(new s.CustomEvent("keksi:change",{detail:{...e}})),H($.filter(t=>!e[t])),$.some(t=>n?.[t]&&!e[t])?s.location.reload():w(e)}function h(e=!1,n=!u||e){let t=f("keksi-banner"),a=f("keksi-settings");!t||!a||(t.hidden=!n,a.hidden=!e,t.firstElementChild.hidden=e)}function y(){h()}function x(){let e=f("keksi-settings");for(let n of $){let t=e?.querySelector(`[name=${n}]`);t&&(t.checked=!!u?.c[n])}h(!0),e?.querySelector(".keksi-close")?.focus()}i.addEventListener("click",e=>{let n=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!n)return;if(n.hasAttribute("data-keksi-open"))return e.preventDefault(),x();let t=n.getAttribute("data-keksi");if(t==="close")h();else if(t==="accept")g({analytics:!0,marketing:!0});else if(t==="reject")g(C);else if(t==="save"){let a=r=>!!f("keksi-settings")?.querySelector(`[name=${r}]`)?.checked;g({analytics:a("analytics"),marketing:a("marketing")})}}),i.addEventListener("keydown",e=>{e.key==="Escape"&&f("keksi-settings")?.hidden===!1&&h()}),i.addEventListener("astro:after-swap",y);class I extends s.HTMLElement{connectedCallback(){this.show(!!u?.c.marketing)}show(n,t=!1){let a=encodeURIComponent(this.getAttribute("video")??""),r=this.getAttribute("label");if(n){if(this.querySelector("iframe"))return;let d=i.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${a}${t?"?autoplay=1":""}`,d.title=r??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let c=i.createElement("div");c.className="keksi-yt";let l=this.getAttribute("poster");l&&(c.style.background=`linear-gradient(#000a,#000a),center/cover url(${JSON.stringify(l)})`);let b=i.createElement("p");b.textContent=o.t.ytText;let m=i.createElement("button");m.type="button",m.textContent=o.t.ytButton,r&&m.setAttribute("aria-label",`${o.t.ytButton}: ${r}`),m.onclick=()=>this.show(!0,!0);let k=i.createElement("a");k.href=`https://www.youtube.com/watch?v=${a}`,k.target="_blank",k.rel="noopener",k.textContent=o.t.ytOpen,c.append(b,m,k),this.replaceChildren(c)}}o.yt&&!s.customElements.get("keksi-youtube")&&s.customElements.define("keksi-youtube",I);let T={get consent(){return u?{...u.c}:null},open:x,save:e=>g({...u?.c??C,...e}),acceptAll:()=>g({analytics:!0,marketing:!0}),rejectAll:()=>g(C),sync:y};return s.keksi=T,y(),u&&w(u.c),T}j(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
286
328
  }
287
329
  function cookieTable(config, { lang = "fi", headingLevel = 2 } = {}) {
288
330
  return `<div class="keksi-policy">${sections(config, lang, headingLevel, false)}</div>`;
@@ -303,7 +345,7 @@ function sections(config, lang, h, dialog) {
303
345
  const items = c === "necessary" ? [site] : active.filter((p) => p.category === c).map((p) => ({ ...p, purpose: p.purpose[lang] }));
304
346
  const head = c === "necessary" ? `<h${h}>${e(t.necessary)}</h${h}><span>${e(t.alwaysOn)}</span>` : dialog ? `<h${h}><label for="keksi-c-${c}">${e(t[c])}</label></h${h}><input type="checkbox" role="switch" id="keksi-c-${c}" name="${c}" aria-describedby="keksi-i-${c}">` : `<h${h}>${e(t[c])}</h${h}>`;
305
347
  const list2 = items.map(
306
- (p) => `<div class="keksi-provider"><h${h + 1}>${e(p.name)}</h${h + 1}>${p.purpose ? `<p>${e(p.purpose)}</p>` : ""}` + (p.company && p.privacyPolicy ? `<p>${e(p.company)} \xB7 <a href="${e(p.privacyPolicy)}" target="_blank" rel="noopener">${e(t.privacyPolicy)}</a></p>` + (p.transfer ? `<p>${e(t.dpf)}</p>` : "") : "") + table(p.cookies, t) + `</div>`
348
+ (p) => `<div class="keksi-provider"><h${h + 1}>${e(p.name)}</h${h + 1}>${p.purpose ? `<p>${e(p.purpose)}</p>` : ""}` + (p.company && p.privacyPolicy ? `<p>${e(p.company)} \xB7 <a href="${e(p.privacyPolicy)}" target="_blank" rel="noopener">${e(t.privacyPolicy)}</a></p>` + (p.transfer ? `<p>${e(t[p.transfer])}</p>` : "") : "") + table(p.cookies, t) + `</div>`
307
349
  ).join("");
308
350
  const body = dialog ? `<details><summary>${e(t.providers)} (${items.length})</summary>${list2}</details>` : list2;
309
351
  return `<section class="keksi-cat"><div class="keksi-cat-head">${head}</div><p id="keksi-i-${c}">${e(t[`${c}Info`])}</p>${body}</section>`;
@@ -2,7 +2,7 @@
2
2
  var fi = {
3
3
  title: "Ev\xE4steet",
4
4
  intro: "K\xE4yt\xE4mme v\xE4ltt\xE4m\xE4tt\xF6mien ev\xE4steiden lis\xE4ksi seuraavia palveluita vain, jos annat siihen luvan:",
5
- transfer: "Palveluntarjoajat voivat siirt\xE4\xE4 tietoja Yhdysvaltoihin. Voit muuttaa valintaasi milloin tahansa.",
5
+ change: "Voit muuttaa valintaasi milloin tahansa.",
6
6
  more: "Lue lis\xE4\xE4",
7
7
  accept: "Hyv\xE4ksy kaikki",
8
8
  reject: "Hylk\xE4\xE4 kaikki",
@@ -22,6 +22,7 @@ var fi = {
22
22
  consentPurpose: "Tallentaa ev\xE4stevalintasi.",
23
23
  privacyPolicy: "Tietosuojaseloste",
24
24
  dpf: "Tietoja voidaan siirt\xE4\xE4 Yhdysvaltoihin EU:n ja Yhdysvaltojen v\xE4lisen tietosuojakehyksen (DPF) perusteella.",
25
+ us: "Tietoja siirret\xE4\xE4n Yhdysvaltoihin.",
25
26
  name: "Nimi",
26
27
  duration: "Kesto",
27
28
  purpose: "Tarkoitus",
@@ -39,7 +40,7 @@ var fi = {
39
40
  var sv = {
40
41
  title: "Kakor",
41
42
  intro: "Ut\xF6ver n\xF6dv\xE4ndiga kakor anv\xE4nder vi f\xF6ljande tj\xE4nster endast om du ger ditt samtycke:",
42
- transfer: "Tj\xE4nsteleverant\xF6rerna kan \xF6verf\xF6ra uppgifter till USA. Du kan \xE4ndra ditt val n\xE4r som helst.",
43
+ change: "Du kan \xE4ndra ditt val n\xE4r som helst.",
43
44
  more: "L\xE4s mer",
44
45
  accept: "Godk\xE4nn alla",
45
46
  reject: "Avvisa alla",
@@ -59,6 +60,7 @@ var sv = {
59
60
  consentPurpose: "Sparar ditt val av kakor.",
60
61
  privacyPolicy: "Dataskyddsbeskrivning",
61
62
  dpf: "Uppgifter kan \xF6verf\xF6ras till USA med st\xF6d av ramverket f\xF6r dataskydd mellan EU och USA (DPF).",
63
+ us: "Uppgifter \xF6verf\xF6rs till USA.",
62
64
  name: "Namn",
63
65
  duration: "Varaktighet",
64
66
  purpose: "Syfte",
@@ -76,7 +78,7 @@ var sv = {
76
78
  var en = {
77
79
  title: "Cookies",
78
80
  intro: "Besides necessary cookies, we use the following services only if you allow it:",
79
- transfer: "The providers may transfer data to the United States. You can change your choice at any time.",
81
+ change: "You can change your choice at any time.",
80
82
  more: "Read more",
81
83
  accept: "Accept all",
82
84
  reject: "Reject all",
@@ -96,6 +98,7 @@ var en = {
96
98
  consentPurpose: "Stores your cookie choice.",
97
99
  privacyPolicy: "Privacy policy",
98
100
  dpf: "Data may be transferred to the United States under the EU\u2013US Data Privacy Framework (DPF).",
101
+ us: "Data is transferred to the United States.",
99
102
  name: "Name",
100
103
  duration: "Duration",
101
104
  purpose: "Purpose",
@@ -172,6 +175,29 @@ var providers = [
172
175
  ],
173
176
  hosts: ["facebook.net", "facebook.com"]
174
177
  },
178
+ {
179
+ id: "posthog",
180
+ name: "PostHog",
181
+ company: "PostHog Inc.",
182
+ privacyPolicy: "https://posthog.com/privacy",
183
+ category: "analytics",
184
+ purpose: {
185
+ fi: "Tuoteanalytiikka: sivujen katselut, klikkaukset ja k\xE4ytt\xF6tavat. Voi tallentaa istuntoja, jos istuntotallenteet ovat k\xE4yt\xF6ss\xE4.",
186
+ sv: "Produktanalys: sidvisningar, klick och anv\xE4ndningsm\xF6nster. Kan spela in sessioner om sessionsinspelning \xE4r aktiverad.",
187
+ en: "Product analytics: page views, clicks and usage patterns. Can record sessions if session replay is enabled."
188
+ },
189
+ cookies: [
190
+ { name: "ph_*_posthog", duration: "1y" },
191
+ { name: "ph_*_posthog_cpm", duration: "1y" },
192
+ { name: "ph_*_posthog", duration: "persistent", storage: "localStorage" },
193
+ { name: "ph_*_posthog__flags", duration: "persistent", storage: "localStorage" },
194
+ { name: "ph_*_posthog", duration: "session", storage: "sessionStorage" },
195
+ { name: "ph_*_primary_window_exists", duration: "session", storage: "sessionStorage" },
196
+ { name: "ph_*_session_registered_properties", duration: "session", storage: "sessionStorage" },
197
+ { name: "ph_*_window_id", duration: "session", storage: "sessionStorage" }
198
+ ],
199
+ hosts: ["posthog.com"]
200
+ },
175
201
  {
176
202
  id: "youtube",
177
203
  name: "YouTube",
@@ -197,8 +223,11 @@ var providers = [
197
223
 
198
224
  // src/config.ts
199
225
  var list = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
226
+ var posthogHost = (config) => (config.posthog?.host ?? "https://eu.i.posthog.com").replace(/\/$/, "");
200
227
  function activeProviders(config) {
201
- return providers.filter((p) => p.id === "youtube" ? config.youtube : list(config[p.id]).length);
228
+ return providers.filter(
229
+ (p) => p.id === "youtube" ? config.youtube : p.id === "posthog" ? config.posthog?.key : list(config[p.id]).length
230
+ ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p);
202
231
  }
203
232
  function textsFor(config, locale) {
204
233
  return { ...texts[locale], ...config.texts?.[locale] };
@@ -220,6 +249,7 @@ function runtimeData(config, locale) {
220
249
  ga: list(config.ga),
221
250
  ads: list(config.googleAds),
222
251
  meta: list(config.meta),
252
+ ph: config.posthog?.key ? { key: config.posthog.key, host: posthogHost(config) } : void 0,
223
253
  yt: !!config.youtube,
224
254
  del: { analytics: del("analytics"), marketing: del("marketing") },
225
255
  t: { ytText: t.ytText, ytButton: t.ytButton, ytOpen: t.ytOpen }
package/dist/render.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface RenderOptions {
5
5
  /** For a CSP with nonces; applied to the inline `<style>` and `<script>`. */
6
6
  nonce?: string;
7
7
  }
8
- /** Banner, settings dialog and runtime. Place it right after `<body>`. Empty when no provider needs consent. */
8
+ /** Banner with its settings view, and the runtime. Place it right after `<body>`. Empty when no provider needs consent. */
9
9
  export declare function renderKeksi(config: KeksiConfig, { lang, nonce }?: RenderOptions): string;
10
10
  export interface CookieTableOptions {
11
11
  lang?: Locale;
package/dist/runtime.d.ts CHANGED
@@ -21,6 +21,9 @@ declare global {
21
21
  gtag?: (...a: unknown[]) => void;
22
22
  fbq?: Queue;
23
23
  _fbq?: Queue;
24
+ posthog?: unknown[] & Record<string, unknown> & {
25
+ _i: unknown[];
26
+ };
24
27
  }
25
28
  }
26
29
  export declare function boot(d: RuntimeData, w?: Window & typeof globalThis): KeksiApi;
package/dist/scan.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/glob.ts
2
+ var glob = (pattern, name) => new RegExp(`^${pattern.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(name);
3
+
1
4
  // src/providers.ts
2
5
  var GOOGLE = { company: "Google Ireland Limited", privacyPolicy: "https://policies.google.com/privacy", transfer: "dpf" };
3
6
  var META = { company: "Meta Platforms Ireland Limited", privacyPolicy: "https://www.facebook.com/privacy/policy/", transfer: "dpf" };
@@ -58,6 +61,29 @@ var providers = [
58
61
  ],
59
62
  hosts: ["facebook.net", "facebook.com"]
60
63
  },
64
+ {
65
+ id: "posthog",
66
+ name: "PostHog",
67
+ company: "PostHog Inc.",
68
+ privacyPolicy: "https://posthog.com/privacy",
69
+ category: "analytics",
70
+ purpose: {
71
+ fi: "Tuoteanalytiikka: sivujen katselut, klikkaukset ja k\xE4ytt\xF6tavat. Voi tallentaa istuntoja, jos istuntotallenteet ovat k\xE4yt\xF6ss\xE4.",
72
+ sv: "Produktanalys: sidvisningar, klick och anv\xE4ndningsm\xF6nster. Kan spela in sessioner om sessionsinspelning \xE4r aktiverad.",
73
+ en: "Product analytics: page views, clicks and usage patterns. Can record sessions if session replay is enabled."
74
+ },
75
+ cookies: [
76
+ { name: "ph_*_posthog", duration: "1y" },
77
+ { name: "ph_*_posthog_cpm", duration: "1y" },
78
+ { name: "ph_*_posthog", duration: "persistent", storage: "localStorage" },
79
+ { name: "ph_*_posthog__flags", duration: "persistent", storage: "localStorage" },
80
+ { name: "ph_*_posthog", duration: "session", storage: "sessionStorage" },
81
+ { name: "ph_*_primary_window_exists", duration: "session", storage: "sessionStorage" },
82
+ { name: "ph_*_session_registered_properties", duration: "session", storage: "sessionStorage" },
83
+ { name: "ph_*_window_id", duration: "session", storage: "sessionStorage" }
84
+ ],
85
+ hosts: ["posthog.com"]
86
+ },
61
87
  {
62
88
  id: "youtube",
63
89
  name: "YouTube",
@@ -113,7 +139,7 @@ var purposes = {
113
139
  };
114
140
  var trackerNames = providers.flatMap((p) => p.cookies.flatMap((c) => c.name.split(", ")));
115
141
  var trackerHosts = providers.flatMap((p) => p.hosts);
116
- var matches = (patterns, name) => patterns.some((p) => p.endsWith("*") ? name.startsWith(p.slice(0, -1)) : name === p);
142
+ var matches = (patterns, name) => patterns.some((p) => glob(p, name));
117
143
  function purposeOf(name) {
118
144
  const k = Object.keys(purposes).find((p) => matches([p], name));
119
145
  return k ? purposes[k] : void 0;
package/dist/sveltekit.js CHANGED
@@ -2,7 +2,7 @@
2
2
  var fi = {
3
3
  title: "Ev\xE4steet",
4
4
  intro: "K\xE4yt\xE4mme v\xE4ltt\xE4m\xE4tt\xF6mien ev\xE4steiden lis\xE4ksi seuraavia palveluita vain, jos annat siihen luvan:",
5
- transfer: "Palveluntarjoajat voivat siirt\xE4\xE4 tietoja Yhdysvaltoihin. Voit muuttaa valintaasi milloin tahansa.",
5
+ change: "Voit muuttaa valintaasi milloin tahansa.",
6
6
  more: "Lue lis\xE4\xE4",
7
7
  accept: "Hyv\xE4ksy kaikki",
8
8
  reject: "Hylk\xE4\xE4 kaikki",
@@ -22,6 +22,7 @@ var fi = {
22
22
  consentPurpose: "Tallentaa ev\xE4stevalintasi.",
23
23
  privacyPolicy: "Tietosuojaseloste",
24
24
  dpf: "Tietoja voidaan siirt\xE4\xE4 Yhdysvaltoihin EU:n ja Yhdysvaltojen v\xE4lisen tietosuojakehyksen (DPF) perusteella.",
25
+ us: "Tietoja siirret\xE4\xE4n Yhdysvaltoihin.",
25
26
  name: "Nimi",
26
27
  duration: "Kesto",
27
28
  purpose: "Tarkoitus",
@@ -39,7 +40,7 @@ var fi = {
39
40
  var sv = {
40
41
  title: "Kakor",
41
42
  intro: "Ut\xF6ver n\xF6dv\xE4ndiga kakor anv\xE4nder vi f\xF6ljande tj\xE4nster endast om du ger ditt samtycke:",
42
- transfer: "Tj\xE4nsteleverant\xF6rerna kan \xF6verf\xF6ra uppgifter till USA. Du kan \xE4ndra ditt val n\xE4r som helst.",
43
+ change: "Du kan \xE4ndra ditt val n\xE4r som helst.",
43
44
  more: "L\xE4s mer",
44
45
  accept: "Godk\xE4nn alla",
45
46
  reject: "Avvisa alla",
@@ -59,6 +60,7 @@ var sv = {
59
60
  consentPurpose: "Sparar ditt val av kakor.",
60
61
  privacyPolicy: "Dataskyddsbeskrivning",
61
62
  dpf: "Uppgifter kan \xF6verf\xF6ras till USA med st\xF6d av ramverket f\xF6r dataskydd mellan EU och USA (DPF).",
63
+ us: "Uppgifter \xF6verf\xF6rs till USA.",
62
64
  name: "Namn",
63
65
  duration: "Varaktighet",
64
66
  purpose: "Syfte",
@@ -76,7 +78,7 @@ var sv = {
76
78
  var en = {
77
79
  title: "Cookies",
78
80
  intro: "Besides necessary cookies, we use the following services only if you allow it:",
79
- transfer: "The providers may transfer data to the United States. You can change your choice at any time.",
81
+ change: "You can change your choice at any time.",
80
82
  more: "Read more",
81
83
  accept: "Accept all",
82
84
  reject: "Reject all",
@@ -96,6 +98,7 @@ var en = {
96
98
  consentPurpose: "Stores your cookie choice.",
97
99
  privacyPolicy: "Privacy policy",
98
100
  dpf: "Data may be transferred to the United States under the EU\u2013US Data Privacy Framework (DPF).",
101
+ us: "Data is transferred to the United States.",
99
102
  name: "Name",
100
103
  duration: "Duration",
101
104
  purpose: "Purpose",
@@ -172,6 +175,29 @@ var providers = [
172
175
  ],
173
176
  hosts: ["facebook.net", "facebook.com"]
174
177
  },
178
+ {
179
+ id: "posthog",
180
+ name: "PostHog",
181
+ company: "PostHog Inc.",
182
+ privacyPolicy: "https://posthog.com/privacy",
183
+ category: "analytics",
184
+ purpose: {
185
+ fi: "Tuoteanalytiikka: sivujen katselut, klikkaukset ja k\xE4ytt\xF6tavat. Voi tallentaa istuntoja, jos istuntotallenteet ovat k\xE4yt\xF6ss\xE4.",
186
+ sv: "Produktanalys: sidvisningar, klick och anv\xE4ndningsm\xF6nster. Kan spela in sessioner om sessionsinspelning \xE4r aktiverad.",
187
+ en: "Product analytics: page views, clicks and usage patterns. Can record sessions if session replay is enabled."
188
+ },
189
+ cookies: [
190
+ { name: "ph_*_posthog", duration: "1y" },
191
+ { name: "ph_*_posthog_cpm", duration: "1y" },
192
+ { name: "ph_*_posthog", duration: "persistent", storage: "localStorage" },
193
+ { name: "ph_*_posthog__flags", duration: "persistent", storage: "localStorage" },
194
+ { name: "ph_*_posthog", duration: "session", storage: "sessionStorage" },
195
+ { name: "ph_*_primary_window_exists", duration: "session", storage: "sessionStorage" },
196
+ { name: "ph_*_session_registered_properties", duration: "session", storage: "sessionStorage" },
197
+ { name: "ph_*_window_id", duration: "session", storage: "sessionStorage" }
198
+ ],
199
+ hosts: ["posthog.com"]
200
+ },
175
201
  {
176
202
  id: "youtube",
177
203
  name: "YouTube",
@@ -197,8 +223,11 @@ var providers = [
197
223
 
198
224
  // src/config.ts
199
225
  var list = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
226
+ var posthogHost = (config) => (config.posthog?.host ?? "https://eu.i.posthog.com").replace(/\/$/, "");
200
227
  function activeProviders(config) {
201
- return providers.filter((p) => p.id === "youtube" ? config.youtube : list(config[p.id]).length);
228
+ return providers.filter(
229
+ (p) => p.id === "youtube" ? config.youtube : p.id === "posthog" ? config.posthog?.key : list(config[p.id]).length
230
+ ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p);
202
231
  }
203
232
  function categoriesOf(config) {
204
233
  return ["analytics", "marketing"].filter((c) => activeProviders(config).some((p) => p.category === c));
@@ -223,6 +252,7 @@ function runtimeData(config, locale) {
223
252
  ga: list(config.ga),
224
253
  ads: list(config.googleAds),
225
254
  meta: list(config.meta),
255
+ ph: config.posthog?.key ? { key: config.posthog.key, host: posthogHost(config) } : void 0,
226
256
  yt: !!config.youtube,
227
257
  del: { analytics: del("analytics"), marketing: del("marketing") },
228
258
  t: { ytText: t.ytText, ytButton: t.ytButton, ytOpen: t.ytOpen }
@@ -231,34 +261,46 @@ function runtimeData(config, locale) {
231
261
 
232
262
  // src/styles.ts
233
263
  var css = [
234
- ":where(.keksi,.keksi-dialog){--_bg:var(--keksi-bg,#fff);--_fg:var(--keksi-fg,#1b1b1b);--_line:var(--keksi-line,#c8c8c8);--_r:var(--keksi-radius,6px);box-sizing:border-box;background:var(--_bg);color:var(--_fg);font:inherit;font-size:15px;line-height:1.5}",
235
- ":where(.keksi-policy){--_line:var(--keksi-line,#c8c8c8)}",
236
- ":where(.keksi){position:fixed;z-index:2147483000;inset:auto 1rem 1rem;max-width:34rem;max-height:calc(100vh - 2rem);overflow:auto;margin-inline:auto;padding:1.25rem;border:1px solid var(--_line);border-radius:var(--_r);box-shadow:0 4px 24px #0003}",
237
- ":where(.keksi,.keksi-dialog) :where(h2){font-size:1.125rem;margin:0 0 .5rem}",
238
- ":where(.keksi) :where(p,ul){margin:0 0 .75rem}",
239
- ":where(.keksi) :where(ul){padding-left:1.25rem}",
240
- ":where(.keksi,.keksi-dialog) :where(a){color:inherit}",
241
- ":where(.keksi-actions){display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1rem}",
242
- ":where(.keksi-btn){flex:1 1 auto;font:inherit;font-weight:600;padding:.6rem 1rem;border:1px solid var(--_fg);border-radius:var(--_r);background:var(--_bg);color:var(--_fg);cursor:pointer}",
264
+ ":where(.keksi,.keksi-policy){--_bg:var(--keksi-bg,#fff);--_fg:var(--keksi-fg,#1b1b1b);--_line:var(--keksi-line,#e3e3e3);--_soft:color-mix(in srgb,var(--_fg) 4%,var(--_bg));--_r:var(--keksi-radius,8px)}",
265
+ ":where(.keksi){box-sizing:border-box;background:var(--_bg);color:var(--_fg);font:inherit;font-size:14px;line-height:1.5;border:1px solid var(--_line);border-radius:var(--_r);box-shadow:0 8px 32px #0000002e}",
266
+ ":where(.keksi){position:fixed;z-index:2147483000;inset:auto 1rem 1rem;display:flex;flex-direction:column;max-width:34rem;max-height:calc(100vh - 2rem);margin-inline:auto;overflow:hidden}",
267
+ ":where(.keksi,.keksi-main,.keksi-settings)[hidden]{display:none}",
268
+ ":where(.keksi-main,.keksi-settings){display:flex;flex-direction:column;min-height:0;margin:0}",
269
+ ":where(.keksi:has(.keksi-settings:not([hidden]))){max-width:40rem}",
270
+ ":where(.keksi-settings .keksi-body){padding-block:0}",
271
+ ":where(.keksi-head){display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.875rem 1.25rem;border-bottom:1px solid var(--_line)}",
272
+ ":where(.keksi-head) :where(h2){font-size:1rem;font-weight:600;line-height:1.3;margin:0}",
273
+ ":where(.keksi-body){padding:1rem 1.25rem;overflow:auto}",
274
+ ":where(.keksi) :where(.keksi-body>*){margin:0 0 .75rem}",
275
+ ":where(.keksi) :where(.keksi-body>:last-child){margin-bottom:0}",
276
+ ":where(.keksi) :where(ul){list-style:none;padding:0}",
277
+ ":where(.keksi) :where(strong){font-weight:600}",
278
+ ":where(.keksi) :where(a){color:inherit}",
279
+ ".keksi a{text-decoration:underline}",
280
+ ":where(.keksi-actions){display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem 1.25rem;background:var(--_soft)}",
281
+ ":where(.keksi-btn){min-width:7.5rem;font:inherit;font-weight:600;padding:.5rem 1rem;border:1px solid var(--_fg);border-radius:calc(var(--_r) - 2px);background:var(--_bg);color:var(--_fg);cursor:pointer}",
243
282
  ":where(.keksi-btn:hover){background:var(--_fg);color:var(--_bg)}",
244
- ":where(.keksi,.keksi-dialog,keksi-youtube) :where(button,a,input,summary):focus-visible{outline:3px solid var(--keksi-focus,#2563eb);outline-offset:2px}",
245
- ":where(.keksi-dialog){width:min(40rem,calc(100% - 2rem));max-height:calc(100% - 2rem);padding:0;border:0;border-radius:var(--_r)}",
246
- ":where(.keksi-dialog)::backdrop{background:#0009}",
247
- ":where(.keksi-dialog form){padding:1.25rem}",
248
- ":where(.keksi-head,.keksi-cat-head){display:flex;align-items:center;justify-content:space-between;gap:1rem}",
249
- ":where(.keksi-close){font:inherit;font-size:1.5rem;line-height:1;padding:.25rem .5rem;border:0;background:none;color:inherit;cursor:pointer}",
250
- ":where(.keksi-cat){padding:.75rem 0;border-top:1px solid var(--_line)}",
251
- ":where(.keksi-cat-head) :where(h2,h3,h4,h5){font-size:1rem;margin:0}",
283
+ ":where(.keksi-text){margin-right:auto;padding:.5rem .25rem;font:inherit;border:0;background:none;color:inherit;text-decoration:underline;text-underline-offset:2px;cursor:pointer}",
284
+ "@media (max-width:30rem){:where(.keksi-btn){flex:1 1 auto}}",
285
+ ":where(.keksi,keksi-youtube) :where(button,a,input,summary):focus-visible{outline:3px solid var(--keksi-focus,#2563eb);outline-offset:2px}",
286
+ ":where(.keksi-close){font:inherit;font-size:1.375rem;line-height:1;margin:-.25rem -.5rem;padding:.25rem .5rem;border:0;background:none;color:inherit;cursor:pointer}",
287
+ ":where(.keksi-cat){padding:1rem 0;border-top:1px solid var(--_line)}",
288
+ ":where(.keksi-body>.keksi-cat:first-child){border-top:0}",
289
+ ":where(.keksi-cat-head){display:flex;align-items:center;justify-content:space-between;gap:1rem}",
290
+ ":where(.keksi-cat-head) :where(h2,h3,h4,h5){font-size:1rem;font-weight:600;margin:0}",
291
+ ":where(.keksi-cat-head) :where(span){font-size:.8125rem;opacity:.7}",
252
292
  ":where(.keksi-cat) :where(p){margin:.25rem 0}",
253
- ":where(.keksi-cat input[role=switch]){appearance:none;position:relative;flex:none;width:2.75rem;height:1.5rem;margin:0;border-radius:1rem;background:var(--_line);cursor:pointer}",
254
- ':where(.keksi-cat input[role=switch])::before{content:"";position:absolute;top:.2rem;left:.2rem;width:1.1rem;height:1.1rem;border-radius:50%;background:#fff;transition:transform .15s}',
293
+ ":where(.keksi-cat input[role=switch]){appearance:none;position:relative;flex:none;width:2.5rem;height:1.375rem;margin:0;border-radius:1rem;background:color-mix(in srgb,var(--_fg) 22%,var(--_bg));cursor:pointer}",
294
+ ':where(.keksi-cat input[role=switch])::before{content:"";position:absolute;top:.1875rem;left:.1875rem;width:1rem;height:1rem;border-radius:50%;background:#fff;transition:transform .15s}',
255
295
  ":where(.keksi-cat input[role=switch]:checked){background:var(--_fg)}",
256
- ":where(.keksi-cat input[role=switch]:checked)::before{transform:translateX(1.25rem)}",
296
+ ":where(.keksi-cat input[role=switch]:checked)::before{transform:translateX(1.125rem)}",
257
297
  ":where(.keksi-cat summary){margin-top:.5rem;font-weight:600;cursor:pointer}",
258
- ":where(.keksi-provider){margin:.75rem 0}",
259
- ":where(.keksi-provider) :where(h3,h4,h5,h6){font-size:.9375rem;margin:0}",
260
- ":where(.keksi-cat table){width:100%;border-collapse:collapse;font-size:.875rem;margin-top:.25rem}",
261
- ":where(.keksi-cat) :where(th,td){padding:.25rem .75rem .25rem 0;border-bottom:1px solid var(--_line);text-align:left;vertical-align:top}",
298
+ ":where(.keksi-provider){margin:.75rem 0;padding:.75rem .875rem;border-radius:calc(var(--_r) - 2px);background:var(--_soft)}",
299
+ ":where(.keksi-policy .keksi-provider){padding:0;background:none}",
300
+ ":where(.keksi-provider) :where(h3,h4,h5,h6){font-size:.9375rem;font-weight:600;margin:0}",
301
+ ":where(.keksi-cat table){width:100%;border-collapse:collapse;font-size:.8125rem;margin-top:.5rem}",
302
+ ":where(.keksi-cat) :where(th,td){padding:.3rem .75rem .3rem 0;border-bottom:1px solid var(--_line);text-align:left;vertical-align:top}",
303
+ ":where(.keksi-cat) :where(th){font-weight:600}",
262
304
  ":where(keksi-youtube){display:block;aspect-ratio:16/9;background:#111;color:#fff}",
263
305
  ":where(keksi-youtube iframe){display:block;width:100%;height:100%;border:0}",
264
306
  ":where(.keksi-yt){display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.75rem;height:100%;padding:1rem;box-sizing:border-box;text-align:center}",
@@ -279,7 +321,7 @@ function renderKeksi(config, { lang = "fi", nonce } = {}) {
279
321
  const btn = (action, label, type = "button") => `<button type="${type}" class="keksi-btn" data-keksi="${action}">${e(label)}</button>`;
280
322
  const uses = cats.map((c) => `<li><strong>${e(t[c])}:</strong> ${e(active.filter((p) => p.category === c).map((p) => p.name).join(", "))}</li>`).join("");
281
323
  const json = JSON.stringify(runtimeData(config, lang)).replace(/</g, "\\u003c");
282
- return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><h2 id="keksi-title">${e(t.title)}</h2><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.transfer)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p><div class="keksi-actions">${btn("accept", t.accept)}${btn("reject", t.reject)}<button type="button" class="keksi-btn" data-keksi-open>${e(t.settings)}</button></div></section><dialog id="keksi-dialog" class="keksi-dialog" aria-labelledby="keksi-dialog-title"><form method="dialog"><div class="keksi-head"><h2 id="keksi-dialog-title">${e(t.settingsTitle)}</h2><button class="keksi-close" aria-label="${e(t.close)}">\xD7</button></div>` + sections(config, lang, 3, true) + `<div class="keksi-actions">${btn("save", t.save, "submit")}${btn("accept", t.accept, "submit")}${btn("reject", t.reject, "submit")}</div></form></dialog><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var v=["analytics","marketing"],C={analytics:!1,marketing:!1};function g(...a){return arguments}function q(a,o=window){if(typeof o.keksi?.sync=="function")return o.keksi.sync(),o.keksi;let r=o.document,h=e=>r.getElementById(e),l=w(),A=!1,E=!1,b=new Set,$=g("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(a.ga.length||a.ads.length){let e=o.dataLayer??(o.dataLayer=[]);o.gtag=function(){e.push(arguments)},e.push($)}if(a.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),o.fbq=o._fbq=e}function w(){let e=r.cookie.split("; ").find(s=>s.startsWith(`${a.name}=`))?.slice(a.name.length+1),[n,,t,i]=e?.split(".")??[];return n!==a.v||!t||!i?null:{id:t,c:{analytics:i.includes("a"),marketing:i.includes("m")}}}function T(e){let n=l?.id??Array.from(o.crypto.getRandomValues(new Uint8Array(8)),i=>i.toString(16).padStart(2,"0")).join(""),t=(e.analytics?"a":"")+(e.marketing?"m":"")||"-";return r.cookie=`${a.name}=${a.v}.${Date.now().toString(36)}.${n}.${t}; Path=/; Max-Age=${a.days*86400}; SameSite=Lax`+(a.domain?`; Domain=${a.domain}`:"")+(o.location.protocol==="https:"?"; Secure":""),{id:n,c:e}}function S(e){let n=r.createElement("script");n.async=!0,n.src=e,r.head.append(n)}function _(e){let n=[...e.analytics?a.ga:[],...e.marketing?a.ads:[]].filter(t=>!b.has(t));if(n.length){let t=e.marketing?"granted":"denied",i=[g("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];b.size||i.push(g("js",new Date));for(let s of n)i.push(g("config",s)),b.add(s);if(A)for(let s of i)o.dataLayer.push(s);else{let s=o.dataLayer;s.splice(s.indexOf($)+1,0,...i),S(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(n[0])}`),A=!0}}if(e.marketing&&a.meta.length&&!E&&(E=!0,o.fbq.queue.unshift(...a.meta.map(t=>g("init",t)),g("track","PageView")),S("https://connect.facebook.net/en_US/fbevents.js")),e.marketing)for(let t of r.querySelectorAll("keksi-youtube"))t.show(!0)}function M(e){let n=e.flatMap(c=>a.del[c]),t=c=>n.some(u=>u.endsWith("*")?c.startsWith(u.slice(0,-1)):c===u),i=o.location.hostname.split("."),s=["",...i.map((c,u)=>`; Domain=${i.slice(u).join(".")}`)];for(let c of r.cookie.split("; ")){let u=c.split("=")[0];if(t(u))for(let y of s)r.cookie=`${u}=; Max-Age=0; Path=/${y}`}try{for(let c of Object.keys(o.localStorage))t(c)&&o.localStorage.removeItem(c)}catch{}}function f(e){let n=l?.c;l=T(e),k(),r.dispatchEvent(new o.CustomEvent("keksi:change",{detail:{...e}})),M(v.filter(t=>!e[t])),v.some(t=>n?.[t]&&!e[t])?o.location.reload():_(e)}function k(){let e=h("keksi-banner");e&&(e.hidden=!!l)}function x(){let e=h("keksi-dialog");if(e){for(let n of v){let t=e.querySelector(`[name=${n}]`);t&&(t.checked=!!l?.c[n])}e.showModal()}}r.addEventListener("click",e=>{let n=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!n)return;if(n.hasAttribute("data-keksi-open"))return e.preventDefault(),x();let t=n.getAttribute("data-keksi");if(t==="accept")f({analytics:!0,marketing:!0});else if(t==="reject")f(C);else if(t==="save"){let i=s=>!!h("keksi-dialog")?.querySelector(`[name=${s}]`)?.checked;f({analytics:i("analytics"),marketing:i("marketing")})}}),r.addEventListener("astro:after-swap",k);class j extends o.HTMLElement{connectedCallback(){this.show(!!l?.c.marketing)}show(n,t=!1){let i=encodeURIComponent(this.getAttribute("video")??""),s=this.getAttribute("label");if(n){if(this.querySelector("iframe"))return;let d=r.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${i}${t?"?autoplay=1":""}`,d.title=s??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let c=r.createElement("div");c.className="keksi-yt";let u=this.getAttribute("poster");u&&(c.style.background=`linear-gradient(#000a,#000a),center/cover url(${JSON.stringify(u)})`);let y=r.createElement("p");y.textContent=a.t.ytText;let m=r.createElement("button");m.type="button",m.textContent=a.t.ytButton,s&&m.setAttribute("aria-label",`${a.t.ytButton}: ${s}`),m.onclick=()=>this.show(!0,!0);let p=r.createElement("a");p.href=`https://www.youtube.com/watch?v=${i}`,p.target="_blank",p.rel="noopener",p.textContent=a.t.ytOpen,c.append(y,m,p),this.replaceChildren(c)}}a.yt&&!o.customElements.get("keksi-youtube")&&o.customElements.define("keksi-youtube",j);let L={get consent(){return l?{...l.c}:null},open:x,save:e=>f({...l?.c??C,...e}),acceptAll:()=>f({analytics:!0,marketing:!0}),rejectAll:()=>f(C),sync:k};return o.keksi=L,k(),l&&_(l.c),L}q(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
324
+ return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><div class="keksi-main"><div class="keksi-head"><h2 id="keksi-title">${e(t.title)}</h2></div><div class="keksi-body"><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.change)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p></div><div class="keksi-actions"><button type="button" class="keksi-text" data-keksi-open>${e(t.settings)}</button>${btn("reject", t.reject)}${btn("accept", t.accept)}</div></div><form id="keksi-settings" class="keksi-settings" aria-labelledby="keksi-settings-title" hidden><div class="keksi-head"><h2 id="keksi-settings-title">${e(t.settingsTitle)}</h2><button type="button" class="keksi-close" data-keksi="close" aria-label="${e(t.close)}">\xD7</button></div><div class="keksi-body">${sections(config, lang, 3, true)}</div><div class="keksi-actions">${btn("save", t.save)}${btn("reject", t.reject)}${btn("accept", t.accept)}</div></form></section><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var M=(o,s)=>new RegExp(`^${o.split("*").map(i=>i.replace(/[.+?^${}()|[\\]\\\\]/g,"\\\\$&")).join(".*")}$`).test(s);var $=["analytics","marketing"],C={analytics:!1,marketing:!1};function p(...o){return arguments}function j(o,s=window){if(typeof s.keksi?.sync=="function")return s.keksi.sync(),s.keksi;let i=s.document,f=e=>i.getElementById(e),u=q(),_=!1,A=!1,S=!1,v=new Set,L=p("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(o.ga.length||o.ads.length){let e=s.dataLayer??(s.dataLayer=[]);s.gtag=function(){e.push(arguments)},e.push(L)}if(o.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),s.fbq=s._fbq=e}if(o.ph){let e=Object.assign([],{_i:[],people:[]});for(let n of"capture identify alias reset register register_once unregister group setPersonProperties getFeatureFlag isFeatureEnabled onFeatureFlags reloadFeatureFlags get_distinct_id startSessionRecording stopSessionRecording captureException on".split(" "))e[n]=(...t)=>e.push([n,...t]);s.posthog=e}function q(){let e=i.cookie.split("; ").find(r=>r.startsWith(`${o.name}=`))?.slice(o.name.length+1),[n,,t,a]=e?.split(".")??[];return n!==o.v||!t||!a?null:{id:t,c:{analytics:a.includes("a"),marketing:a.includes("m")}}}function F(e){let n=u?.id??Array.from(s.crypto.getRandomValues(new Uint8Array(8)),a=>a.toString(16).padStart(2,"0")).join(""),t=(e.analytics?"a":"")+(e.marketing?"m":"")||"-";return i.cookie=`${o.name}=${o.v}.${Date.now().toString(36)}.${n}.${t}; Path=/; Max-Age=${o.days*86400}; SameSite=Lax`+(o.domain?`; Domain=${o.domain}`:"")+(s.location.protocol==="https:"?"; Secure":""),{id:n,c:e}}function E(e){let n=i.createElement("script");return n.async=!0,n.src=e,i.head.append(n),n}function w(e){let n=[...e.analytics?o.ga:[],...e.marketing?o.ads:[]].filter(t=>!v.has(t));if(n.length){let t=e.marketing?"granted":"denied",a=[p("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];v.size||a.push(p("js",new Date));for(let r of n)a.push(p("config",r)),v.add(r);if(_)for(let r of a)s.dataLayer.push(r);else{let r=s.dataLayer;r.splice(r.indexOf(L)+1,0,...a),E(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(n[0])}`),_=!0}}if(e.marketing&&o.meta.length&&!A&&(A=!0,s.fbq.queue.unshift(...o.meta.map(t=>p("init",t)),p("track","PageView")),E("https://connect.facebook.net/en_US/fbevents.js")),e.analytics&&o.ph&&!S&&(S=!0,s.posthog._i.push([o.ph.key,{api_host:o.ph.host,defaults:"2026-08-30"},"posthog"]),E(`${o.ph.host.replace(".i.posthog.com","-assets.i.posthog.com")}/static/array.js`).crossOrigin="anonymous"),e.marketing)for(let t of i.querySelectorAll("keksi-youtube"))t.show(!0)}function H(e){let n=e.flatMap(c=>o.del[c]),t=c=>n.some(l=>M(l,c)),a=s.location.hostname.split("."),r=["",...a.map((c,l)=>`; Domain=${a.slice(l).join(".")}`)];for(let c of i.cookie.split("; ")){let l=c.split("=")[0];if(t(l))for(let b of r)i.cookie=`${l}=; Max-Age=0; Path=/${b}`}try{for(let c of[s.localStorage,s.sessionStorage])for(let l of Object.keys(c))t(l)&&c.removeItem(l)}catch{}}function g(e){let n=u?.c;u=F(e),y(),i.dispatchEvent(new s.CustomEvent("keksi:change",{detail:{...e}})),H($.filter(t=>!e[t])),$.some(t=>n?.[t]&&!e[t])?s.location.reload():w(e)}function h(e=!1,n=!u||e){let t=f("keksi-banner"),a=f("keksi-settings");!t||!a||(t.hidden=!n,a.hidden=!e,t.firstElementChild.hidden=e)}function y(){h()}function x(){let e=f("keksi-settings");for(let n of $){let t=e?.querySelector(`[name=${n}]`);t&&(t.checked=!!u?.c[n])}h(!0),e?.querySelector(".keksi-close")?.focus()}i.addEventListener("click",e=>{let n=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!n)return;if(n.hasAttribute("data-keksi-open"))return e.preventDefault(),x();let t=n.getAttribute("data-keksi");if(t==="close")h();else if(t==="accept")g({analytics:!0,marketing:!0});else if(t==="reject")g(C);else if(t==="save"){let a=r=>!!f("keksi-settings")?.querySelector(`[name=${r}]`)?.checked;g({analytics:a("analytics"),marketing:a("marketing")})}}),i.addEventListener("keydown",e=>{e.key==="Escape"&&f("keksi-settings")?.hidden===!1&&h()}),i.addEventListener("astro:after-swap",y);class I extends s.HTMLElement{connectedCallback(){this.show(!!u?.c.marketing)}show(n,t=!1){let a=encodeURIComponent(this.getAttribute("video")??""),r=this.getAttribute("label");if(n){if(this.querySelector("iframe"))return;let d=i.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${a}${t?"?autoplay=1":""}`,d.title=r??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let c=i.createElement("div");c.className="keksi-yt";let l=this.getAttribute("poster");l&&(c.style.background=`linear-gradient(#000a,#000a),center/cover url(${JSON.stringify(l)})`);let b=i.createElement("p");b.textContent=o.t.ytText;let m=i.createElement("button");m.type="button",m.textContent=o.t.ytButton,r&&m.setAttribute("aria-label",`${o.t.ytButton}: ${r}`),m.onclick=()=>this.show(!0,!0);let k=i.createElement("a");k.href=`https://www.youtube.com/watch?v=${a}`,k.target="_blank",k.rel="noopener",k.textContent=o.t.ytOpen,c.append(b,m,k),this.replaceChildren(c)}}o.yt&&!s.customElements.get("keksi-youtube")&&s.customElements.define("keksi-youtube",I);let T={get consent(){return u?{...u.c}:null},open:x,save:e=>g({...u?.c??C,...e}),acceptAll:()=>g({analytics:!0,marketing:!0}),rejectAll:()=>g(C),sync:y};return s.keksi=T,y(),u&&w(u.c),T}j(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
283
325
  }
284
326
  function sections(config, lang, h, dialog) {
285
327
  const t = textsFor(config, lang);
@@ -297,7 +339,7 @@ function sections(config, lang, h, dialog) {
297
339
  const items = c === "necessary" ? [site] : active.filter((p) => p.category === c).map((p) => ({ ...p, purpose: p.purpose[lang] }));
298
340
  const head = c === "necessary" ? `<h${h}>${e(t.necessary)}</h${h}><span>${e(t.alwaysOn)}</span>` : dialog ? `<h${h}><label for="keksi-c-${c}">${e(t[c])}</label></h${h}><input type="checkbox" role="switch" id="keksi-c-${c}" name="${c}" aria-describedby="keksi-i-${c}">` : `<h${h}>${e(t[c])}</h${h}>`;
299
341
  const list2 = items.map(
300
- (p) => `<div class="keksi-provider"><h${h + 1}>${e(p.name)}</h${h + 1}>${p.purpose ? `<p>${e(p.purpose)}</p>` : ""}` + (p.company && p.privacyPolicy ? `<p>${e(p.company)} \xB7 <a href="${e(p.privacyPolicy)}" target="_blank" rel="noopener">${e(t.privacyPolicy)}</a></p>` + (p.transfer ? `<p>${e(t.dpf)}</p>` : "") : "") + table(p.cookies, t) + `</div>`
342
+ (p) => `<div class="keksi-provider"><h${h + 1}>${e(p.name)}</h${h + 1}>${p.purpose ? `<p>${e(p.purpose)}</p>` : ""}` + (p.company && p.privacyPolicy ? `<p>${e(p.company)} \xB7 <a href="${e(p.privacyPolicy)}" target="_blank" rel="noopener">${e(t.privacyPolicy)}</a></p>` + (p.transfer ? `<p>${e(t[p.transfer])}</p>` : "") : "") + table(p.cookies, t) + `</div>`
301
343
  ).join("");
302
344
  const body = dialog ? `<details><summary>${e(t.providers)} (${items.length})</summary>${list2}</details>` : list2;
303
345
  return `<section class="keksi-cat"><div class="keksi-cat-head">${head}</div><p id="keksi-i-${c}">${e(t[`${c}Info`])}</p>${body}</section>`;
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Texts } from './i18n.js';
2
2
  export type Locale = 'fi' | 'sv' | 'en';
3
3
  export type Category = 'analytics' | 'marketing';
4
- export type ProviderId = 'ga' | 'googleAds' | 'meta' | 'youtube';
4
+ export type ProviderId = 'ga' | 'googleAds' | 'meta' | 'posthog' | 'youtube';
5
5
  export type Consent = Record<Category, boolean>;
6
6
  export interface KeksiConfig {
7
7
  /** GA4 measurement ids, e.g. `G-XXXXXXX`. */
@@ -10,6 +10,11 @@ export interface KeksiConfig {
10
10
  googleAds?: string | string[];
11
11
  /** Meta Pixel ids. */
12
12
  meta?: string | string[];
13
+ /** PostHog project token and ingestion host (default `https://eu.i.posthog.com`). */
14
+ posthog?: {
15
+ key: string;
16
+ host?: string;
17
+ };
13
18
  /** Enables `<keksi-youtube>` embeds. */
14
19
  youtube?: boolean;
15
20
  /** Cookie policy page, linked from the banner. */
@@ -40,7 +45,7 @@ export interface CookieInfo {
40
45
  duration: Duration;
41
46
  /** Third-party host that sets it. Omitted = set on this site, deleted on withdrawal. */
42
47
  host?: string;
43
- storage?: 'localStorage';
48
+ storage?: 'localStorage' | 'sessionStorage';
44
49
  }
45
50
  export interface Provider {
46
51
  id: ProviderId;
@@ -49,8 +54,8 @@ export interface Provider {
49
54
  company: string;
50
55
  privacyPolicy: string;
51
56
  purpose: Record<Locale, string>;
52
- /** Transfer outside the EU and its legal basis. */
53
- transfer?: 'dpf';
57
+ /** Transfer outside the EU: under the DPF, or to the US with no basis stated. */
58
+ transfer?: 'dpf' | 'us';
54
59
  cookies: CookieInfo[];
55
60
  /** Hosts it contacts, for the Playwright check. */
56
61
  hosts: string[];
@@ -64,8 +69,12 @@ export interface RuntimeData {
64
69
  ga: string[];
65
70
  ads: string[];
66
71
  meta: string[];
72
+ ph?: {
73
+ key: string;
74
+ host: string;
75
+ };
67
76
  yt: boolean;
68
- /** First-party cookie and localStorage names per category, `*` suffix = prefix. */
77
+ /** First-party cookie and storage names per category, `*` = wildcard. */
69
78
  del: Record<Category, string[]>;
70
79
  t: Pick<Texts, 'ytText' | 'ytButton' | 'ytOpen'>;
71
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keksi",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Lean cookie consent for Google and Meta trackers",
5
5
  "license": "MIT",
6
6
  "type": "module",