HubSpot CMS Analytics: HubL Data Layers, Script Injection, | OpsBlu Docs

HubSpot CMS Analytics: HubL Data Layers, Script Injection,

Implement analytics on HubSpot CMS Hub. Covers site-wide and page-level script injection, HubL template data layers, HubSpot tracking code conflicts,...

Analytics Architecture on HubSpot CMS

HubSpot CMS Hub uses the HubL templating language (Jinja2-based) and a module system for content rendering. Analytics tracking integrates through three layers:

  • Site-wide code injection in Settings adds scripts to the head or footer of every page, including blog posts and landing pages
  • Page-level code injection in the page editor's Advanced Options adds scripts to individual pages
  • HubL template code in coded templates and modules provides the most control, with access to CRM contact data, page metadata, and content variables

HubSpot has its own tracking code (hs-script-loader) that automatically loads on every page. This tracks page views, form submissions, CTA clicks, and meeting bookings in HubSpot's native analytics. When adding external analytics (GA4, GTM), you need to account for the overlap to avoid double-counting events.

The HubSpot tracking code also sets the hubspotutk cookie for contact identification. This cookie persists the visitor's identity across sessions and ties page views to CRM contacts. External analytics tools track independently and do not share this identity unless you explicitly pass it through the data layer.

HubSpot CMS pages are served through HubSpot's CDN with automatic SSL, caching, and image optimization. Scripts added via the admin interface are included in the cached HTML response.


Installing Tracking Scripts

Add scripts that should run on every page through the HubSpot admin:

  1. Navigate to Settings > Website > Pages
  2. Scroll to Site Header HTML (loads in <head>) or Site Footer HTML (loads before </body>)
  3. Paste your tracking snippet

For GTM, add the head snippet to Site Header HTML:

<!-- GTM - Site Header HTML -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXX');</script>

And the noscript fallback to Site Footer HTML:

<!-- GTM noscript - Site Footer HTML -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>

Page-Level Scripts

For page-specific tracking (landing page conversion pixels, A/B test scripts):

  1. Open the page in the page editor
  2. Click Settings > Advanced Options
  3. Add code to Additional code snippets in head or footer

Blog-Specific Scripts

Blog templates have their own injection point:

  1. Navigate to Settings > Website > Blog
  2. Under Advanced, add scripts to the blog template header or footer

These scripts load only on blog pages, making them useful for content-specific analytics like scroll depth tracking or read time measurement.


Data Layer with HubL Templates

HubL gives you access to page metadata, content properties, and CRM contact data. Build a data layer in your coded template or a global module:

{# base.html or a global module #}
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    'page_type': '{{ content.template_path }}',
    'page_title': '{{ page_meta.html_title|escapejson }}',
    'content_id': '{{ content.id }}',
    'content_group': '{{ content_group.name|escapejson }}',
    'language': '{{ content.language.languageTag }}',
    'domain': '{{ request.domain }}',
    'is_landing_page': {{ content.is_landing_page|lower }},
    'publish_date': '{{ content.publish_date|datetimeformat("%Y-%m-%d") }}'
  });
</script>

Blog Post Data Layer

Blog posts expose additional metadata:

{# blog-post.html template #}
{% if content.blog_post_author %}
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    'event': 'blog_view',
    'blog_title': '{{ content.name|escapejson }}',
    'blog_author': '{{ content.blog_post_author.display_name|escapejson }}',
    'blog_topic': '{{ content.topic_list|join(", ")|escapejson }}',
    'blog_publish_date': '{{ content.publish_date|datetimeformat("%Y-%m-%d") }}',
    'word_count': {{ content.post_body|striptags|wordcount }}
  });
</script>
{% endif %}

CRM Contact Data in Data Layer

HubSpot's unique advantage is CRM data access in templates. For logged-in contacts or identified visitors:

{% if request.contact %}
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    'user_lifecycle_stage': '{{ request.contact.lifecyclestage }}',
    'user_company': '{{ request.contact.company|escapejson }}',
    'is_customer': {{ 'true' if request.contact.lifecyclestage == 'customer' else 'false' }},
    'hubspot_contact_id': '{{ request.contact.vid }}'
  });
</script>
{% endif %}

This enables analytics segmentation by lifecycle stage, company size, or any CRM property without client-side identity resolution.


HubSpot Tracking Code Conflicts

HubSpot's native tracking code (hs-script-loader/YOUR_HUB_ID.js) automatically tracks:

  • Page views (every navigation)
  • Form submissions (HubSpot forms only)
  • CTA clicks
  • Meeting bookings
  • Email link clicks

When running GA4 alongside HubSpot analytics, both systems independently count page views. This is not duplicate data -- they are separate analytics systems with different attribution models. However, if you are sending HubSpot form events to GA4 via GTM, you may double-count conversions.

To prevent form submission double-counting:

// GTM Custom HTML tag - Listen for HubSpot form submissions
window.addEventListener('message', function(event) {
  if (event.data.type === 'hsFormCallback' && event.data.eventName === 'onFormSubmitted') {
    window.dataLayer.push({
      'event': 'hubspot_form_submit',
      'form_id': event.data.id,
      'form_name': event.data.data?.submissionValues?.firstname ? 'contact_form' : 'unknown'
    });
  }
});

Use this GTM event as your form conversion trigger instead of a generic form submit listener, which would fire on both HubSpot and non-HubSpot forms.

Disabling HubSpot Tracking Code

If you want to use only external analytics and disable HubSpot's native tracking:

  1. Navigate to Settings > Tracking & Analytics > Tracking Code
  2. Toggle off Automatically add tracking code to your pages

Note that disabling the tracking code breaks HubSpot's contact activity timeline, form analytics, and CTA tracking. Most implementations keep both active.


Smart Content and Analytics

HubSpot's Smart Content displays different content to different visitors based on CRM data, lifecycle stage, country, device, or referral source. This creates analytics challenges:

  • The same URL serves different content to different visitors
  • A/B test results in HubSpot analytics may not match GA4 data
  • Content engagement metrics are split across variants without explicit tracking

Track which Smart Content variant a visitor sees:

{% smart_content "hero_cta" %}
  {% smart_rule "customer" %}
    <div data-smart-variant="customer">
      <p>Welcome back!</p>
    </div>
    <script>
      window.dataLayer = window.dataLayer || [];
      dataLayer.push({
        'event': 'smart_content_view',
        'smart_content_id': 'hero_cta',
        'smart_variant': 'customer'
      });
    </script>
  {% end_smart_rule %}
  {% smart_rule "lead" %}
    <div data-smart-variant="lead">
      <p>Start your free trial!</p>
    </div>
    <script>
      window.dataLayer = window.dataLayer || [];
      dataLayer.push({
        'event': 'smart_content_view',
        'smart_content_id': 'hero_cta',
        'smart_variant': 'lead'
      });
    </script>
  {% end_smart_rule %}
{% end_smart_content %}

CTA Click Tracking

HubSpot CTAs are tracked natively, but if you need CTA click data in GA4:

// GTM Custom HTML tag - Track HubSpot CTA clicks
document.addEventListener('click', function(event) {
  var ctaLink = event.target.closest('.hs-cta-wrapper a, .cta_button');
  if (ctaLink) {
    window.dataLayer = window.dataLayer || [];
    dataLayer.push({
      'event': 'cta_click',
      'cta_id': ctaLink.closest('.hs-cta-wrapper')?.id || 'unknown',
      'cta_text': ctaLink.textContent.trim(),
      'cta_url': ctaLink.href
    });
  }
});

Common Errors

Error Cause Fix
Duplicate page views in GA4 Both HubSpot tracking code and GA4 count page views This is expected -- they are separate systems. Only deduplicate if double-firing GA4 events
Form submission counted twice GTM generic form listener fires alongside HubSpot's native tracking Use hsFormCallback message event instead of generic form submit trigger
HubL variables render as empty Template variable does not exist for the content type Check content type (blog vs page) and use {% if %} guards before accessing variables
Scripts not loading on landing pages Landing pages use a different template than site pages Add scripts to both site-wide header AND landing page template settings
Smart Content variant not tracked No data layer push inside smart_rule blocks Add variant-specific data layer events inside each smart_rule
CRM data unavailable in template Contact not identified (no hubspotutk cookie) Use {% if request.contact %} guard; CRM data only works for identified visitors
Blog post word count returns 0 Using content.post_body instead of rendered body Ensure the wordcount filter is applied to content.post_body|striptags
GTM blocked by Content Security Policy Custom domain CSP does not allow GTM domains Add *.googletagmanager.com and *.google-analytics.com to CSP in HubSpot settings
Scripts lost after template change Code was in template-level injection, not site-wide Use site-wide header/footer injection for persistent scripts
Meeting booking not tracked in GA4 Meeting embed uses iframe, events do not bubble to parent Listen for HubSpot meeting callback via window.addEventListener('message')

Performance Considerations

  • HubSpot CDN: All pages are served through HubSpot's CDN with automatic caching. Scripts in the site header are included in the cached response and do not add extra requests
  • Tracking code weight: HubSpot's tracking code (hs-script-loader) is ~30KB gzipped. If you disable it, you lose CRM activity tracking but save a network request
  • Module loading: HubSpot modules with custom JavaScript add to the page's total script weight. Use the module_asset HubL function to load module JS only when the module is present on the page
  • Image optimization: HubSpot automatically optimizes images uploaded to the file manager but does not optimize images linked from external URLs. Use HubL's resize_image_url filter for dynamic resizing
  • Third-party scripts: Audit all scripts in Settings > Website > Pages header/footer. Each script adds to page load time. Remove unused pixels and consolidate through GTM where possible
  • Lazy loading: HubSpot does not natively lazy-load images below the fold. Add loading="lazy" to image modules or use the lazy_load attribute in custom modules