This guide covers setting up Google Analytics 4 (GA4) on Episerver CMS and Commerce platforms (now Optimizely).
Prerequisites
- Episerver CMS 11+ or CMS 12+
- Google Analytics 4 property created
- GA4 Measurement ID (format:
G-XXXXXXXXXX) - Access to Episerver solution and templates
Implementation Methods
Method 1: Master Layout Template (Recommended)
Add GA4 directly to your master layout for site-wide tracking.
Step 1: Locate Master Layout
Find your main layout file, typically at:
Views/Shared/_Layout.cshtml(Standard MVC)Views/Shared/_Root.cshtml(Episerver Foundation)Views/Shared/Layouts/_Layout.cshtml(Custom structure)
Step 2: Add GA4 Script
Add this code in the <head> section:
@{
var gaTrackingId = "G-XXXXXXXXXX"; // Replace with your Measurement ID
var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
}
@if (!isEditMode)
{
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=@gaTrackingId"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '@gaTrackingId', {
'send_page_view': true
});
</script>
}
Key Points:
- The
isEditModecheck prevents tracking in Episerver edit mode - Replace
G-XXXXXXXXXXwith your actual Measurement ID - Script is loaded asynchronously for performance
Method 2: Configuration-Based Approach
Store the Measurement ID in configuration for easier management across environments.
Step 1: Add to appsettings.json (CMS 12+)
{
"GoogleAnalytics": {
"MeasurementId": "G-XXXXXXXXXX",
"Enabled": true
}
}
For CMS 11, use web.config:
<configuration>
<appSettings>
<add key="GoogleAnalytics:MeasurementId" value="G-XXXXXXXXXX" />
<add key="GoogleAnalytics:Enabled" value="true" />
</appSettings>
</configuration>
Step 2: Create Configuration Class
public class GoogleAnalyticsSettings
{
public string MeasurementId { get; set; }
public bool Enabled { get; set; }
}
Step 3: Register in Startup (CMS 12+)
public void ConfigureServices(IServiceCollection services)
{
services.Configure<GoogleAnalyticsSettings>(
Configuration.GetSection("GoogleAnalytics"));
}
Step 4: Update Layout Template
@using Microsoft.Extensions.Options
@inject IOptions<GoogleAnalyticsSettings> GASettings
@{
var gaSettings = GASettings.Value;
var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
}
@if (gaSettings.Enabled && !isEditMode && !string.IsNullOrEmpty(gaSettings.MeasurementId))
{
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=@gaSettings.MeasurementId"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '@gaSettings.MeasurementId');
</script>
}
Method 3: Multi-Site Configuration
For managing multiple sites with different GA4 properties.
Step 1: Add Site Definition Extensions
public static class SiteDefinitionExtensions
{
public static string GetGAMeasurementId(this SiteDefinition site)
{
return site.SiteUrl.Host switch
{
"www.site1.com" => "G-XXXXXXXXX1",
"www.site2.com" => "G-XXXXXXXXX2",
_ => "G-DEFAULTXXXX"
};
}
}
Or store in custom site properties:
[UIHint("Textarea")]
public virtual string GAMeasurementId { get; set; }
Step 2: Use in Template
@{
var currentSite = SiteDefinition.Current;
var gaId = currentSite.GetGAMeasurementId();
var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
}
@if (!string.IsNullOrEmpty(gaId) && !isEditMode)
{
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=@gaId"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '@gaId');
</script>
}
Method 4: View Component Approach
Create a reusable view component for cleaner templates.
Step 1: Create View Component
using Microsoft.AspNetCore.Mvc;
using EPiServer;
using EPiServer.Web;
public class GoogleAnalyticsViewComponent : ViewComponent
{
private readonly IContentLoader _contentLoader;
private readonly ISiteDefinitionRepository _siteRepository;
public GoogleAnalyticsViewComponent(
IContentLoader contentLoader,
ISiteDefinitionRepository siteRepository)
{
_contentLoader = contentLoader;
_siteRepository = siteRepository;
}
public IViewComponentResult Invoke()
{
// Don't load in edit mode
if (EPiServer.Editor.PageEditing.PageIsInEditMode)
{
return Content(string.Empty);
}
var currentSite = _siteRepository.Get(SiteDefinition.Current.Id);
var measurementId = currentSite.GetGAMeasurementId();
if (string.IsNullOrEmpty(measurementId))
{
return Content(string.Empty);
}
return View("~/Views/Shared/Components/GoogleAnalytics/Default.cshtml",
measurementId);
}
}
Step 2: Create View
Create Views/Shared/Components/GoogleAnalytics/Default.cshtml:
@model string
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=@Model"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '@Model');
</script>
Step 3: Add to Layout
@await Component.InvokeAsync("GoogleAnalytics")
Enhanced Configuration
User Properties
Track Episerver-specific user information:
<script>
gtag('config', '@gaId', {
'user_properties': {
'cms_version': '@EPiServer.Framework.FrameworkVersion.Current',
'site_name': '@SiteDefinition.Current.Name'
}
});
</script>
Custom Dimensions via User Properties
@{
var currentPage = Model as IContent;
var contentType = currentPage?.GetOriginalType().Name ?? "Unknown";
}
<script>
gtag('config', '@gaId', {
'user_properties': {
'content_type': '@contentType',
'language': '@Model.Language?.Name',
'is_commerce': '@(Model is EPiServer.Commerce.Catalog.ContentTypes.EntryContentBase ? "true" : "false")'
}
});
</script>
Debug Mode
Enable debug mode in development:
@{
var isDebug = HostingEnvironment.IsDevelopment();
}
<script>
gtag('config', '@gaId', {
'debug_mode': @(isDebug ? "true" : "false")
});
</script>
Content Delivery API (Headless)
For headless Episerver implementations:
Server-Side Setup
Add GA Measurement ID to API responses:
public class ContentApiModelConverter : IContentApiModelConverter
{
public ConvertedContentApiModel Convert(IContent content, ConverterContext converterContext)
{
var model = new ConvertedContentApiModel();
// Add GA tracking ID to metadata
model.Properties.Add("gaTrackingId", "G-XXXXXXXXXX");
return model;
}
}
Client-Side Implementation
In your frontend framework:
// React example
import { useEffect } from 'react';
function useGA4(measurementId) {
useEffect(() => {
if (!measurementId) return;
// Load gtag script
const script = document.createElement('script');
script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`;
script.async = true;
document.head.appendChild(script);
// Initialize GA4
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', measurementId);
}, [measurementId]);
}
// In your component
function App({ content }) {
useGA4(content.gaTrackingId);
// ...
}
Verification
1. Check Script Loading
Open your site and view source:
- Look for
gtag/js?id=G-XXXXXXXXXX - Verify the script is NOT present in edit mode
- Check that Measurement ID is correct
2. Browser Developer Tools
- Open DevTools Network tab
- Filter for
google-analytics.comorgtag - Look for successful requests
- Check the
collectendpoint calls
3. GA4 DebugView
Enable debug mode and check DebugView:
<script>
gtag('config', '@gaId', {
'debug_mode': true
});
</script>
Visit your site and check GA4 > Configure > DebugView
4. Tag Assistant
- Install Google Tag Assistant
- Connect to your site
- Verify GA4 tag is firing
- Check for errors or warnings
Troubleshooting
Script Not Loading in Edit Mode
Cause: Edit mode check missing or incorrect
Solution: Add edit mode check:
var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
Multiple GA4 Tags Firing
Cause: Script added in multiple places
Solution:
- Search for
gtag/jsin all templates - Remove duplicate implementations
- Use a single, centralized location
Preview Mode Tracking
Problem: Tracking fires in preview mode
Solution: Add preview mode check:
var isPreview = EPiServer.Web.ContextMode.Current == EPiServer.Web.ContextMode.Preview;
if (!isEditMode && !isPreview)
{
// Add tracking
}
Content Security Policy Issues
Error: Refused to load script from Google
Solution: Update CSP headers:
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Content-Security-Policy"
value="script-src 'self' 'unsafe-inline' https://*.googletagmanager.com https://*.google-analytics.com; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com;" />
</customHeaders>
</httpProtocol>
</system.webServer>
Next Steps
- Event Tracking - Add custom events
- Ecommerce Tracking - Track Episerver Commerce
- Troubleshooting - Fix common issues