From d13373e086cb96a52708ee58a9266be68994f1d1 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Mon, 16 Mar 2026 12:12:28 -0700 Subject: [PATCH 1/2] fix: use transformIndexHtml for GA instead of vocs.config head MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vocs v2 removed the `head` config property — it existed in v1 but is silently ignored in v2. Move Google Analytics injection to a Vite plugin using transformIndexHtml, which correctly injects script tags into the HTML during build. Also keep the loadEnv fix so VITE_* env vars from .env files are available to all plugins. --- vite.config.ts | 24 +++++++++++++++++++++++- vocs.config.ts | 18 ------------------ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 5fb213cb..402f1428 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,10 +12,32 @@ export default defineConfig(({ mode }) => { if (!(key in process.env)) process.env[key] = env[key] } return { - plugins: [syncTips(), vocs(), react(), tempoNode()], + plugins: [googleAnalytics(), syncTips(), vocs(), react(), tempoNode()], } }) +function googleAnalytics(): Plugin { + const id = process.env.VITE_GA_MEASUREMENT_ID + return { + name: 'google-analytics', + transformIndexHtml() { + if (!id) return [] + return [ + { + tag: 'script', + attrs: { async: true, src: `https://www.googletagmanager.com/gtag/js?id=${id}` }, + injectTo: 'head', + }, + { + tag: 'script', + children: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${id}');`, + injectTo: 'head', + }, + ] + }, + } +} + function tempoNode(): Plugin { return { name: 'tempo-node', diff --git a/vocs.config.ts b/vocs.config.ts index 82c5ae11..9ebdd607 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -1,9 +1,6 @@ -import { createElement, Fragment } from 'react' import { Changelog, defineConfig, McpSource } from 'vocs/config' import { createFeedbackAdapter } from './src/lib/feedback-adapter' -const gaMeasurementId = process.env.VITE_GA_MEASUREMENT_ID - const baseUrl = (() => { if (URL.canParse(process.env.VITE_BASE_URL)) return process.env.VITE_BASE_URL // VERCEL_BRANCH_URL is the stable URL for the branch (e.g., next.docs.tempo.xyz) @@ -16,21 +13,6 @@ const baseUrl = (() => { })() export default defineConfig({ - head: gaMeasurementId - ? createElement( - Fragment, - null, - createElement('script', { - async: true, - src: `https://www.googletagmanager.com/gtag/js?id=${gaMeasurementId}`, - }), - createElement('script', { - dangerouslySetInnerHTML: { - __html: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${gaMeasurementId}');`, - }, - }), - ) - : undefined, changelog: Changelog.github({ prereleases: true, repo: 'tempoxyz/tempo' }), // TODO: Set back to true once tempoxyz/tempo#tip-1011 dead link is fixed checkDeadlinks: 'warn', From 8facd65b4dd845a5c97f657ddaec94dae209181b Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Mon, 16 Mar 2026 12:47:59 -0700 Subject: [PATCH 2/2] fix: use React component for GA instead of dead transformIndexHtml Vocs v2 uses Waku (RSC) which bypasses Vite's HTML pipeline entirely. transformIndexHtml is never invoked. Use a client-side React component in _layout.tsx instead, matching the pattern used for Vercel Analytics and PostHog. --- src/components/GoogleAnalytics.tsx | 37 ++++++++++++++++++++++++++++++ src/pages/_layout.tsx | 2 ++ vite.config.ts | 24 +------------------ 3 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 src/components/GoogleAnalytics.tsx diff --git a/src/components/GoogleAnalytics.tsx b/src/components/GoogleAnalytics.tsx new file mode 100644 index 00000000..fd12f674 --- /dev/null +++ b/src/components/GoogleAnalytics.tsx @@ -0,0 +1,37 @@ +'use client' + +import { useEffect } from 'react' + +declare global { + interface Window { + dataLayer: unknown[] + gtag: (...args: unknown[]) => void + } +} + +function GoogleAnalyticsInit({ id }: { id: string }) { + useEffect(() => { + if (typeof window.gtag !== 'undefined') return + + window.dataLayer = window.dataLayer || [] + window.gtag = function gtag() { + // biome-ignore lint/complexity/noArguments: gtag API requires arguments object + window.dataLayer.push(arguments) + } + window.gtag('js', new Date()) + window.gtag('config', id) + + const script = document.createElement('script') + script.async = true + script.src = `https://www.googletagmanager.com/gtag/js?id=${id}` + document.head.appendChild(script) + }, [id]) + + return null +} + +export default function GoogleAnalytics() { + const id = import.meta.env.VITE_GA_MEASUREMENT_ID + if (!id) return null + return +} diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index c45a1050..6b37fb56 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -4,6 +4,7 @@ import { Analytics } from '@vercel/analytics/react' import { SpeedInsights } from '@vercel/speed-insights/react' import type React from 'react' import { Toaster } from 'sonner' +import GoogleAnalytics from '../components/GoogleAnalytics' import PostHogSetup from '../components/PostHogSetup' export default function Layout( @@ -29,6 +30,7 @@ export default function Layout( /> + ) diff --git a/vite.config.ts b/vite.config.ts index 402f1428..5fb213cb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,32 +12,10 @@ export default defineConfig(({ mode }) => { if (!(key in process.env)) process.env[key] = env[key] } return { - plugins: [googleAnalytics(), syncTips(), vocs(), react(), tempoNode()], + plugins: [syncTips(), vocs(), react(), tempoNode()], } }) -function googleAnalytics(): Plugin { - const id = process.env.VITE_GA_MEASUREMENT_ID - return { - name: 'google-analytics', - transformIndexHtml() { - if (!id) return [] - return [ - { - tag: 'script', - attrs: { async: true, src: `https://www.googletagmanager.com/gtag/js?id=${id}` }, - injectTo: 'head', - }, - { - tag: 'script', - children: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${id}');`, - injectTo: 'head', - }, - ] - }, - } -} - function tempoNode(): Plugin { return { name: 'tempo-node',