Google Analytics 4 Setup on Craft CMS | OpsBlu Docs

Google Analytics 4 Setup on Craft CMS

Complete guide to implementing Google Analytics 4 (GA4) on Craft CMS using Twig templates, plugins, and environment configuration.

This guide covers multiple methods for implementing Google Analytics 4 (GA4) on your Craft CMS website, from simple template integration to advanced plugin configurations.

Prerequisites

  • Active Google Analytics 4 property
  • GA4 Measurement ID (format: G-XXXXXXXXXX)
  • Craft CMS 4.x or 5.x installation
  • Basic understanding of Twig templating

Method 1: Direct Twig Template Integration

Step 1: Configure Environment Variables

Add your GA4 Measurement ID to your .env file:

# .env
GOOGLE_ANALYTICS_ID="G-XXXXXXXXXX"
ENVIRONMENT="production"

Step 2: Create Analytics Partial Template

Create a reusable partial at templates/_analytics/google-analytics.twig:

{# templates/_analytics/google-analytics.twig #}

{% set analyticsId = getenv('GOOGLE_ANALYTICS_ID') %}
{% set environment = craft.app.config.general.environment %}

{# Only load in production and if ID is configured #}
{% if analyticsId and environment == 'production' and not craft.app.request.isLivePreview %}

<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ analyticsId }}"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', '{{ analyticsId }}', {
    'send_page_view': true,
    {% if currentUser is defined and currentUser %}
    'user_id': '{{ currentUser.id }}',
    {% endif %}
    'custom_map': {
      'dimension1': 'entry_type',
      'dimension2': 'section',
      'dimension3': 'author'
    }
  });

  {% if entry is defined and entry %}
  {# Track entry metadata as custom dimensions #}
  gtag('set', {
    'entry_type': '{{ entry.type.handle }}',
    'section': '{{ entry.section.handle }}',
    'author': '{{ entry.author.fullName }}'
  });
  {% endif %}
</script>

{% elseif craft.app.config.general.devMode %}
<!-- Google Analytics disabled in dev mode -->
{% endif %}

Step 3: Include in Base Layout

Add the partial to your main layout template:

{# templates/_layouts/base.twig #}
<!DOCTYPE html>
<html lang="{{ currentSite.language }}">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title ?? siteName }}</title>

    {# Google Analytics - Load in <head> for accurate tracking #}
    {{ include('_analytics/google-analytics') }}

    {# Other head content #}
    {{ head() }}
</head>
<body>
    {{ beginBody() }}

    {# Main content #}
    {% block content %}{% endblock %}

    {{ endBody() }}
</body>
</html>

Method 2: Using SEOmatic Plugin

Step 1: Install SEOmatic

cd /path/to/craft-project
composer require nystudio107/craft-seomatic

Step 2: Configure in Control Panel

  1. Navigate to SEOmatic → Tracking Scripts
  2. Click Google Analytics
  3. Enter your GA4 Measurement ID in the Google Analytics Tracking ID field
  4. Configure settings:

Step 3: Configure Environment-Specific Settings

Create config/seomatic.php:

<?php

use craft\helpers\App;

return [
    '*' => [
        'pluginName' => 'SEOmatic',
        'renderEnabled' => true,
        'environment' => App::env('ENVIRONMENT') ?: 'production',
    ],
    'production' => [
        'renderEnabled' => true,
    ],
    'staging' => [
        'renderEnabled' => false,
    ],
    'dev' => [
        'renderEnabled' => false,
    ],
];

Step 4: Advanced Configuration via Plugin Settings

For programmatic control, use SEOmatic's PHP configuration:

// config/seomatic.php - Google Analytics configuration
return [
    '*' => [
        'googleAnalytics' => [
            'id' => App::env('GOOGLE_ANALYTICS_ID'),
            'sendPageView' => true,
            'ipAnonymization' => true,
            'demographics' => false,
            'enhancedLinkAttribution' => true,
            'enhancedEcommerce' => true,
        ],
    ],
];

Step 5: Template-Level Customization

Override SEOmatic settings in templates:

{% do seomatic.script.get('googleAnalytics').include(true) %}
{% do seomatic.script.get('googleAnalytics').config({
    'cookieDomain': 'auto',
    'anonymizeIp': true,
}) %}

Method 3: Custom Module Implementation

For advanced requirements, create a custom Craft module:

Step 1: Create Analytics Module

mkdir -p modules/analytics

Create modules/analytics/Module.php:

<?php

namespace modules\analytics;

use Craft;
use craft\events\TemplateEvent;
use craft\web\View;
use yii\base\Event;
use yii\base\Module as BaseModule;

class Module extends BaseModule
{
    public static $instance;

    public function init()
    {
        parent::init();
        self::$instance = $this;

        // Register analytics script injection
        Event::on(
            View::class,
            View::EVENT_END_HEAD,
            [$this, 'injectAnalyticsScript']
        );
    }

    public function injectAnalyticsScript(TemplateEvent $event)
    {
        // Only inject in production
        if (Craft::$app->config->general->environment !== 'production') {
            return;
        }

        // Skip during Live Preview
        if (Craft::$app->request->isLivePreview) {
            return;
        }

        $analyticsId = App::env('GOOGLE_ANALYTICS_ID');
        if (!$analyticsId) {
            return;
        }

        $script = $this->renderAnalyticsScript($analyticsId);
        $event->output = str_replace('</head>', $script . '</head>', $event->output);
    }

    private function renderAnalyticsScript(string $analyticsId): string
    {
        $currentUser = Craft::$app->user->identity;
        $userId = $currentUser ? $currentUser->id : null;

        return <<<HTML
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id={$analyticsId}"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', '{$analyticsId}', {
    'send_page_view': true
    {$userId ? ",'user_id': '{$userId}'" : ''}
  });
</script>
HTML;
    }
}

Step 2: Bootstrap the Module

Edit config/app.php:

<?php

use craft\helpers\App;

return [
    'modules' => [
        'analytics' => [
            'class' => \modules\analytics\Module::class,
        ],
    ],
    'bootstrap' => ['analytics'],
];

Multi-Site Configuration

For Craft CMS multi-site installations:

{# templates/_analytics/google-analytics.twig #}

{% set analyticsIds = {
    'siteHandleOne': getenv('GA_SITE_ONE'),
    'siteHandleTwo': getenv('GA_SITE_TWO'),
    'siteHandleThree': getenv('GA_SITE_THREE'),
} %}

{% set currentAnalyticsId = analyticsIds[currentSite.handle] ?? null %}

{% if currentAnalyticsId and craft.app.config.general.environment == 'production' %}
<script async src="https://www.googletagmanager.com/gtag/js?id={{ currentAnalyticsId }}"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', '{{ currentAnalyticsId }}', {
    'site_name': '{{ currentSite.name }}',
    'site_handle': '{{ currentSite.handle }}'
  });
</script>
{% endif %}

Performance Optimization

DNS Prefetch and Preconnect

Add resource hints to improve loading performance:

{# In <head> section #}
<link rel="dns-prefetch" href="//www.google-analytics.com">
<link rel="preconnect" href="https://www.google-analytics.com" crossorigin>
<link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>

Lazy Loading for Non-Critical Pages

Delay loading on specific page types:

{% if entry.section.handle == 'blog' %}
    {# Immediate loading for blog posts #}
    {{ include('_analytics/google-analytics') }}
{% else %}
    {# Defer loading on other pages #}
    <script>
        window.addEventListener('load', function() {
            // Load GA4 after page load
            var script = document.createElement('script');
            script.src = 'https://www.googletagmanager.com/gtag/js?id={{ analyticsId }}';
            script.async = true;
            document.head.appendChild(script);
        });
    </script>
{% endif %}

User Privacy and GDPR Compliance

Implement conditional loading based on consent:

{# Check for cookie consent #}
{% set analyticsConsent = craft.cookies.get('cookie_consent_analytics') %}

{% if analyticsConsent == 'accepted' %}
    {{ include('_analytics/google-analytics') }}
{% else %}
    {# Display cookie consent banner #}
    {{ include('_components/cookie-consent-banner') }}
{% endif %}

IP Anonymization

Enable IP anonymization in your GA4 configuration:

<script>
  gtag('config', '{{ analyticsId }}', {
    'anonymize_ip': true,
    'allow_google_signals': false,
    'allow_ad_personalization_signals': false
  });
</script>

User ID Tracking (Optional)

Track logged-in users while respecting privacy:

{% if currentUser and not currentUser.isInGroup('admins') %}
<script>
  gtag('config', '{{ analyticsId }}', {
    'user_id': '{{ currentUser.id|hash('sha256') }}' // Hashed for privacy
  });
</script>
{% endif %}

Testing and Validation

Test in Development

Enable debug mode for local testing:

{% if craft.app.config.general.devMode %}
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', '{{ analyticsId }}', {
    'debug_mode': true
  });
  console.log('GA4 Debug Mode Enabled');
</script>
{% endif %}

Verify Installation

  1. Browser Console: Check for dataLayer array
  2. Network Tab: Verify requests to google-analytics.com
  3. GA4 DebugView: Use DebugView in GA4 interface
  4. Real-time Reports: Check GA4 real-time data

Tag Assistant

Install Google Tag Assistant Chrome extension and verify:

  • Tag is firing correctly
  • Measurement ID matches your property
  • Page views are being recorded

Common Issues and Solutions

Analytics Not Loading

{# Add debug output #}
{% set analyticsId = getenv('GOOGLE_ANALYTICS_ID') %}
{% set environment = craft.app.config.general.environment %}

<!-- Debug Info (remove in production) -->
<!-- Analytics ID: {{ analyticsId ? 'Set' : 'NOT SET' }} -->
<!-- Environment: {{ environment }} -->
<!-- Live Preview: {{ craft.app.request.isLivePreview ? 'Yes' : 'No' }} -->

Content Security Policy (CSP) Blocking

Configure CSP headers in config/general.php:

return [
    '*' => [
        'securityHeaders' => [
            'Content-Security-Policy' => implode('; ', [
                "default-src 'self'",
                "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com",
                "img-src 'self' data: https://www.google-analytics.com",
                "connect-src 'self' https://www.google-analytics.com https://analytics.google.com",
            ]),
        ],
    ],
];

Next Steps

GraphQL API Integration

For headless Craft CMS, expose analytics configuration via GraphQL:

# In your schema
type GlobalSet_SiteSettings_GlobalSet {
  googleAnalyticsId: String
  enableAnalytics: Boolean
}

Query from your frontend:

query {
  globalSet(handle: "siteSettings") {
    ... on siteSettings_GlobalSet {
      googleAnalyticsId
      enableAnalytics
    }
  }
}

Resources