Google Tag Manager Data Layer on HubSpot | Blue Frog Docs

Google Tag Manager Data Layer on HubSpot

Implement a comprehensive data layer for GTM on HubSpot using HubL, CRM data, and custom properties.

Google Tag Manager Data Layer on HubSpot

The data layer is a JavaScript object that passes information from your HubSpot site to Google Tag Manager. This guide shows how to leverage HubSpot's unique features (HubL, CRM data, content properties) in your GTM data layer.

Prerequisites

  1. Install GTM on HubSpot
  2. Access to HubSpot Site Header HTML or template code
  3. Understanding of HubL templating language
  4. Super Admin or Website Editor permissions

Data Layer Basics

What is the Data Layer?

The data layer is a JavaScript array that holds information about:

  • Page details (type, name, author)
  • User/contact information (lifecycle stage, CRM data)
  • Events (form submissions, clicks, interactions)
  • Ecommerce data (products, transactions)

Basic Data Layer Structure

window.dataLayer = window.dataLayer || [];
dataLayer.push({
  'key': 'value',
  'anotherKey': 'anotherValue'
});

Implementing HubSpot Data Layer

Method 1: Site Header HTML (Global Implementation)

Add this code to Settings → Website → Pages → Site Header HTML BEFORE your GTM container code:

<script>
  // Initialize data layer
  window.dataLayer = window.dataLayer || [];

  // Push HubSpot data to data layer
  dataLayer.push({
    // Page Information
    'pageType': '{{ content.type }}',
    'pageId': '{{ content.id }}',
    'pageName': '{{ content.name }}',
    'pageUrl': '{{ content.absolute_url }}',
    'pageLanguage': '{{ content.language }}',

    {% if content.type == 'blog_post' %}
    // Blog-specific data
    'blogAuthor': '{{ content.blog_post_author.display_name }}',
    'publishDate': '{{ content.publish_date }}',
    'blogTags': [{% for tag in content.topic_list %}'{{ tag.name }}'{% if not loop.last %},{% endif %}{% endfor %}],
    {% endif %}

    {% if contact %}
    // Contact/CRM Information (for known visitors)
    'contactId': '{{ contact.vid }}',
    'lifecycleStage': '{{ contact.lifecycle_stage }}',
    'contactEmail': '{{ contact.email }}',
    'leadSource': '{{ contact.hs_analytics_source }}',
    'isContact': true,

    {% if contact.associatedcompany %}
    // Company information (if contact has associated company)
    'companyName': '{{ contact.associatedcompany.name }}',
    'companyIndustry': '{{ contact.associatedcompany.industry }}',
    'companySize': '{{ contact.associatedcompany.numberofemployees }}',
    {% endif %}
    {% else %}
    // Anonymous visitor
    'isContact': false,
    {% endif %}

    // Portal/Domain Information
    'portalId': '{{ portal_id }}',
    'domain': '{{ request.domain }}'
  });
</script>

<!-- Then your GTM container code -->
<script>(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXX');</script>

Method 2: Template-Level Implementation

For more control, add to specific templates in Design Tools:

{# Add before </head> in template #}
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    'pageType': '{{ content.type }}',
    'contentGroup': '{{ content.content_group_id }}',
    {% if content.custom_property %}
    'customProperty': '{{ content.custom_property }}',
    {% endif %}
    {% if contact %}
    'contactId': '{{ contact.vid }}',
    'lifecycleStage': '{{ contact.lifecycle_stage }}'
    {% endif %}
  });
</script>

HubSpot-Specific Data Layer Variables

Page & Content Variables

Standard Page Properties

dataLayer.push({
  // Basic page info
  'pageType': '{{ content.type }}', // page, landing_page, blog_post, etc.
  'pageId': '{{ content.id }}',
  'pageName': '{{ content.name }}',
  'pageTitle': '{{ page_meta.html_title }}',
  'metaDescription': '{{ page_meta.meta_description }}',

  // URLs and paths
  'pageUrl': '{{ content.absolute_url }}',
  'canonicalUrl': '{{ content.canonical_url }}',
  'pagePath': '{{ request.path }}',

  // Dates
  'publishDate': '{{ content.publish_date }}',
  'lastModified': '{{ content.updated }}',
  'createDate': '{{ content.created }}'
});

Blog Post Properties

{% if content.type == 'blog_post' %}
dataLayer.push({
  'blogId': '{{ group.id }}',
  'blogName': '{{ group.name }}',
  'postAuthor': '{{ content.blog_post_author.display_name }}',
  'authorEmail': '{{ content.blog_post_author.email }}',
  'postCategory': '{{ content.category_id }}',
  'postTags': [
    {% for tag in content.topic_list %}
    '{{ tag.name }}'{% if not loop.last %},{% endif %}
    {% endfor %}
  ],
  'commentCount': {{ content.comment_count|default(0) }},
  'isFeatured': {{ content.featured_image != null }}
});
{% endif %}

Landing Page Properties

{% if content.type == 'landing_page' %}
dataLayer.push({
  'landingPageCampaign': '{{ content.campaign }}',
  'landingPageName': '{{ content.name }}',
  'isGated': {% if content.has_form %}true{% else %}false{% endif %}
});
{% endif %}

Contact & CRM Variables

Contact Properties

{% if contact %}
dataLayer.push({
  // Core contact info
  'contactId': '{{ contact.vid }}',
  'contactEmail': '{{ contact.email }}',
  'firstName': '{{ contact.firstname }}',
  'lastName': '{{ contact.lastname }}',

  // Lifecycle and status
  'lifecycleStage': '{{ contact.lifecycle_stage }}', // subscriber, lead, MQL, SQL, customer, etc.
  'contactStatus': '{{ contact.hs_lead_status }}',

  // Attribution
  'originalSource': '{{ contact.hs_analytics_source }}',
  'originalSourceData1': '{{ contact.hs_analytics_source_data_1 }}',
  'originalSourceData2': '{{ contact.hs_analytics_source_data_2 }}',

  // Engagement metrics
  'emailOpens': {{ contact.hs_email_open|default(0) }},
  'emailClicks': {{ contact.hs_email_click|default(0) }},
  'pageViews': {{ contact.hs_analytics_num_page_views|default(0) }},
  'visits': {{ contact.hs_analytics_num_visits|default(0) }},

  // Dates
  'createDate': '{{ contact.createdate }}',
  'lastModifiedDate': '{{ contact.lastmodifieddate }}',
  'lastContactedDate': '{{ contact.notes_last_contacted }}'
});
{% endif %}

Company Properties

{% if contact.associatedcompany %}
dataLayer.push({
  'companyId': '{{ contact.associatedcompany.companyid }}',
  'companyName': '{{ contact.associatedcompany.name }}',
  'companyDomain': '{{ contact.associatedcompany.domain }}',
  'companyIndustry': '{{ contact.associatedcompany.industry }}',
  'companyType': '{{ contact.associatedcompany.type }}',
  'companySize': '{{ contact.associatedcompany.numberofemployees }}',
  'companyRevenue': '{{ contact.associatedcompany.annualrevenue }}',
  'companyCity': '{{ contact.associatedcompany.city }}',
  'companyState': '{{ contact.associatedcompany.state }}',
  'companyCountry': '{{ contact.associatedcompany.country }}'
});
{% endif %}

Deal Properties

{% if contact %}
dataLayer.push({
  'hasDeals': {% if contact.num_associated_deals > 0 %}true{% else %}false{% endif %},
  'numberOfDeals': {{ contact.num_associated_deals|default(0) }},
  'totalRevenue': {{ contact.total_revenue|default(0) }}
});
{% endif %}

Smart Content & Personalization Variables

Track which Smart Content variant is shown:

dataLayer.push({
  'smartContentVariant': '{% if contact.lifecycle_stage == "customer" %}customer{% elif contact.lifecycle_stage == "lead" %}lead{% else %}default{% endif %}',
  'personalizationEnabled': true
});

Custom HubSpot Properties

If you've created custom contact or company properties:

{% if contact %}
dataLayer.push({
  'customPropertyName': '{{ contact.custom_property_name }}',
  'industryVertical': '{{ contact.industry_vertical }}',
  'productInterest': '{{ contact.product_interest }}'
});
{% endif %}

Event-Based Data Layer Pushes

Form Submission Events

Add to Site Header HTML:

<script>
  // Listen for HubSpot form submissions
  window.addEventListener('message', function(event) {
    if (event.data.type === 'hsFormCallback' && event.data.eventName === 'onFormSubmitted') {
      dataLayer.push({
        'event': 'form_submission',
        'formId': event.data.id,
        'formType': 'hubspot_form',
        'formName': event.data.data.formGuid || 'unknown',
        'pageUrl': window.location.href,
        {% if contact %}
        'contactId': '{{ contact.vid }}',
        'lifecycleStage': '{{ contact.lifecycle_stage }}'
        {% endif %}
      });
    }
  });
</script>

CTA Click Events

<script>
  document.addEventListener('click', function(event) {
    var ctaElement = event.target.closest('.cta_button, [class*="hs-cta"]');

    if (ctaElement) {
      dataLayer.push({
        'event': 'cta_click',
        'ctaId': ctaElement.getAttribute('data-hs-cta-id') || 'unknown',
        'ctaText': ctaElement.textContent.trim(),
        'ctaUrl': ctaElement.href || '',
        'pageUrl': window.location.href
      });
    }
  });
</script>

Meeting Booking Events

<script>
  window.addEventListener('message', function(event) {
    if (event.data.meetingBookSucceeded === true) {
      dataLayer.push({
        'event': 'meeting_booked',
        'meetingType': event.data.meetingPayload.meetingType || 'unknown',
        'meetingDuration': event.data.meetingPayload.duration || '',
        {% if contact %}
        'contactId': '{{ contact.vid }}'
        {% endif %}
      });
    }
  });
</script>

Chat Interaction Events

<script>
  // Wait for HubSpot conversations to load
  window.hsConversationsOnReady = [function() {
    window.HubSpotConversations.on('conversationStarted', function() {
      dataLayer.push({
        'event': 'chat_started',
        'chatType': 'hubspot_live_chat'
      });
    });

    window.HubSpotConversations.on('conversationClosed', function() {
      dataLayer.push({
        'event': 'chat_closed'
      });
    });
  }];
</script>

Creating GTM Variables from Data Layer

Step 1: Create Data Layer Variables in GTM

For each data layer variable you want to use:

  1. In GTM, go to VariablesNew
  2. Variable Type: Data Layer Variable
  3. Data Layer Variable Name: Enter the exact key from your data layer (e.g., pageType)
  4. Data Layer Version: Version 2
  5. Name the variable (e.g., "DL - Page Type")
  6. Save

Common HubSpot Variables to Create

Create these data layer variables in GTM:

Variable Name Data Layer Key Use Case
DL - Page Type pageType Trigger tags based on page type
DL - Contact ID contactId Track identified users
DL - Lifecycle Stage lifecycleStage Segment by customer lifecycle
DL - Blog Author blogAuthor Track by content author
DL - Company Name companyName B2B attribution
DL - Form ID formId Track specific forms

Step 2: Use Variables in Tags and Triggers

Example: Send Lifecycle Stage to GA4

  1. Edit your GA4 Configuration tag
  2. Fields to Set → Add Row:
    • Field Name: lifecycle_stage
    • Value: \{\{DL - Lifecycle Stage\}\}
  3. Save and Publish

Example: Trigger Tag Only for Blog Posts

  1. Create new trigger
  2. Type: Page View
  3. Fire on: Some Page Views
  4. Condition: \{\{DL - Page Type\}\} equals blog_post
  5. Save

Advanced Data Layer Implementations

Conditional Data Based on Page Type

<script>
  window.dataLayer = window.dataLayer || [];

  var pageData = {
    'pageType': '{{ content.type }}',
    'pageId': '{{ content.id }}'
  };

  {% if content.type == 'blog_post' %}
  pageData.blogAuthor = '{{ content.blog_post_author.display_name }}';
  pageData.blogTags = [{% for tag in content.topic_list %}'{{ tag.name }}'{% if not loop.last %},{% endif %}{% endfor %}];
  {% elif content.type == 'landing_page' %}
  pageData.campaign = '{{ content.campaign }}';
  pageData.hasForm = {{ content.has_form }};
  {% endif %}

  {% if contact %}
  pageData.contactId = '{{ contact.vid }}';
  pageData.lifecycleStage = '{{ contact.lifecycle_stage }}';
  {% endif %}

  dataLayer.push(pageData);
</script>

E-commerce Data Layer

For HubSpot Commerce or Payments:

{% if content.product_id %}
dataLayer.push({
  'event': 'view_item',
  'ecommerce': {
    'currency': 'USD',
    'value': {{ content.product_price }},
    'items': [{
      'item_id': '{{ content.product_id }}',
      'item_name': '{{ content.product_name }}',
      'item_category': '{{ content.product_category }}',
      'price': {{ content.product_price }},
      'quantity': 1
    }]
  }
});
{% endif %}

User ID Tracking

Pass HubSpot contact ID as GA4 User ID:

{% if contact %}
dataLayer.push({
  'user_id': '{{ contact.vid }}', // HubSpot contact ID
  'user_properties': {
    'lifecycle_stage': '{{ contact.lifecycle_stage }}',
    'customer_type': '{% if contact.lifecycle_stage == "customer" %}paying{% else %}free{% endif %}'
  }
});
{% endif %}

Debugging the Data Layer

Method 1: Browser Console

// View entire data layer
console.log(dataLayer);

// View specific data layer push
console.log(dataLayer[0]); // First push
console.log(dataLayer[dataLayer.length - 1]); // Most recent push

// Find specific events
dataLayer.filter(item => item.event === 'form_submission');

Method 2: GTM Preview Mode

  1. Enable GTM Preview mode
  2. Visit your HubSpot page
  3. In Tag Assistant, click Variables
  4. Expand Data Layer
  5. See all values pushed to data layer

Method 3: Google Tag Assistant (Browser Extension)

  1. Install Google Tag Assistant browser extension
  2. Visit your HubSpot site
  3. Click extension icon
  4. View data layer values in real-time

Common Data Layer Issues

HubL Variables Return Empty

Problem: \{\{ contact.email \}\} returns empty string

Cause: Contact not identified yet, or HubL syntax error

Solution:

{% if contact.email %}
'contactEmail': '{{ contact.email }}'
{% else %}
'contactEmail': 'anonymous'
{% endif %}

Data Layer Pushed After GTM Loads

Problem: Variables undefined in GTM

Cause: Data layer push happens after GTM container loads

Fix: Always push data layer BEFORE GTM code:

<!-- Data layer first -->
<script>dataLayer.push({...});</script>

<!-- GTM container second -->
<script>(function(w,d,s,l,i){...})</script>

Variables Not Available in GTM

Problem: Data layer variable shows "undefined" in GTM

Debug:

  1. Check variable name matches exactly (case-sensitive)
  2. Verify data layer pushes before GTM loads
  3. Check HubL syntax is correct
  4. Use GTM Preview to inspect data layer

Testing Your Data Layer

1. Use GTM Preview Mode

  • Enable Preview in GTM
  • Visit HubSpot pages
  • Check Variables tab in Tag Assistant
  • Verify all expected variables populate

2. Test Across Page Types

Test data layer on different HubSpot content types:

  • Standard pages
  • Blog posts
  • Landing pages
  • Thank you pages

Verify page-specific variables populate correctly.

3. Test Logged In vs. Anonymous

  • Visit as anonymous user → Contact variables should be empty/false
  • Log in or identify yourself → Contact variables should populate

4. Verify Events Fire

Trigger events and check data layer:

  • Submit form → Check event: 'form_submission' pushed
  • Click CTA → Check event: 'cta_click' pushed
  • Book meeting → Check event: 'meeting_booked' pushed

Next Steps

For general GTM data layer concepts, see GTM Data Layer Guide.

// SYS.FOOTER