diff --git a/.env.example b/.env.example index 5f200cf..c2c9cd2 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,12 @@ PUBLIC_SANITY_STRICT_MODE=false # Optional. Used by `` in BaseLayout. PUBLIC_FB_APP_ID= +# ---- Google Analytics (gtag.js / GA4) -------------------------------------- +# Optional. When set, BaseLayout loads gtag and tracks page views +# (including Astro ClientRouter navigations). +# Example: PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXX +PUBLIC_GA_MEASUREMENT_ID= + # ---- Deploy trigger (server-only) ----------------------------------------- # Used by /deploy and /api/trigger-deploy in local dev + Vercel. # Do NOT prefix with PUBLIC_. Keep these secret. diff --git a/README.md b/README.md index 63e8776..ec11f4d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Release history: [CHANGELOG.md](CHANGELOG.md). - **Three.js background** — Particle field and wireframe accents (respects reduced motion; pause control) - **Design system** — Scoped component styles plus tokens in `src/styles/global.css` (self-hosted **Tiny5** + **Nunito**) - **Configurable imagery** — Header brand, org logos, footer sponsors (`src/data/siteLogos.ts`) -- **Analytics** — [Vercel Analytics](https://vercel.com/docs/analytics) (`@vercel/analytics`) +- **Analytics** — [Vercel Analytics](https://vercel.com/docs/analytics) (`@vercel/analytics`); optional GA4 via `PUBLIC_GA_MEASUREMENT_ID` - **TypeScript**, sitemap, client-side transitions (`ClientRouter`) ## Requirements @@ -56,9 +56,11 @@ npm run preview PUBLIC_SANITY_PROJECT_ID=your_project_id PUBLIC_SANITY_DATASET=production PUBLIC_SANITY_STRICT_MODE=false +PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXX ``` - Set `PUBLIC_SANITY_STRICT_MODE=true` in production to fail builds when Sanity fetches fail instead of silently shipping empty fallback content. +- Set `PUBLIC_GA_MEASUREMENT_ID` to enable Google Analytics 4 (gtag). Leave unset to skip loading the tag. **Standalone Studio** (`studio-hack-michigan-/`) resolves project and dataset in `sanity.env.ts`: @@ -70,6 +72,8 @@ The Sanity CLI loads `.env` from `studio-hack-michigan-/` when you run `sanity d For Facebook Open Graph sharing you can set `PUBLIC_FB_APP_ID` (see `src/env.d.ts`). +For Google Analytics 4, set `PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXX`. When present, `BaseLayout` loads gtag.js and records page views on first load and on Astro `ClientRouter` navigations. + ### Deploy trigger env The internal `/deploy/` page posts to `/api/trigger-deploy/`, which forwards to an n8n webhook. diff --git a/src/env.d.ts b/src/env.d.ts index 298dde6..9b218ed 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -12,6 +12,8 @@ declare module "*.astro" { interface ImportMetaEnv { /** Facebook App ID for `fb:app_id` (Open Graph). Set in Vercel / `.env`. */ readonly PUBLIC_FB_APP_ID?: string; + /** GA4 Measurement ID (e.g. `G-XXXXXXXX`). When set, BaseLayout injects gtag. */ + readonly PUBLIC_GA_MEASUREMENT_ID?: string; /** Sanity project ID. Defaults to the HackMI project in `astro.config.mjs` if unset. */ readonly PUBLIC_SANITY_PROJECT_ID?: string; /** Sanity dataset. Defaults to `production` in `astro.config.mjs` if unset. */ diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index d344569..d365236 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -24,6 +24,33 @@ const { } = Astro.props; const fbAppId = import.meta.env.PUBLIC_FB_APP_ID?.trim(); +const gaMeasurementId = import.meta.env.PUBLIC_GA_MEASUREMENT_ID?.trim(); + +/** + * Inline gtag bootstrap as a string so prettier-plugin-astro does not parse + * the script body (define:vars + modern JS has triggered SyntaxError on check). + */ +const gtagBootstrap = gaMeasurementId + ? ` +window.dataLayer = window.dataLayer || []; +function gtag() { + window.dataLayer.push(arguments); +} +if (!window.__hackmiGtagInit) { + window.__hackmiGtagInit = true; + gtag("js", new Date()); + gtag("config", ${JSON.stringify(gaMeasurementId)}, { send_page_view: false }); + function sendGaPageView() { + gtag("event", "page_view", { + page_title: document.title, + page_location: location.href, + page_path: location.pathname + location.search + location.hash, + }); + } + document.addEventListener("astro:page-load", sendGaPageView); +} +`.trim() + : ""; const defaultOgPath = `${base}social-card.jpg`; const image = imageProp ?? defaultOgPath; @@ -151,6 +178,18 @@ const selfHostedFontFaces = ` />