Install Google Analytics 4 on Directus Sites | OpsBlu Docs

Install Google Analytics 4 on Directus Sites

How to install GA4 on Directus-powered sites using Next.js, Vue, React, and other frontend frameworks for headless data platform integration.

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

  1. Create a GA4 Property

    • Go to Google Analytics
    • Create a new GA4 property
    • Note your Measurement ID (format: G-XXXXXXXXXX)
  2. 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>
  );
}

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 → ReportsRealtime
  • 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 AdminDebugView 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

For general GA4 concepts, see Google Analytics 4 Guide.