Installing Google Analytics 4 on PayloadCMS | OpsBlu Docs

Installing Google Analytics 4 on PayloadCMS

Complete guide to setting up GA4 on PayloadCMS frontends using React/Next.js integration with proper server-side rendering support.

PayloadCMS is a headless CMS, so Google Analytics 4 integration happens on your frontend application (typically React, Next.js, or custom). This guide covers GA4 implementation for common PayloadCMS frontend frameworks with consideration for server-side rendering.


Prerequisites

Before you begin:

  • Have a Google Analytics 4 property created in your Google Analytics account
  • Know your GA4 Measurement ID (format: G-XXXXXXXXXX)
  • Have access to your PayloadCMS frontend application code
  • Understand your frontend framework (Next.js, React, etc.)

Understanding PayloadCMS Architecture

PayloadCMS operates as:

  • Backend: Node.js/Express API serving content
  • Frontend: Separate application (React, Next.js, Vue, etc.)
  • Admin Panel: Built-in React admin interface

Where to Add GA4

You'll add GA4 tracking to:

  1. Your public-facing frontend (primary)
  2. Payload Admin Panel (optional, for internal analytics)

Most PayloadCMS projects use Next.js for the frontend.

Step 1: Install GA4 Package

npm install react-ga4
# or
yarn add react-ga4

Step 2: Create GA4 Configuration

File: lib/gtag.js or utils/analytics.js

export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_ID;

// Initialize GA4
export const initGA = () => {
  if (typeof window !== 'undefined' && GA_MEASUREMENT_ID) {
    window.dataLayer = window.dataLayer || [];
    function gtag() {
      window.dataLayer.push(arguments);
    }
    gtag('js', new Date());
    gtag('config', GA_MEASUREMENT_ID, {
      page_path: window.location.pathname,
    });
  }
};

// Log page views
export const pageview = (url) => {
  if (typeof window !== 'undefined' && window.gtag) {
    window.gtag('config', GA_MEASUREMENT_ID, {
      page_path: url,
    });
  }
};

// Log events
export const event = ({ action, category, label, value }) => {
  if (typeof window !== 'undefined' && window.gtag) {
    window.gtag('event', action, {
      event_category: category,
      event_label: label,
      value: value,
    });
  }
};

Step 3: Add Script to _app.js

File: pages/_app.js (Pages Router) or app/layout.js (App Router)

Pages Router (_app.js):

import { useEffect } from 'react';
import { useRouter } from 'next/router';
import Script from 'next/script';
import * as gtag from '../lib/gtag';

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_MEASUREMENT_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_MEASUREMENT_ID}', {
              page_path: window.location.pathname,
            });
          `,
        }}
      />
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

App Router (app/layout.js):

import Script from 'next/script';

export default function RootLayout({ children }) {
  const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_ID;

  return (
    <html lang="en">
      <head>
        <Script
          strategy="afterInteractive"
          src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
        />
        <Script
          id="gtag-init"
          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,
              });
            `,
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Step 4: Add Environment Variable

File: .env.local

NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX

Replace G-XXXXXXXXXX with your actual Measurement ID.

Step 5: Track Route Changes (App Router)

File: app/components/Analytics.js

'use client';

import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import * as gtag from '@/lib/gtag';

export default function Analytics() {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    const url = pathname + searchParams.toString();
    gtag.pageview(url);
  }, [pathname, searchParams]);

  return null;
}

Add to layout.js:

import Analytics from './components/Analytics';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Analytics />
        {children}
      </body>
    </html>
  );
}

Method 2: React (Create React App) Integration

For standard React applications without Next.js.

Step 1: Install react-ga4

npm install react-ga4

Step 2: Initialize in App.js

File: src/App.js

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import ReactGA from 'react-ga4';

const GA_MEASUREMENT_ID = process.env.REACT_APP_GA_ID;

function App() {
  const location = useLocation();

  useEffect(() => {
    // Initialize GA4
    ReactGA.initialize(GA_MEASUREMENT_ID);
  }, []);

  useEffect(() => {
    // Track page views on route change
    ReactGA.send({ hitType: 'pageview', page: location.pathname });
  }, [location]);

  return (
    <div className="App">
      {/* Your app components */}
    </div>
  );
}

export default App;

Step 3: Add Environment Variable

File: .env

REACT_APP_GA_ID=G-XXXXXXXXXX

Method 3: PayloadCMS Admin Panel Integration

Track usage of your Payload admin panel.

Step 1: Create Custom Script Component

File: src/components/Analytics.tsx (in your Payload config directory)

import React, { useEffect } from 'react';

const GA_MEASUREMENT_ID = process.env.PAYLOAD_PUBLIC_GA_ID;

export const Analytics: React.FC = () => {
  useEffect(() => {
    if (typeof window !== 'undefined' && GA_MEASUREMENT_ID) {
      // Load gtag script
      const script = document.createElement('script');
      script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`;
      script.async = true;
      document.head.appendChild(script);

      // Initialize gtag
      window.dataLayer = window.dataLayer || [];
      function gtag() {
        window.dataLayer.push(arguments);
      }
      window.gtag = gtag;
      gtag('js', new Date());
      gtag('config', GA_MEASUREMENT_ID);
    }
  }, []);

  return null;
};

Step 2: Add to Payload Config

File: payload.config.ts

import { buildConfig } from 'payload/config';
import { Analytics } from './components/Analytics';

export default buildConfig({
  // ... other config
  admin: {
    components: {
      afterNavLinks: [Analytics],
    },
  },
  // ... rest of config
});

Method 4: Server-Side Tracking (Advanced)

For tracking API requests or server-side events.

Step 1: Install Measurement Protocol Library

npm install axios

Step 2: Create Server-Side Tracker

File: lib/server-analytics.js

const axios = require('axios');

const GA_MEASUREMENT_ID = process.env.GA_MEASUREMENT_ID;
const GA_API_SECRET = process.env.GA_API_SECRET;

async function trackServerEvent({
  clientId,
  eventName,
  eventParams = {}
}) {
  try {
    await axios.post(
      `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`,
      {
        client_id: clientId,
        events: [
          {
            name: eventName,
            params: eventParams,
          },
        ],
      }
    );
  } catch (error) {
    console.error('GA4 server tracking error:', error);
  }
}

module.exports = { trackServerEvent };

Step 3: Use in Payload Hooks

File: collections/Posts.ts

import { CollectionConfig } from 'payload/types';
import { trackServerEvent } from '../lib/server-analytics';

const Posts: CollectionConfig = {
  slug: 'posts',
  hooks: {
    afterChange: [
      async ({ doc, operation }) => {
        if (operation === 'create') {
          await trackServerEvent({
            clientId: 'server',
            eventName: 'post_created',
            eventParams: {
              post_id: doc.id,
              post_title: doc.title,
            },
          });
        }
      },
    ],
  },
  // ... rest of collection config
};

export default Posts;

Tracking PayloadCMS Content

Track Content Fetching

Track when content is fetched from Payload API:

// In your frontend component
import { event } from '../lib/gtag';

async function fetchPost(slug) {
  const response = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/posts?where[slug][equals]=${slug}`);
  const data = await response.json();

  // Track content view
  if (data.docs[0]) {
    event({
      action: 'view_content',
      category: 'Posts',
      label: data.docs[0].title,
      value: data.docs[0].id,
    });
  }

  return data;
}

Track Dynamic Routes

For dynamic content pages:

// pages/posts/[slug].js
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import * as gtag from '../../lib/gtag';

export default function Post({ post }) {
  const router = useRouter();

  useEffect(() => {
    // Track content type
    gtag.event({
      action: 'page_view',
      category: 'Content',
      label: `Post: ${post.title}`,
    });
  }, [post]);

  return <div>{/* Post content */}</div>;
}

export async function getStaticProps({ params }) {
  const res = await fetch(
    `${process.env.PAYLOAD_URL}/api/posts?where[slug][equals]=${params.slug}`
  );
  const data = await res.json();

  return {
    props: {
      post: data.docs[0],
    },
  };
}

Verification & Testing

Step 1: Check Browser Console

// Open browser console and run:
window.dataLayer

// Should return array with GA4 events

Step 2: Use Google Tag Assistant

  1. Install Google Tag Assistant Chrome Extension
  2. Visit your PayloadCMS frontend
  3. Click extension icon
  4. Verify GA4 tag is detected and firing

Step 3: Check GA4 DebugView

  1. Enable debug mode in development:
gtag('config', GA_MEASUREMENT_ID, {
  debug_mode: true,
});
  1. In GA4, go to Configure > DebugView
  2. Visit your site
  3. Verify events appear in DebugView

Step 4: Real-Time Reports

  1. In GA4, navigate to Reports > Realtime
  2. Visit your PayloadCMS frontend
  3. Confirm page views appear within 30 seconds

Environment-Specific Configuration

Development vs Production

// lib/gtag.js
export const GA_MEASUREMENT_ID =
  process.env.NODE_ENV === 'production'
    ? process.env.NEXT_PUBLIC_GA_ID_PROD
    : process.env.NEXT_PUBLIC_GA_ID_DEV;

export const initGA = () => {
  // Don't load GA in development unless explicitly enabled
  if (process.env.NODE_ENV === 'development' && !process.env.NEXT_PUBLIC_GA_DEV_MODE) {
    console.log('GA4 disabled in development');
    return;
  }

  if (typeof window !== 'undefined' && GA_MEASUREMENT_ID) {
    // ... initialization code
  }
};

.env.local:

NEXT_PUBLIC_GA_ID_PROD=G-XXXXXXXXXX
NEXT_PUBLIC_GA_ID_DEV=G-YYYYYYYYYY
NEXT_PUBLIC_GA_DEV_MODE=false

Privacy & Compliance

// lib/consent.js
export const hasConsent = () => {
  return localStorage.getItem('cookie_consent') === 'granted';
};

export const grantConsent = () => {
  localStorage.setItem('cookie_consent', 'granted');
  if (window.gtag) {
    window.gtag('consent', 'update', {
      analytics_storage: 'granted',
    });
  }
};

export const revokeConsent = () => {
  localStorage.setItem('cookie_consent', 'denied');
  if (window.gtag) {
    window.gtag('consent', 'update', {
      analytics_storage: 'denied',
    });
  }
};

Initialize with default consent:

// In _app.js or layout.js, before GA4 script
gtag('consent', 'default', {
  analytics_storage: 'denied',
  wait_for_update: 500,
});

Common Issues & Solutions

Issue: GA4 Not Loading in Production

Cause: Environment variable not set in production build

Solution:

  1. Verify .env.production has NEXT_PUBLIC_GA_ID
  2. Rebuild and redeploy: npm run build
  3. Check Vercel/Netlify environment variables

Issue: Duplicate Page Views

Cause: Multiple tracking implementations or router events firing incorrectly

Solution:

  1. Remove duplicate GA4 scripts
  2. Ensure page view tracking only in one location
  3. Use strategy="afterInteractive" for Script component

Issue: Server-Side Rendering Errors

Cause: Accessing window during SSR

Solution:

  • Always check typeof window !== 'undefined'
  • Use useEffect for client-side only code
  • Use Next.js Script component with proper strategy

Performance Optimization

Lazy Load Analytics

// Only load GA4 after user interaction
import { useState, useEffect } from 'react';

export default function App() {
  const [analyticsLoaded, setAnalyticsLoaded] = useState(false);

  useEffect(() => {
    // Load GA4 after first interaction
    const loadAnalytics = () => {
      if (!analyticsLoaded) {
        setAnalyticsLoaded(true);
        // Initialize GA4 here
      }
    };

    window.addEventListener('scroll', loadAnalytics, { once: true });
    window.addEventListener('click', loadAnalytics, { once: true });

    return () => {
      window.removeEventListener('scroll', loadAnalytics);
      window.removeEventListener('click', loadAnalytics);
    };
  }, [analyticsLoaded]);

  return <div>{/* App content */}</div>;
}

Next Steps


Additional Resources