Install Google Analytics 4 on Grav Sites | OpsBlu Docs

Install Google Analytics 4 on Grav Sites

How to install GA4 on Grav flat-file CMS using plugins, Twig templates, and custom theme integration.

Grav is a flat-file CMS built with PHP and Twig templates. This guide covers multiple methods to install Google Analytics 4 on your Grav site, from simple plugins to custom template integration.

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

    • Grav Plugin: Easiest, no code required
    • Twig Template: More control, theme-level integration
    • Google Tag Manager: Recommended for advanced tracking
  3. Admin Access Required

    • You'll need access to Grav Admin Panel or file system
    • FTP/SSH access for manual plugin installation

The simplest method uses the official Grav Google Analytics plugin.

Install Plugin via Admin Panel

  1. Navigate to Admin Panel

    • Go to https://yoursite.com/admin
    • Log in with your credentials
  2. Install Plugin

    • Click Plugins in sidebar
    • Click + Add button
    • Search for "Google Analytics"
    • Click Install on "Google Analytics" plugin
  3. Configure Plugin

    • Go to PluginsGoogle Analytics
    • Enter your Tracking ID: G-XXXXXXXXXX
    • Enable Status: Toggle to ON
    • Configure options:
      • Position: head or body (recommended: head)
      • Async Loading: Enable (recommended)
      • Anonymize IP: Enable if required by GDPR
      • Debug Mode: Enable for testing
    • Click Save

Install Plugin via GPM (Command Line)

# SSH into your server
cd /path/to/grav

# Install via Grav Package Manager
bin/gpm install google-analytics

# Or install manually
cd user/plugins
git clone https://github.com/escopecz/grav-ganalytics.git google-analytics

Manual Configuration

Edit the plugin configuration file:

# user/config/plugins/google-analytics.yaml
enabled: true
trackingId: 'G-XXXXXXXXXX'
position: 'head'
async: true
anonymizeIp: true
debugMode: false
objectName: 'ga'
forceSsl: true
blockedIps: []
blockedPaths: []

Plugin Features

The Google Analytics plugin provides:

  • Automatic page view tracking
  • IP anonymization for GDPR compliance
  • Block specific IPs from tracking (admin IPs)
  • Block specific paths from tracking
  • Debug mode for testing
  • Async/defer script loading

Method 2: Twig Template Integration

For complete control, add GA4 directly to your theme templates.

Add to Base Template

{# user/themes/your-theme/templates/partials/base.html.twig #}

<!DOCTYPE html>
<html lang="{{ grav.language.getActive ?: 'en' }}">
<head>
    {% block head %}
        <meta charset="utf-8" />
        <title>{% if page.title %}{{ page.title }} | {% endif %}{{ site.title }}</title>

        {# Google Analytics 4 #}
        {% if not grav.config.plugins['google-analytics'].enabled %}
            <!-- Google tag (gtag.js) -->
            <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
            <script>
                window.dataLayer = window.dataLayer || [];
                function gtag(){dataLayer.push(arguments);}
                gtag('js', new Date());
                gtag('config', 'G-XXXXXXXXXX', {
                    'anonymize_ip': true,
                    'cookie_flags': 'SameSite=None;Secure'
                });
            </script>
        {% endif %}

        {{ assets.css()|raw }}
    {% endblock %}
</head>
<body>
    {% block content %}{% endblock %}
    {{ assets.js()|raw }}
</body>
</html>

Add to Header Partial

If your theme uses a header partial:

{# user/themes/your-theme/templates/partials/head.html.twig #}

<meta charset="utf-8" />
<title>{% if page.title %}{{ page.title|e('html') }} | {% endif %}{{ site.title|e('html') }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">

{# Google Analytics 4 #}
{% set ga_id = grav.config.site.google_analytics_id %}
{% if ga_id %}
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ ga_id }}"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', '{{ ga_id }}', {
    'anonymize_ip': {{ grav.config.site.ga_anonymize_ip ? 'true' : 'false' }},
    'cookie_domain': '{{ grav.uri.host() }}',
    'send_page_view': true
  });
</script>
{% endif %}

Configure in site.yaml

# user/config/site.yaml
title: My Grav Site
default_lang: en
google_analytics_id: 'G-XXXXXXXXXX'
ga_anonymize_ip: true

Method 3: Custom Plugin Development

Create your own minimal GA4 plugin:

Create Plugin Structure

# Create plugin directory
mkdir -p user/plugins/ga4-custom

# Create required files
touch user/plugins/ga4-custom/ga4-custom.php
touch user/plugins/ga4-custom/ga4-custom.yaml

Plugin Code

<?php
// user/plugins/ga4-custom/ga4-custom.php

namespace Grav\Plugin;

use Grav\Common\Plugin;
use Grav\Common\Page\Page;

class Ga4CustomPlugin extends Plugin
{
    public static function getSubscribedEvents()
    {
        return [
            'onPluginsInitialized' => ['onPluginsInitialized', 0]
        ];
    }

    public function onPluginsInitialized()
    {
        if ($this->isAdmin()) {
            return;
        }

        $this->enable([
            'onPageContentRaw' => ['onPageContentRaw', 0]
        ]);
    }

    public function onPageContentRaw()
    {
        $config = $this->config->get('plugins.ga4-custom');

        if (!$config['enabled'] || empty($config['tracking_id'])) {
            return;
        }

        $tracking_id = $config['tracking_id'];
        $anonymize_ip = $config['anonymize_ip'] ? 'true' : 'false';

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

        $this->grav['assets']->addInlineJs($ga4_script, ['position' => 'head']);
    }
}

Plugin Configuration

# user/plugins/ga4-custom/ga4-custom.yaml
enabled: true
tracking_id: 'G-XXXXXXXXXX'
anonymize_ip: true

Method 4: Page-Level Tracking

Add GA4 to specific pages using page frontmatter:

Page Frontmatter

---
title: Special Landing Page
google_analytics: true
ga_tracking_id: 'G-YYYYYYYYYY'
---

# Page content here

Template Logic

{# Check if page has custom GA ID #}
{% if page.header.google_analytics %}
    {% set ga_id = page.header.ga_tracking_id ?: grav.config.site.google_analytics_id %}
    {% if ga_id %}
    <!-- Google tag (gtag.js) -->
    <script async src="https://www.googletagmanager.com/gtag/js?id={{ ga_id }}"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
      gtag('config', '{{ ga_id }}');
    </script>
    {% endif %}
{% endif %}

Advanced Configuration

Track Custom Dimensions

<script>
  gtag('config', 'G-XXXXXXXXXX', {
    'custom_map': {
      'dimension1': 'page_type',
      'dimension2': 'author',
      'dimension3': 'category'
    }
  });

  gtag('event', 'page_view', {
    'page_type': '{{ page.template() }}',
    'author': '{{ page.header.author ?: "Unknown" }}',
    'category': '{{ page.taxonomy.category|first ?: "Uncategorized" }}'
  });
</script>

Enhanced Measurement Settings

gtag('config', 'G-XXXXXXXXXX', {
  'send_page_view': true,
  'anonymize_ip': true,
  'cookie_domain': 'auto',
  'cookie_flags': 'SameSite=None;Secure',
  'allow_google_signals': false,
  'allow_ad_personalization_signals': false,
  // Enhanced measurement
  'link_attribution': true,
  'linker': {
    'domains': ['example.com', 'subdomain.example.com']
  }
});

Track Modular Pages

{# For modular pages with multiple content sections #}
{% if page.modular %}
<script>
  gtag('event', 'modular_page_view', {
    'page_location': '{{ grav.uri.url(true) }}',
    'page_title': '{{ page.title }}',
    'module_count': {{ page.collection()|length }}
  });
</script>
{% endif %}

GDPR Compliance

{# Conditional loading based on consent #}
<script>
  // Check if user has consented
  function hasGAConsent() {
    return localStorage.getItem('ga_consent') === 'true';
  }

  // Load GA4 only if consent given
  if (hasGAConsent()) {
    var script = document.createElement('script');
    script.async = true;
    script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX';
    document.head.appendChild(script);

    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'G-XXXXXXXXXX', {
      'anonymize_ip': true
    });
  }
</script>

Opt-Out Functionality

<script>
  // GA4 opt-out
  var gaProperty = 'G-XXXXXXXXXX';
  var disableStr = 'ga-disable-' + gaProperty;

  if (document.cookie.indexOf(disableStr + '=true') > -1) {
    window[disableStr] = true;
  }

  function gaOptout() {
    document.cookie = disableStr + '=true; expires=Thu, 31 Dec 2099 23:59:59 UTC; path=/';
    window[disableStr] = true;
    alert('Google Analytics tracking has been disabled.');
  }
</script>

{# Opt-out link #}
<a href="javascript:gaOptout()">Disable Google Analytics</a>

Multi-Language Sites

Language-Specific Tracking

{% set active_lang = grav.language.getActive ?: grav.config.site.default_lang %}

<script>
  gtag('config', 'G-XXXXXXXXXX', {
    'language': '{{ active_lang }}',
    'content_group': 'lang_{{ active_lang }}'
  });
</script>

Multiple Properties for Different Languages

{% set ga_ids = {
  'en': 'G-XXXXXXXXXX',
  'es': 'G-YYYYYYYYYY',
  'fr': 'G-ZZZZZZZZZZ'
} %}

{% set current_ga_id = ga_ids[active_lang] ?: ga_ids['en'] %}

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

Verification & Testing

1. Check GA4 Realtime Reports

  • Open GA4 → ReportsRealtime
  • Navigate your Grav site
  • Verify page views 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. View Page Source

  • Right-click → View Page Source
  • Search for googletagmanager.com/gtag/js
  • Verify your Measurement ID appears

4. Use GA4 DebugView

Enable debug mode in plugin or add to template:

gtag('config', 'G-XXXXXXXXXX', {
  'debug_mode': true
});

Then check AdminDebugView in GA4.

5. Test Different Page Types

  • Homepage (default.html.twig)
  • Blog posts (item.html.twig)
  • Modular pages (modular.html.twig)
  • Custom templates

Common Issues

GA4 Not Loading

Possible Causes:

  • Plugin disabled
  • Incorrect Tracking ID
  • Ad blocker active
  • JavaScript errors in console

Solution:

  1. Check plugin status in Admin Panel
  2. Verify Tracking ID format: G-XXXXXXXXXX
  3. Check browser console for errors
  4. Test in incognito mode

Duplicate Tracking Codes

Cause: Both plugin and template include GA4.

Solution: Use conditional check:

{% if not grav.config.plugins['google-analytics'].enabled %}
    {# Your GA4 code here #}
{% endif %}

Tracking Not Working in Admin

Expected: GA4 should not track admin pages.

Solution: Most plugins exclude admin by default. To verify:

if ($this->isAdmin()) {
    return; // Don't track admin pages
}

Cache Issues

Symptom: Changes not appearing.

Solution:

# Clear Grav cache
bin/grav clear-cache

# Or via Admin Panel
Admin → Configuration → Clear Cache

Performance Optimization

Defer Loading

<script defer src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>

Conditional Loading

{# Only load on production #}
{% if grav.config.system.environment == 'production' %}
    {# GA4 code here #}
{% endif %}

Exclude Admin IPs

# user/config/plugins/google-analytics.yaml
blockedIps:
  - '192.168.1.100'
  - '10.0.0.1'

Next Steps

For general GA4 concepts, see Google Analytics 4 Guide.