Google Analytics & Google Tag Manager¶
Breadcrumbs: Documentation > Guides > SEO > Analytics
This guide explains the Google Analytics and Google Tag Manager implementation for tracking website metrics.
Table of Contents¶
- Overview
- Google Analytics Setup
- Google Tag Manager Setup
- Implementation
- Event Tracking
- Privacy Considerations
- Testing
Overview¶
The portfolio uses both Google Analytics 4 (GA4) and Google Tag Manager (GTM) for comprehensive analytics tracking.
Features:
- Page view tracking
- Event tracking (downloads, clicks, etc.)
- User behavior analysis
- Conversion tracking
- Real-time monitoring
Additional analytics:
- Vercel Analytics (built-in)
- Vercel Speed Insights (performance monitoring)
Google Analytics Setup¶
Create GA4 Property¶
Steps:
- Go to Google Analytics
- Click "Admin" (gear icon)
- Click "Create Property"
- Enter property details:
- Property name: "Portfolio Website"
- Time zone: Your timezone
- Currency: Your currency
- Click "Next"
- Fill business information
- Click "Create"
- Accept terms
- Copy Measurement ID (format:
G-XXXXXXXXXX)
Configure Data Streams¶
- Click "Data Streams"
- Click "Add stream" → "Web"
- Enter website URL
- Enter stream name
- Click "Create stream"
- Copy Measurement ID
Add to Environment Variables¶
Google Tag Manager Setup¶
Create GTM Account¶
Steps:
- Go to Google Tag Manager
- Click "Create Account"
- Enter account name (your name/company)
- Enter container name (your website domain)
- Select "Web" as target platform
- Click "Create"
- Accept terms
- Copy Container ID (format:
GTM-XXXXXXX)
Add to Environment Variables¶
Install GTM Container Code¶
The code is automatically installed via the Analytics component.
Implementation¶
All analytics are gated behind GDPR cookie consent. Scripts only load after the user explicitly accepts via the cookie banner. See Consent System for full details.
Analytics Component¶
File: components/meta/analytics.tsx
"use client";
import { analyticsConfig, appConfig } from "@/lib/config";
import { useConsent } from "@/components/consent/ConsentProvider";
import { Analytics as VercelAnalytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
export function Analytics() {
const { consent } = useConsent();
// Only load after explicit user consent
if (consent !== "accepted") return null;
if (!analyticsConfig.gaId || !analyticsConfig.gtmId || appConfig.isDevelopment) return null;
return (
<>
<script async src={`https://www.googletagmanager.com/gtag/js?id=${analyticsConfig.gaId}`} />
<script dangerouslySetInnerHTML={{ __html: `...gtag config...` }} />
<script dangerouslySetInnerHTML={{ __html: `...GTM loader...` }} />
<VercelAnalytics />
<SpeedInsights />
</>
);
}
Layout Integration¶
// app/layout.tsx
import { ConsentProvider } from "@/components/consent/ConsentProvider";
import { CookieBanner } from "@/components/consent/CookieBanner";
import Analytics from "@/components/meta/analytics";
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
{/* google-site-verification is in the static metadata object, not the Analytics component */}
</head>
<body>
<ConsentProvider>
<Analytics /> {/* renders nothing until consent === 'accepted' */}
<ThemeProvider>
{children}
<CookieBanner /> {/* shown until user decides */}
</ThemeProvider>
</ConsentProvider>
</body>
</html>
);
}
Configuration¶
// lib/config.ts
export const analyticsConfig = {
gaId: process.env.NEXT_PUBLIC_GA_ID || "",
gtmId: process.env.NEXT_PUBLIC_GTM_ID || "",
};
export const appConfig = {
environment: process.env.NODE_ENV || "development",
isDevelopment: process.env.NODE_ENV === "development",
isProduction: process.env.NODE_ENV === "production",
};
Event Tracking¶
Custom Events¶
Track button clicks:
// components/download-button.tsx
'use client';
export function DownloadButton() {
const handleDownload = () => {
// Track with Google Analytics
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'download', {
event_category: 'engagement',
event_label: 'resume',
value: 1,
});
}
// Download logic
window.location.href = '/download/resume.pdf';
};
return <Button onClick={handleDownload}>Download Resume</Button>;
}
Track external links:
// components/external-link.tsx
'use client';
export function ExternalLink({ href, children }) {
const handleClick = () => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'click', {
event_category: 'outbound',
event_label: href,
transport_type: 'beacon',
});
}
};
return (
<a href={href} target="_blank" rel="noopener noreferrer" onClick={handleClick}>
{children}
</a>
);
}
GTM Custom Events¶
Using Data Layer:
'use client';
export function ContactForm() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Push event to GTM Data Layer
if (typeof window !== 'undefined' && window.dataLayer) {
window.dataLayer.push({
event: 'form_submission',
formName: 'contact_form',
formId: 'contact',
});
}
// Form submission logic
};
return <form onSubmit={handleSubmit}>{/* Form fields */}</form>;
}
TypeScript declarations:
// global.d.ts
declare global {
interface Window {
gtag: (...args: any[]) => void;
dataLayer: any[];
}
}
export {};
Common Events to Track¶
1. Page views (automatic with GA4)
2. Downloads:
gtag("event", "file_download", {
file_name: "resume.pdf",
file_extension: "pdf",
link_url: "/download/resume.pdf",
});
3. Outbound clicks:
gtag("event", "click", {
event_category: "outbound",
event_label: "https://github.com/username",
});
4. Project views:
gtag("event", "view_item", {
item_id: project.slug,
item_name: project.title,
item_category: "project",
});
5. Social clicks:
Privacy Considerations¶
GDPR Compliance¶
The site is fully compliant with the EU ePrivacy Directive and GDPR for analytics:
- Consent-first: analytics scripts never load until the user clicks Accept
- Cookie banner:
components/consent/CookieBanner.tsx— fixed bottom bar with equal-prominence Accept/Decline buttons - Persistent preference: consent stored in
localStorage(cookie-consent: 'accepted' | 'declined') - Revocable: "Cookie Settings" link in the footer resets consent so the banner reappears
- Privacy policy:
/privacypage covers data controller, cookie table, legal basis, retention, GDPR rights
See: Privacy & Compliance Guide
Consent State¶
// Three possible states
type ConsentState = "accepted" | "declined" | null; // null = no decision yet
// From useConsent() hook
const { consent, accept, decline, reset } = useConsent();
Anonymize IP Addresses¶
GA4 anonymizes IPs automatically. To be explicit:
Testing¶
Test in Development¶
1. Enable in development (temporarily):
// components/meta/analytics.tsx
export function Analytics() {
// Remove isDevelopment check for testing
if (!analyticsConfig.gaId || !analyticsConfig.gtmId) {
return null;
}
// Rest of code...
}
2. Check browser console:
Test with GA Debugger¶
Chrome Extension:
- Install Google Analytics Debugger
- Enable extension
- Visit your site
- Check console for GA debug messages
Test with GTM Preview¶
Steps:
- Go to Google Tag Manager
- Click "Preview"
- Enter your website URL
- Click "Connect"
- Interact with your site
- Check which tags fire
Test in Production¶
Real-time reports:
- Go to Google Analytics
- Click "Reports" → "Realtime"
- Visit your site in another tab
- Check if visit appears
Common Issues¶
Issue: Analytics not loading¶
Check:
- Environment variables set correctly
- Not in development mode
- Ad blocker disabled
- Console for errors
Issue: Events not tracking¶
Check:
gtagfunction exists- Event name and parameters correct
- GTM tags configured properly
- Check GA4 DebugView
See Also¶
- SEO Guide - Complete SEO implementation
- Performance - Performance optimization
- Configuration - Environment variables
Next Steps¶
- Create GA4 property
- Create GTM container
- Add IDs to environment variables
- Test analytics tracking
- Set up custom events
- Monitor analytics reports
Last Updated: February 2026
Maintainers: Simon Stijnen
Questions? Open an issue on GitHub