Fork CMS Analytics Implementation Guide | OpsBlu Docs

Fork CMS Analytics Implementation Guide

Install tracking scripts, build data layers, and debug analytics on Fork CMS using Twig templates, modules, kernel events, and head/footer injection.

Analytics Architecture on Fork CMS

Fork CMS is built on the Symfony framework and uses Twig for templating. Its modular architecture separates frontend rendering from backend administration. Analytics implementation touches several layers:

  • Twig templates in themes render page output with access to page variables, module data, and global settings
  • ThemesBundle provides the theming layer where layout templates define <head> and <body> structure
  • Head/footer code injection via the CMS settings allows non-developers to paste tracking scripts without template changes
  • Module system packages functionality into self-contained units with their own templates, actions, and widgets
  • Kernel events (Symfony kernel.response, kernel.request) allow programmatic script injection
  • Locale system provides multi-language content with language-specific routing

Fork CMS routes requests through a front controller to the appropriate module action, which loads data and renders a Twig template within the theme's layout.


Installing Tracking Scripts

Method 1: CMS Head/Footer Code Settings (Simplest)

Fork CMS provides built-in fields for injecting code into every page's <head> and before </body>:

  1. Go to Settings > General in the Fork CMS admin
  2. Paste your tracking script into the "Header HTML" or "Footer HTML" field
  3. Save
<!-- Header HTML field -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXX');
</script>

This outputs inside the {{ siteHTMLHeader|raw }} and {{ siteHTMLFooter|raw }} Twig tags in the theme layout.

Add tracking code directly in your theme's layout template:

{# src/Frontend/Themes/MyTheme/Core/Layout/Templates/Default.html.twig #}

<!DOCTYPE html>
<html lang="{{ LANGUAGE }}">
<head>
  <meta charset="utf-8">
  <title>{{ page.title }} - {{ SITE_TITLE }}</title>

  {{ siteHTMLHeader|raw }}

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

  {% block head_extra %}{% endblock %}
</head>
<body>
  {% block content %}
    {{ block('main') }}
  {% endblock %}

  {{ siteHTMLFooter|raw }}
</body>
</html>

Method 3: Kernel Event Subscriber

Inject scripts programmatically via a Symfony event subscriber in a custom module:

<?php
// src/Frontend/Modules/Analytics/EventSubscriber/TrackingSubscriber.php

namespace Frontend\Modules\Analytics\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class TrackingSubscriber implements EventSubscriberInterface
{
    private string $propertyId;

    public function __construct(string $propertyId)
    {
        $this->propertyId = $propertyId;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::RESPONSE => 'onKernelResponse',
        ];
    }

    public function onKernelResponse(ResponseEvent $event): void
    {
        if (!$event->isMainRequest()) return;

        $response = $event->getResponse();
        $content = $response->getContent();

        if (!str_contains($content, '</head>')) return;

        $script = sprintf(
            '<script async src="https://www.googletagmanager.com/gtag/js?id=%s"></script>',
            htmlspecialchars($this->propertyId)
        );

        $response->setContent(
            str_replace('</head>', $script . '</head>', $content)
        );
    }
}

Register it as a service:

# src/Frontend/Modules/Analytics/config/services.yaml

services:
  Frontend\Modules\Analytics\EventSubscriber\TrackingSubscriber:
    arguments:
      $propertyId: '%analytics.property_id%'
    tags:
      - { name: kernel.event_subscriber }

Data Layer Implementation

Twig Template Data Layer

Build the data layer in your theme's layout template using Fork CMS's template variables:

{# In the layout template, inside <head> #}

<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'page_id': {{ page.id|default(0) }},
  'page_title': {{ page.title|default('')|json_encode|raw }},
  'page_language': {{ LANGUAGE|json_encode|raw }},
  'site_title': {{ SITE_TITLE|json_encode|raw }},
  'page_template': {{ page.template_path|default('')|json_encode|raw }},
  {% if page.parent_id is defined and page.parent_id %}
  'parent_id': {{ page.parent_id }},
  {% endif %}
  'is_action': {{ page.is_action|default(false) ? 'true' : 'false' }}
});
</script>

Module-Specific Data

Fork CMS modules (Blog, FAQ, etc.) pass data to their templates. Extend the data layer in module templates:

{# src/Frontend/Modules/Blog/Layout/Templates/Detail.html.twig #}

{% block head_extra %}
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'page_type': 'blog_post',
  'article_id': {{ item.id }},
  'article_title': {{ item.title|json_encode|raw }},
  'article_category': {{ item.category_title|default('')|json_encode|raw }},
  'author': {{ item.user_id|default('')|json_encode|raw }},
  'publish_date': {{ item.publish_on|date('Y-m-d')|json_encode|raw }},
  'comment_count': {{ item.comments_count|default(0) }},
  {% if item.tags is defined %}
  'tags': {{ item.tags|map(t => t.name)|json_encode|raw }}
  {% endif %}
});
</script>
{% endblock %}

Widget Data Layer Contributions

Fork CMS widgets render in page positions. Widgets can push their own data:

{# Widget template #}

<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'widget_type': 'recent_posts',
  'widget_items_count': {{ items|length }}
});
</script>

Locale-Aware Data Layer

Fork CMS serves content in multiple languages with language-prefixed URLs (/en/, /fr/). Track the active locale:

<script>
window.dataLayer.push({
  'content_language': '{{ LANGUAGE }}',
  'interface_language': '{{ INTERFACE_LANGUAGE }}',
  'available_languages': {{ LANGUAGES|json_encode|raw }}
});
</script>

Common Issues

Draft and Revision Pages

Fork CMS supports page drafts and revisions. Draft pages are accessible to logged-in admins at preview URLs. Ensure tracking only fires on published pages:

{% if page.status == 'active' %}
  {# Inject analytics #}
{% endif %}

Or check in the event subscriber:

// Skip non-200 responses and admin routes
if ($event->getResponse()->getStatusCode() !== 200) return;
if (str_starts_with($event->getRequest()->getPathInfo(), '/private/')) return;

Backend vs Frontend Routes

Fork CMS has separate frontend (/en/page) and backend (/private/en/...) route namespaces. Never inject analytics on backend routes. The kernel event subscriber should check the request path:

$path = $event->getRequest()->getPathInfo();
if (str_contains($path, '/private/')) return;

In Twig, backend templates use different layout files, so analytics code in frontend themes does not leak into the admin.

Module Action URLs

Fork CMS module actions generate URLs like /en/blog/detail/article-slug. The URL structure includes the module name and action. Your data layer should parse the module context:

{% if 'blog/detail' in app.request.pathInfo %}
  {# Blog detail page #}
{% elif 'blog/category' in app.request.pathInfo %}
  {# Blog category listing #}
{% endif %}

Twig Variable Availability

Not all variables are available in all templates. Module-specific variables (like item in blog detail) only exist in that module's templates. Attempting to access them in the layout template causes errors. Use {% if item is defined %} guards or push module data from child templates via the {% block %} mechanism.

SEO Module URL Overrides

Fork CMS's SEO module lets editors override page URLs and meta data. The data layer should use the actual rendered URL, not the default slug:

'canonical_url': '{{ page.full_url|default(app.request.uri) }}'

Platform-Specific Considerations

Theme Structure: Fork CMS themes live in src/Frontend/Themes/{ThemeName}/. The layout template at Core/Layout/Templates/Default.html.twig is the primary injection point. Module templates extend or include blocks from this layout. All analytics code in the layout automatically applies to every module page.

siteHTMLHeader / siteHTMLFooter: These Twig variables output whatever is configured in Settings > General. They are the lowest-effort way to inject tracking code but provide no conditional logic. For environment-specific tracking (staging vs production), use template-level conditionals or the kernel event subscriber approach.

Module Development: If building a custom Fork CMS module that needs analytics events (e.g., form submission tracking, search tracking), push data layer events from the module's action class:

// In a module action
$this->template->assign('searchQuery', $query);
$this->template->assign('resultCount', count($results));

Then in the module template:

<script>
window.dataLayer.push({
  'event': 'site_search',
  'search_query': {{ searchQuery|json_encode|raw }},
  'search_results': {{ resultCount }}
});
</script>

Multisite Configuration: Fork CMS instances typically serve one site. For multi-domain setups, configure separate Fork CMS installations per domain, each with its own analytics property in Settings. There is no built-in multi-site from a single installation.

Asset Pipeline: Fork CMS does not include a JavaScript bundler. Tracking scripts in theme templates load as-is. For performance, place async/defer attributes on analytics scripts and position them after critical CSS in the <head>.