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
- Navigate to SEOmatic → Tracking Scripts
- Click Google Analytics
- Enter your GA4 Measurement ID in the Google Analytics Tracking ID field
- Configure settings:
- Environment: Set to
productiononly - Send Page View: Enabled
- IP Anonymization: Enable for GDPR compliance
- Display Advertising Features: As needed
- Environment: Set to
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
Cookie Consent Integration
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
- Browser Console: Check for
dataLayerarray - Network Tab: Verify requests to
google-analytics.com - GA4 DebugView: Use DebugView in GA4 interface
- 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
- Event Tracking - Track custom events and user interactions
- E-commerce Tracking - Implement Craft Commerce integration with GA4
- Troubleshooting - Resolve common tracking issues
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
}
}
}