Since Strapi is a headless CMS, Google Analytics 4 is installed on your frontend framework (Next.js, Gatsby, Nuxt.js, etc.), not in Strapi itself. This guide covers GA4 installation for the most common frontend frameworks used with Strapi.
Important: Frontend vs Backend
Remember:
- Install GA4 in your frontend application (Next.js, Gatsby, etc.)
- Do NOT try to install GA4 in Strapi admin panel
- Strapi provides content via API; tracking happens on frontend
- Use GTM for easier management across all frameworks
Before You Begin
Create a GA4 Property
- Go to Google Analytics
- Create a new GA4 property
- Note your Measurement ID (format:
G-XXXXXXXXXX)
Choose Your Implementation Method
- Direct gtag.js: Simple, framework-specific installation
- Google Tag Manager: Recommended for easier management
- Third-party libraries: Framework-specific analytics packages
Method 1: Next.js + Strapi
Next.js is the most popular framework for Strapi-powered sites.
App Router (Next.js 13+)
1. Install GA4 Script Globally
Create a Google Analytics component:
// app/components/GoogleAnalytics.tsx
'use client';
import Script from 'next/script';
export default function GoogleAnalytics({ GA_MEASUREMENT_ID }: { GA_MEASUREMENT_ID: string }) {
return (
<>
<Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
/>
<Script
id="google-analytics"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_MEASUREMENT_ID}', {
page_path: window.location.pathname,
});
`,
}}
/>
</>
);
}
2. Add to Root Layout
// app/layout.tsx
import GoogleAnalytics from '@/components/GoogleAnalytics';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<GoogleAnalytics GA_MEASUREMENT_ID={process.env.NEXT_PUBLIC_GA_ID!} />
</body>
</html>
);
}
3. Track Route Changes
// app/components/PageViewTracker.tsx
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
export default function PageViewTracker() {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (pathname) {
window.gtag('config', process.env.NEXT_PUBLIC_GA_ID!, {
page_path: pathname + searchParams.toString(),
});
}
}, [pathname, searchParams]);
return null;
}
Add to layout:
// app/layout.tsx
import PageViewTracker from '@/components/PageViewTracker';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<GoogleAnalytics GA_MEASUREMENT_ID={process.env.NEXT_PUBLIC_GA_ID!} />
<Suspense fallback={null}>
<PageViewTracker />
</Suspense>
</body>
</html>
);
}
Pages Router (Next.js 12 and earlier)
1. Create Analytics Library
// lib/analytics.js
export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID;
// Log page views
export const pageview = (url) => {
if (typeof window.gtag !== 'undefined') {
window.gtag('config', GA_TRACKING_ID, {
page_path: url,
});
}
};
// Log specific events
export const event = ({ action, category, label, value }) => {
if (typeof window.gtag !== 'undefined') {
window.gtag('event', action, {
event_category: category,
event_label: label,
value: value,
});
}
};
2. Add GA4 to _app.js
// pages/_app.js
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import Script from 'next/script';
import * as gtag from '../lib/analytics';
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const handleRouteChange = (url) => {
gtag.pageview(url);
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
return (
<>
{/* Global Site Tag (gtag.js) - Google Analytics */}
<Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_TRACKING_ID}`}
/>
<Script
id="gtag-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${gtag.GA_TRACKING_ID}', {
page_path: window.location.pathname,
});
`,
}}
/>
<Component {...pageProps} />
</>
);
}
export default MyApp;
3. Track Strapi Content Views
// pages/articles/[slug].js
import { useEffect } from 'react';
import * as gtag from '@/lib/analytics';
export default function Article({ article }) {
useEffect(() => {
// Track article view with Strapi metadata
gtag.event({
action: 'view_content',
category: 'Article',
label: article.attributes.title,
value: article.id,
});
}, [article]);
return (
<article>
<h1>{article.attributes.title}</h1>
{/* Article content */}
</article>
);
}
export async function getStaticProps({ params }) {
const res = await fetch(`${process.env.STRAPI_API_URL}/api/articles?filters[slug][$eq]=${params.slug}&populate=*`);
const { data } = await res.json();
return {
props: { article: data[0] },
revalidate: 60,
};
}
Environment Variables
# .env.local
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
STRAPI_API_URL=http://localhost:1337
Method 2: Gatsby + Strapi
Gatsby uses plugins for analytics integration.
Installation
npm install gatsby-plugin-google-gtag
Configuration
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-google-gtag`,
options: {
trackingIds: [
process.env.GA_MEASUREMENT_ID, // Google Analytics / GA
],
pluginConfig: {
head: false, // Put script in <head>
respectDNT: true, // Respect Do Not Track
exclude: ['/preview/**', '/do-not-track/me/too/'],
origin: 'https://www.googletagmanager.com',
delayOnRouteUpdate: 0,
},
},
},
{
resolve: 'gatsby-source-strapi',
options: {
apiURL: process.env.STRAPI_API_URL || 'http://localhost:1337',
accessToken: process.env.STRAPI_TOKEN,
collectionTypes: ['article', 'category', 'author'],
singleTypes: ['homepage', 'about'],
},
},
],
};
Track Strapi Content
// src/templates/article.js
import React, { useEffect } from 'react';
import { graphql } from 'gatsby';
const ArticleTemplate = ({ data }) => {
const article = data.strapiArticle;
useEffect(() => {
// Track article view
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'view_content', {
content_type: 'article',
content_id: article.strapiId,
content_title: article.title,
});
}
}, [article]);
return (
<article>
<h1>{article.title}</h1>
{/* Article content */}
</article>
);
};
export const query = graphql`
query($slug: String!) {
strapiArticle(slug: { eq: $slug }) {
strapiId
title
content
publishedAt
}
}
`;
export default ArticleTemplate;
Environment Variables
# .env.production
GA_MEASUREMENT_ID=G-XXXXXXXXXX
STRAPI_API_URL=https://your-strapi.com
STRAPI_TOKEN=your-api-token
Method 3: Nuxt.js + Strapi
Nuxt uses modules for analytics integration.
Installation
npm install @nuxtjs/google-analytics
# or for Nuxt 3
npm install @nuxtjs/gtm
Configuration (Nuxt 2)
// nuxt.config.js
export default {
modules: [
'@nuxtjs/google-analytics',
],
googleAnalytics: {
id: process.env.GA_MEASUREMENT_ID,
dev: false, // Disable in dev mode
},
publicRuntimeConfig: {
strapiUrl: process.env.STRAPI_URL || 'http://localhost:1337',
},
};
Configuration (Nuxt 3)
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/gtm'],
gtm: {
id: process.env.GA_MEASUREMENT_ID,
enabled: true,
debug: false,
},
runtimeConfig: {
public: {
strapiUrl: process.env.STRAPI_URL || 'http://localhost:1337',
},
},
});
Track Strapi Content
<!-- pages/articles/[slug].vue -->
<script setup>
const route = useRoute();
const { data: article } = await useFetch(`/api/articles/${route.params.slug}`);
onMounted(() => {
// Track article view
if (process.client && window.gtag) {
window.gtag('event', 'view_content', {
content_type: 'article',
content_id: article.value.id,
content_title: article.value.attributes.title,
});
}
});
</script>
<template>
<article>
<h1>{{ article.attributes.title }}</h1>
<!-- Article content -->
</article>
</template>
Method 4: React SPA + Strapi
For simple React single-page applications.
Installation
npm install react-ga4
Implementation
// src/analytics.js
import ReactGA from 'react-ga4';
export const initGA = () => {
ReactGA.initialize(process.env.REACT_APP_GA_ID);
};
export const logPageView = () => {
ReactGA.send({ hitType: 'pageview', page: window.location.pathname });
};
export const logEvent = (category, action, label) => {
ReactGA.event({
category: category,
action: action,
label: label,
});
};
// src/App.js
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { initGA, logPageView } from './analytics';
function App() {
const location = useLocation();
useEffect(() => {
initGA();
}, []);
useEffect(() => {
logPageView();
}, [location]);
return (
<div className="App">
{/* Your app content */}
</div>
);
}
export default App;
Track Strapi Content
// src/components/Article.js
import { useEffect } from 'react';
import { logEvent } from '../analytics';
function Article({ article }) {
useEffect(() => {
logEvent('Content', 'View Article', article.attributes.title);
}, [article]);
return (
<article>
<h1>{article.attributes.title}</h1>
{/* Article content */}
</article>
);
}
Method 5: Google Tag Manager (Recommended)
GTM provides the easiest management across all frameworks.
See Install Google Tag Manager on Strapi Sites for complete GTM implementation, which is recommended over direct GA4 installation.
Benefits:
- Framework-agnostic implementation
- Easier to update without code changes
- Centralized tag management
- Better for teams with non-technical marketers
SSR/SSG Considerations
Server-Side Rendering (SSR)
Problem: GA4 scripts should only run on client, not server.
Solution: Always check for window object:
// Only initialize on client
if (typeof window !== 'undefined') {
initializeGA();
}
Static Site Generation (SSG)
Problem: Scripts run during build can cause issues.
Solution: Use dynamic imports or lazy loading:
// Next.js example
useEffect(() => {
import('../lib/analytics').then((mod) => {
mod.initGA();
});
}, []);
Hybrid Rendering
For sites using both SSR and SSG:
// Detect environment
const isClient = typeof window !== 'undefined';
const isProduction = process.env.NODE_ENV === 'production';
if (isClient && isProduction) {
initializeGA();
}
Verification & Testing
1. Check GA4 Realtime Reports
- Open GA4 → Reports → Realtime
- Navigate your Strapi-powered site
- Verify events appear within 30 seconds
2. Use Browser Console
// Check if GA4 is loaded
console.log(window.gtag);
console.log(window.dataLayer);
// Test event manually
gtag('event', 'test_event', { test_parameter: 'test_value' });
3. Use GA4 DebugView
Enable debug mode:
gtag('config', 'G-XXXXXXXXXX', {
debug_mode: true
});
Then check Admin → DebugView in GA4.
4. Test Different Page Types
Test across your Strapi content structure:
- Homepage (Single Type)
- Blog posts (Collection Type)
- Category pages (Collection Type)
- Dynamic routes (SSR/ISR)
- Static pages (SSG)
5. Verify Route Changes
For SPAs, ensure page views fire on navigation:
// Should fire on each route change
router.events.on('routeChangeComplete', (url) => {
console.log('Route changed to:', url);
gtag('config', GA_ID, { page_path: url });
});
Common Issues
GA4 Not Loading in Development
Solution: GA4 often disabled in dev mode. Either:
- Test in production build
- Remove dev environment check temporarily
- Use GA4 debug mode
Duplicate Page Views
Cause: Both automatic and manual page view tracking.
Solution: Disable automatic tracking:
gtag('config', 'G-XXXXXXXXXX', {
send_page_view: false
});
Events Not Firing on Client Navigation
Cause: SPA route changes not tracked.
Solution: Implement route change listener (shown in framework examples above).
SSR Hydration Errors
Cause: GA4 script running on server.
Solution: Use useEffect or onMounted to ensure client-only execution.
Troubleshooting
For detailed troubleshooting, see:
Strapi Plugin Ecosystem for Analytics
While GA4 is installed on your frontend, Strapi's plugin ecosystem can enhance your analytics setup.
Available Strapi Plugins
1. Strapi Plugin Analytics (Community)
Track content operations in Strapi admin panel:
npm install strapi-plugin-analytics
Configuration:
// config/plugins.js
module.exports = {
analytics: {
enabled: true,
config: {
providers: [
{
name: 'google-analytics',
enabled: true,
config: {
trackingId: process.env.GA_MEASUREMENT_ID,
},
},
],
},
},
};
2. Strapi Plugin Sitemap
Auto-generate sitemaps to improve SEO and tracking:
npm install strapi-plugin-sitemap
Configuration:
// config/plugins.js
module.exports = {
sitemap: {
enabled: true,
config: {
autoGenerate: true,
allowedFields: ['title', 'slug', 'publishedAt'],
excludeDrafts: true,
},
},
};
3. Custom Analytics Plugin
Create a custom plugin to track content changes:
# Create plugin
cd plugins
npx create-strapi-plugin analytics-tracker
Plugin code:
// plugins/analytics-tracker/server/bootstrap.js
module.exports = ({ strapi }) => {
strapi.db.lifecycles.subscribe({
models: ['api::article.article'],
async afterCreate(event) {
const { result } = event;
// Send to GA4 Measurement Protocol
await strapi.plugin('analytics-tracker').service('tracker').trackEvent({
name: 'content_created',
params: {
content_type: 'article',
content_id: result.id,
content_title: result.title,
},
});
},
async afterUpdate(event) {
const { result } = event;
await strapi.plugin('analytics-tracker').service('tracker').trackEvent({
name: 'content_updated',
params: {
content_type: 'article',
content_id: result.id,
},
});
},
});
};
Content Type Tracking and Data Layer Setup
Track specific Strapi content types with structured data.
Define Content Type Data Layer
Create a standardized data layer structure for your Strapi content:
// lib/strapiDataLayer.ts
interface StrapiContentData {
id: number;
contentType: string;
title: string;
slug: string;
category?: string;
author?: string;
publishedAt?: string;
tags?: string[];
}
export function buildContentDataLayer(
strapiData: any,
contentType: string
): StrapiContentData {
const attributes = strapiData.attributes || strapiData;
return {
id: strapiData.id,
contentType,
title: attributes.title || attributes.name || 'Untitled',
slug: attributes.slug,
category: attributes.category?.data?.attributes?.name,
author: attributes.author?.data?.attributes?.name,
publishedAt: attributes.publishedAt,
tags: attributes.tags?.data?.map((tag: any) => tag.attributes.name) || [],
};
}
export function pushContentToDataLayer(contentData: StrapiContentData) {
if (typeof window !== 'undefined' && window.dataLayer) {
window.dataLayer.push({
event: 'view_content',
content: contentData,
});
}
// Also track with gtag if available
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'view_content', {
content_type: contentData.contentType,
content_id: contentData.id.toString(),
item_id: contentData.slug,
items: [{
item_id: contentData.slug,
item_name: contentData.title,
item_category: contentData.category,
}],
});
}
}
Track Different Content Types
Articles:
// components/ArticleView.tsx
'use client';
import { useEffect } from 'react';
import { buildContentDataLayer, pushContentToDataLayer } from '@/lib/strapiDataLayer';
export function ArticleView({ article }: { article: any }) {
useEffect(() => {
const contentData = buildContentDataLayer(article, 'article');
pushContentToDataLayer(contentData);
}, [article.id]);
return (
<article>
<h1>{article.attributes.title}</h1>
{/* Article content */}
</article>
);
}
Products (for e-commerce sites):
// components/ProductView.tsx
'use client';
import { useEffect } from 'react';
export function ProductView({ product }: { product: any }) {
useEffect(() => {
if (window.gtag) {
window.gtag('event', 'view_item', {
currency: 'USD',
value: product.attributes.price,
items: [{
item_id: product.attributes.sku,
item_name: product.attributes.name,
item_category: product.attributes.category?.data?.attributes?.name,
price: product.attributes.price,
}],
});
}
}, [product.id]);
return <div>{/* Product UI */}</div>;
}
Categories/Collections:
// components/CategoryView.tsx
'use client';
export function CategoryView({ category, articles }: any) {
useEffect(() => {
if (window.gtag) {
window.gtag('event', 'view_item_list', {
item_list_id: category.id.toString(),
item_list_name: category.attributes.name,
items: articles.map((article: any, index: number) => ({
item_id: article.attributes.slug,
item_name: article.attributes.title,
index,
})),
});
}
}, [category.id]);
return <div>{/* Category UI */}</div>;
}
GraphQL Data Layer Structure
When using Strapi's GraphQL API:
// lib/graphqlDataLayer.ts
import { gql } from '@apollo/client';
export const ARTICLE_TRACKING_FRAGMENT = gql`
fragment ArticleTracking on Article {
id
attributes {
title
slug
publishedAt
category {
data {
attributes {
name
}
}
}
author {
data {
attributes {
name
email
}
}
}
tags {
data {
attributes {
name
}
}
}
}
}
`;
export const GET_ARTICLE_FOR_TRACKING = gql`
${ARTICLE_TRACKING_FRAGMENT}
query GetArticle($slug: String!) {
articles(filters: { slug: { eq: $slug } }) {
data {
...ArticleTracking
}
}
}
`;
Usage:
// pages/articles/[slug].tsx
import { useQuery } from '@apollo/client';
import { GET_ARTICLE_FOR_TRACKING } from '@/lib/graphqlDataLayer';
export function ArticlePage({ slug }: { slug: string }) {
const { data } = useQuery(GET_ARTICLE_FOR_TRACKING, {
variables: { slug },
});
useEffect(() => {
if (data?.articles?.data?.[0]) {
const article = data.articles.data[0];
pushContentToDataLayer(buildContentDataLayer(article, 'article'));
}
}, [data]);
// ... rest of component
}
Webhook-Based Event Tracking for Content Changes
Track content changes server-side using Strapi webhooks.
Set Up Strapi Webhooks
1. Configure Webhook in Strapi Admin
Go to Settings → Webhooks → Create new webhook
- Name: GA4 Content Tracking
- URL:
https://your-site.com/api/webhook/strapi-tracking - Events: Select:
entry.createentry.updateentry.publishentry.unpublishentry.delete
2. Create Webhook Handler (Next.js)
// app/api/webhook/strapi-tracking/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const payload = await request.json();
// Verify webhook signature (recommended)
const signature = request.headers.get('x-strapi-signature');
if (!verifySignature(signature, payload)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
// Extract event data
const { event, model, entry } = payload;
// Send to GA4 Measurement Protocol
await sendToGA4({
name: mapStrapiEventToGA4(event),
params: {
content_type: model,
content_id: entry.id.toString(),
content_title: entry.title || entry.name,
event_source: 'strapi_webhook',
timestamp: new Date().toISOString(),
},
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 });
}
}
function mapStrapiEventToGA4(event: string): string {
const eventMap: Record<string, string> = {
'entry.create': 'content_created',
'entry.update': 'content_updated',
'entry.publish': 'content_published',
'entry.unpublish': 'content_unpublished',
'entry.delete': 'content_deleted',
};
return eventMap[event] || 'content_modified';
}
async function sendToGA4(event: { name: string; params: any }) {
const measurementId = process.env.GA_MEASUREMENT_ID!;
const apiSecret = process.env.GA_API_SECRET!;
const response = await fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`,
{
method: 'POST',
body: JSON.stringify({
client_id: 'strapi-webhook',
events: [event],
}),
}
);
if (!response.ok) {
throw new Error(`GA4 API error: ${response.statusText}`);
}
return response;
}
function verifySignature(signature: string | null, payload: any): boolean {
// Implement signature verification based on your security needs
// Example using HMAC:
const crypto = require('crypto');
const secret = process.env.STRAPI_WEBHOOK_SECRET!;
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
const expectedSignature = hmac.digest('hex');
return signature === expectedSignature;
}
3. Configure Environment Variables
# .env.local
GA_MEASUREMENT_ID=G-XXXXXXXXXX
GA_API_SECRET=your_api_secret_from_ga4
STRAPI_WEBHOOK_SECRET=your_webhook_secret
Get GA4 API Secret:
- Go to GA4 Admin → Data Streams
- Select your web stream
- Scroll to "Measurement Protocol API secrets"
- Click "Create" to generate a new secret
Advanced Webhook Tracking
Track Content Performance:
// app/api/webhook/content-performance/route.ts
export async function POST(request: NextRequest) {
const { event, entry } = await request.json();
if (event === 'entry.publish') {
// Track when content goes live
await sendToGA4({
name: 'content_published',
params: {
content_type: entry.__component || 'article',
content_id: entry.id.toString(),
publish_date: new Date().toISOString(),
author_id: entry.author?.id,
category: entry.category?.name,
word_count: countWords(entry.content),
},
});
}
return NextResponse.json({ success: true });
}
function countWords(content: string): number {
return content?.split(/\s+/).length || 0;
}
Track Editorial Workflow:
// Track content through editorial stages
export async function POST(request: NextRequest) {
const { event, entry } = await request.json();
const workflowStages: Record<string, string> = {
'entry.create': 'draft_created',
'entry.update': 'draft_revised',
'entry.publish': 'published',
'entry.unpublish': 'unpublished',
};
await sendToGA4({
name: 'editorial_workflow',
params: {
workflow_stage: workflowStages[event],
content_id: entry.id.toString(),
content_type: entry.__component,
time_to_publish: calculateTimeToPublish(entry),
},
});
return NextResponse.json({ success: true });
}
REST API Webhook Implementation
For non-Next.js setups:
// Node.js/Express example
const express = require('express');
const app = express();
app.post('/webhook/strapi-tracking', async (req, res) => {
const { event, model, entry } = req.body;
try {
// Send to GA4
await fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${process.env.GA_MEASUREMENT_ID}&api_secret=${process.env.GA_API_SECRET}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: 'strapi-server',
events: [{
name: event.replace('.', '_'),
params: {
content_type: model,
content_id: entry.id,
},
}],
}),
}
);
res.json({ success: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Failed to process webhook' });
}
});
Webhook Security Best Practices
1. Verify Webhook Source
// Verify requests come from your Strapi instance
function verifyWebhookSource(request: NextRequest): boolean {
const allowedIPs = process.env.STRAPI_SERVER_IPS?.split(',') || [];
const clientIP = request.headers.get('x-forwarded-for')?.split(',')[0];
return allowedIPs.includes(clientIP || '');
}
2. Use Webhook Signatures
// Add signature to Strapi webhook
// In Strapi: plugins/strapi-plugin-webhooks/server/services/webhook.js
const crypto = require('crypto');
function signPayload(payload) {
const secret = process.env.WEBHOOK_SECRET;
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
return hmac.digest('hex');
}
3. Rate Limiting
// Implement rate limiting for webhook endpoints
import rateLimit from 'express-rate-limit';
const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many webhook requests',
});
app.use('/api/webhook', webhookLimiter);
Environment Variable Configuration
Comprehensive environment variable setup for all frameworks.
Next.js Environment Variables
# .env.local (Development)
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_GTM_ID=GTM-XXXXXX
GA_API_SECRET=your_api_secret
STRAPI_API_URL=http://localhost:1337
STRAPI_TOKEN=your_strapi_api_token
STRAPI_WEBHOOK_SECRET=your_webhook_secret
# .env.production (Production)
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_GTM_ID=GTM-XXXXXX
GA_API_SECRET=your_production_api_secret
STRAPI_API_URL=https://api.yoursite.com
STRAPI_TOKEN=your_production_strapi_token
STRAPI_WEBHOOK_SECRET=your_production_webhook_secret
Access in code:
// Client-side (NEXT_PUBLIC_ prefix required)
const GA_ID = process.env.NEXT_PUBLIC_GA_ID;
// Server-side (no prefix needed)
const API_SECRET = process.env.GA_API_SECRET;
Gatsby Environment Variables
# .env.development
GA_MEASUREMENT_ID=G-XXXXXXXXXX
GTM_ID=GTM-XXXXXX
STRAPI_API_URL=http://localhost:1337
STRAPI_TOKEN=your_token
# .env.production
GA_MEASUREMENT_ID=G-XXXXXXXXXX
GTM_ID=GTM-XXXXXX
STRAPI_API_URL=https://api.yoursite.com
STRAPI_TOKEN=your_production_token
Load in gatsby-config.js:
require('dotenv').config({
path: `.env.${process.env.NODE_ENV}`,
});
module.exports = {
plugins: [
{
resolve: 'gatsby-plugin-google-gtag',
options: {
trackingIds: [process.env.GA_MEASUREMENT_ID],
},
},
{
resolve: 'gatsby-source-strapi',
options: {
apiURL: process.env.STRAPI_API_URL,
accessToken: process.env.STRAPI_TOKEN,
},
},
],
};
Nuxt Environment Variables
# .env
GA_MEASUREMENT_ID=G-XXXXXXXXXX
GTM_ID=GTM-XXXXXX
STRAPI_URL=http://localhost:1337
STRAPI_TOKEN=your_token
Access in nuxt.config.ts:
export default defineNuxtConfig({
runtimeConfig: {
// Private keys (server-side only)
strapiToken: process.env.STRAPI_TOKEN,
public: {
// Public keys (client-side accessible)
gaId: process.env.GA_MEASUREMENT_ID,
gtmId: process.env.GTM_ID,
strapiUrl: process.env.STRAPI_URL,
},
},
});
React/Vue SPA Environment Variables
# .env (React - must start with REACT_APP_)
REACT_APP_GA_ID=G-XXXXXXXXXX
REACT_APP_GTM_ID=GTM-XXXXXX
REACT_APP_STRAPI_URL=http://localhost:1337
# .env (Vite/Vue - must start with VITE_)
VITE_GA_ID=G-XXXXXXXXXX
VITE_GTM_ID=GTM-XXXXXX
VITE_STRAPI_URL=http://localhost:1337
Environment-Specific Configuration
Disable tracking in development:
// lib/analytics.ts
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
export function initializeAnalytics() {
if (!isProduction) {
console.log('Analytics disabled in development');
return;
}
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('config', process.env.NEXT_PUBLIC_GA_ID!, {
debug_mode: isDevelopment,
send_page_view: true,
});
}
}
Staging environment configuration:
# .env.staging
NEXT_PUBLIC_GA_ID=G-STAGING-ID
NEXT_PUBLIC_GTM_ID=GTM-STAGING
STRAPI_API_URL=https://staging-api.yoursite.com
NODE_ENV=staging
Security Best Practices
1. Never expose API secrets client-side:
// BAD - Exposes secret to client
const apiSecret = process.env.NEXT_PUBLIC_GA_API_SECRET;
// GOOD - Server-side only
// app/api/track/route.ts
const apiSecret = process.env.GA_API_SECRET;
2. Use different credentials per environment:
# Development
STRAPI_TOKEN=dev_token_12345
# Staging
STRAPI_TOKEN=staging_token_67890
# Production
STRAPI_TOKEN=prod_token_abcde
3. Validate environment variables at build:
// lib/env.ts
const requiredEnvVars = [
'NEXT_PUBLIC_GA_ID',
'NEXT_PUBLIC_GTM_ID',
'STRAPI_API_URL',
];
requiredEnvVars.forEach((envVar) => {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
});
Next Steps
- Configure GA4 Events - Track custom Strapi events
- Install GTM - For easier tag management
- Set up Data Layer - Structure your tracking data
- Troubleshoot Events - Fix tracking issues
For general GA4 concepts, see Google Analytics 4 Guide.