Since Directus is a headless data platform, Google Analytics 4 is installed on your frontend application (Next.js, Vue, React, etc.), not in Directus itself. This guide covers GA4 installation for common frontend frameworks used with Directus.
Important: Frontend vs Backend
Remember:
- Install GA4 in your frontend application (Next.js, Vue, React, etc.)
- Do NOT try to install GA4 in Directus admin panel
- Directus provides data 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 + Directus
Next.js is a popular framework for Directus-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';
import { Suspense } from 'react';
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 Directus Content Views
// pages/articles/[slug].js
import { useEffect } from 'react';
import * as gtag from '@/lib/analytics';
import { directus } from '@/lib/directus';
export default function Article({ article }) {
useEffect(() => {
// Track article view with Directus metadata
gtag.event({
action: 'view_content',
category: 'Article',
label: article.title,
value: article.id,
});
}, [article]);
return (
<article>
<h1>{article.title}</h1>
{/* Article content */}
</article>
);
}
export async function getStaticProps({ params }) {
const { data } = await directus.items('articles').readByQuery({
filter: { slug: { _eq: params.slug } },
fields: ['*'],
});
return {
props: { article: data[0] },
revalidate: 60,
};
}
Environment Variables
# .env.local
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
DIRECTUS_URL=http://localhost:8055
DIRECTUS_TOKEN=your-api-token
Method 2: Vue/Nuxt + Directus
Vue and Nuxt are popular choices for Directus frontends.
Nuxt 3 Installation
npm install @nuxtjs/google-analytics
Configuration
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/google-analytics'],
googleAnalytics: {
id: process.env.GA_MEASUREMENT_ID
},
runtimeConfig: {
public: {
directusUrl: process.env.DIRECTUS_URL || 'http://localhost:8055',
},
},
});
Track Directus Content
<!-- pages/articles/[slug].vue -->
<script setup>
const route = useRoute();
const config = useRuntimeConfig();
// Fetch Directus content
const { data: article } = await useFetch(`${config.public.directusUrl}/items/articles`, {
params: {
filter: { slug: { _eq: route.params.slug } }
}
});
onMounted(() => {
// Track article view
if (process.client && window.gtag && article.value) {
window.gtag('event', 'view_content', {
content_type: 'article',
content_id: article.value.data[0].id,
content_title: article.value.data[0].title,
});
}
});
</script>
<template>
<article v-if="article">
<h1>{{ article.data[0].title }}</h1>
<!-- Article content -->
</article>
</template>
Method 3: React SPA + Directus
For React single-page applications with Directus.
Installation
npm install react-ga4
npm install @directus/sdk
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 Directus Content
// src/components/Article.js
import { useEffect, useState } from 'react';
import { logEvent } from '../analytics';
import { createDirectus, rest, readItem } from '@directus/sdk';
const directus = createDirectus(process.env.REACT_APP_DIRECTUS_URL).with(rest());
function Article({ articleId }) {
const [article, setArticle] = useState(null);
useEffect(() => {
async function fetchArticle() {
const data = await directus.request(readItem('articles', articleId));
setArticle(data);
}
fetchArticle();
}, [articleId]);
useEffect(() => {
if (article) {
logEvent('Content', 'View Article', article.title);
}
}, [article]);
return (
<article>
{article && (
<>
<h1>{article.title}</h1>
{/* Article content */}
</>
)}
</article>
);
}
Method 4: Google Tag Manager (Recommended)
GTM provides the easiest management across all frameworks.
See Install Google Tag Manager on Directus 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 Directus-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 Content Types
Test across your Directus collections:
- Articles (collection)
- Pages (collection)
- Products (collection)
- Dynamic routes
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:
Next Steps
- Configure GA4 Events - Track custom Directus events
- Install GTM - For easier tag management
- Set up Data Layer - Structure your tracking data
For general GA4 concepts, see Google Analytics 4 Guide.