Skip to content

Commit 75eb54b

Browse files
authored
feat(docs): consent banner + vue-privacy 1.2.2 with gtag fix (#255)
* fix(docs): add ConsentBanner to layout and update vue-privacy to 1.2.2 - Add ConsentBanner to layout-bottom slot (fixes banner not showing) - Update @structured-world/vue-privacy ^1.1.0 → ^1.2.2 - Fixes gtag dataLayer.push using Array instead of Arguments - Fixes consent banner race condition (bannerPending) * docs(theme): clarify KV storage endpoint in consent config - Add comment that /api/consent is served by Cloudflare Worker * fix(docs): use consentTheme Layout with DefaultTheme fallback consentTheme may provide its own Layout — using DefaultTheme.Layout directly bypasses consent-aware layout enhancements.
1 parent ad912f6 commit 75eb54b

File tree

3 files changed

+33
-71
lines changed

3 files changed

+33
-71
lines changed

docs/.vitepress/theme/index.ts

Lines changed: 16 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,30 @@
1-
import { h, watch, nextTick } from "vue";
1+
import { h } from "vue";
22
import DefaultTheme from "vitepress/theme";
33
import type { Theme } from "vitepress";
4-
import { inBrowser } from "vitepress";
4+
import { enhanceWithConsent, ConsentBanner } from "@structured-world/vue-privacy/vitepress";
5+
import { createKVStorage } from "@structured-world/vue-privacy";
56
import "./style.css";
67
import BugReportWidget from "./components/BugReportWidget.vue";
78

89
// GA4 tracking ID - fallback is gitlab-mcp.sw.foundation production property
910
const GA_ID = import.meta.env.VITE_GA_ID || "G-RY1XJ7LR5F";
1011

11-
// Declare gtag types
12-
declare global {
13-
interface Window {
14-
dataLayer?: unknown[];
15-
gtag?: (...args: unknown[]) => void;
16-
}
17-
}
18-
19-
function initGoogleAnalytics() {
20-
if (!inBrowser || !GA_ID) return;
21-
22-
// Avoid duplicate initialization
23-
if (window.dataLayer && window.gtag) return;
24-
25-
// Load gtag script
26-
const script = document.createElement("script");
27-
script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`;
28-
script.async = true;
29-
document.head.appendChild(script);
30-
31-
// Initialize gtag
32-
window.dataLayer = window.dataLayer || [];
33-
window.gtag = function (...args: unknown[]) {
34-
window.dataLayer!.push(args);
35-
};
36-
37-
// Set default consent - required for GA4 to send data
38-
// This grants analytics by default (no ads tracking)
39-
window.gtag("consent", "default", {
40-
analytics_storage: "granted",
41-
ad_storage: "denied",
42-
ad_user_data: "denied",
43-
ad_personalization: "denied",
44-
});
45-
46-
window.gtag("js", new Date());
47-
// Disable automatic page_view - we send manually to track SPA navigation
48-
window.gtag("config", GA_ID, { send_page_view: false });
49-
}
50-
51-
function trackPageView(path: string) {
52-
if (!inBrowser || !window.gtag) return;
53-
54-
window.gtag("event", "page_view", {
55-
page_path: path,
56-
page_location: window.location.href,
57-
page_title: document.title,
58-
});
59-
}
12+
// Enhance DefaultTheme with GDPR-compliant consent management and SPA page tracking.
13+
// enhanceWithConsent automatically:
14+
// - Sets consent defaults (denied for EU, granted for non-EU)
15+
// - Loads gtag.js with send_page_view: false (SPA mode)
16+
// - Tracks initial page view and watches router for SPA navigations
17+
const consentTheme = enhanceWithConsent(DefaultTheme, {
18+
gaId: GA_ID,
19+
// KV storage via Cloudflare Worker (vue-privacy-worker) at gitlab-mcp.sw.foundation/api/consent*
20+
storage: createKVStorage("/api/consent"),
21+
});
6022

6123
export default {
62-
extends: DefaultTheme,
24+
...consentTheme,
6325
Layout() {
64-
return h(DefaultTheme.Layout, null, {
65-
"layout-bottom": () => h(BugReportWidget),
26+
return h(consentTheme.Layout ?? DefaultTheme.Layout, null, {
27+
"layout-bottom": () => [h(ConsentBanner), h(BugReportWidget)],
6628
});
6729
},
68-
enhanceApp({ router }) {
69-
if (inBrowser) {
70-
initGoogleAnalytics();
71-
72-
// Track initial page view after DOM is ready
73-
nextTick(() => trackPageView(window.location.pathname));
74-
75-
// Track subsequent SPA navigations
76-
watch(
77-
() => router.route.path,
78-
(path: string) => {
79-
// Wait for Vue to update DOM (including document.title) before tracking
80-
nextTick(() => trackPageView(path));
81-
}
82-
);
83-
}
84-
},
8530
} satisfies Theme;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@
468468
"@semantic-release/git": "^10.0.1",
469469
"@semantic-release/github": "^12.0.2",
470470
"@semantic-release/npm": "^13.1.3",
471+
"@structured-world/vue-privacy": "^1.2.2",
471472
"@types/express": "^5.0.6",
472473
"@types/jest": "^30.0.0",
473474
"@types/node": "^24.10.9",

yarn.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2627,6 +2627,7 @@ __metadata:
26272627
"@semantic-release/git": "npm:^10.0.1"
26282628
"@semantic-release/github": "npm:^12.0.2"
26292629
"@semantic-release/npm": "npm:^13.1.3"
2630+
"@structured-world/vue-privacy": "npm:^1.2.2"
26302631
"@types/express": "npm:^5.0.6"
26312632
"@types/jest": "npm:^30.0.0"
26322633
"@types/node": "npm:^24.10.9"
@@ -2664,6 +2665,21 @@ __metadata:
26642665
languageName: unknown
26652666
linkType: soft
26662667

2668+
"@structured-world/vue-privacy@npm:^1.2.2":
2669+
version: 1.2.2
2670+
resolution: "@structured-world/vue-privacy@npm:1.2.2"
2671+
peerDependencies:
2672+
vue: ^3.3.0
2673+
vue-router: ^4.0.0
2674+
peerDependenciesMeta:
2675+
vue:
2676+
optional: true
2677+
vue-router:
2678+
optional: true
2679+
checksum: 10c0/8feb5fa57431eb9dc6e180e24ef6c06fe568ee94147fa11bad7f31d45e668134c08061ad181c65b24a0ca96f716145fa6917fea41d96cd8f1d3c33f19e92cd8c
2680+
languageName: node
2681+
linkType: hard
2682+
26672683
"@tsconfig/node10@npm:^1.0.7":
26682684
version: 1.0.11
26692685
resolution: "@tsconfig/node10@npm:1.0.11"

0 commit comments

Comments
 (0)