Ghost GTM Setup: Install and Configure | OpsBlu Docs

Ghost GTM Setup: Install and Configure

How to integrate Google Tag Manager with Ghost for centralized tag management. Covers container installation, data layer configuration, trigger setup,...

Complete guide to setting up Google Tag Manager (GTM) on your Ghost site for centralized tracking and tag management.

Getting Started

GTM Setup Guide

Step-by-step instructions for installing GTM on Ghost.

Data Layer Implementation

Implement a comprehensive data layer for enhanced tracking capabilities.

Why Use GTM with Ghost?

GTM provides powerful tag management benefits:

  • Centralized Management: Control all tracking from one interface
  • No Code Deploys: Add/modify tags without site changes
  • Version Control: Track changes and roll back if needed
  • Preview Mode: Test tags before publishing
  • Advanced Triggers: Fire tags based on complex conditions

Prerequisites

Before installing GTM on Ghost:

  • Ghost 4.0 or later (Ghost 5.x recommended)
  • Access to Ghost Admin panel
  • Site owner or administrator role
  • Google Tag Manager account created
  • GTM Container ID (format: GTM-XXXXXXX)
  • Understanding of Ghost's code injection features

Installation Methods

Ghost's built-in code injection feature is the easiest installation method:

Site-Wide Installation:

  1. Log into your Ghost Admin panel
  2. Navigate to Settings > Code Injection
  3. In the Site Header section, paste your GTM head code:
<!-- Google Tag Manager -->
<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-XXXXXXX');</script>
<!-- End Google Tag Manager -->
  1. In the Site Footer section, paste your GTM body code:
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
  1. Click Save to apply changes
  2. Changes take effect immediately

Pros: Easy to implement, no theme modification required, survives theme updates Cons: Loads on all pages, limited conditional logic

Method 2: Theme Integration

For more control, integrate GTM directly into your Ghost theme:

Edit default.hbs:

  1. Access your Ghost theme files via FTP or Ghost Admin
  2. Navigate to Settings > Design > Change theme > Advanced > Download current theme
  3. Extract the theme ZIP file
  4. Open default.hbs in a code editor
  5. Add GTM head code immediately after {{ghost_head}}:
{{ghost_head}}

<!-- Google Tag Manager -->
<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-XXXXXXX');</script>
<!-- End Google Tag Manager -->
  1. Add GTM body code immediately after opening <body> tag:
<body class="{{body_class}}">

<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
  1. Re-zip the theme files
  2. Upload via Settings > Design > Change theme > Upload theme

Pros: Optimal placement, better performance, more control Cons: Requires theme editing, may be overwritten on theme updates

Method 3: Custom Partial

Create a reusable partial for GTM:

  1. Create partials/gtm-head.hbs:
<!-- Google Tag Manager -->
<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','{{@site.gtm_container_id}}');</script>
<!-- End Google Tag Manager -->
  1. Create partials/gtm-body.hbs:
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{@site.gtm_container_id}}"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
  1. Include partials in default.hbs:
{{ghost_head}}
{{> gtm-head}}

<body>
{{> gtm-body}}
  1. Store Container ID in Code Injection or config

Container ID Configuration

Finding Your Container ID

  1. Log into Google Tag Manager
  2. Select your container (or create a new one)
  3. Container ID appears in the format GTM-XXXXXXX
  4. Copy the full ID including the GTM- prefix

Environment-Specific Containers

Use different containers for development and production:

Development:

  • Container ID: GTM-DEV1234
  • Test thoroughly in preview mode

Production:

  • Container ID: GTM-PROD5678
  • Publish only after full testing

Configure based on domain:

<script>
var gtmId = window.location.hostname.includes('localhost') ||
            window.location.hostname.includes('staging')
            ? 'GTM-DEV1234'
            : 'GTM-PROD5678';
// Use gtmId in GTM snippet
</script>

Data Layer Implementation

Ghost-Specific Data Layer

Implement a data layer using Ghost's built-in context helpers:

<script>
window.dataLayer = window.dataLayer || [];
dataLayer.push({
  'pageType': '{{#is "home"}}home{{/is}}{{#is "post"}}post{{/is}}{{#is "page"}}page{{/is}}{{#is "tag"}}tag{{/is}}{{#is "author"}}author{{/is}}',
  'postId': '{{#post}}{{id}}{{/post}}',
  'postTitle': '{{#post}}{{title}}{{/post}}',
  'postAuthor': '{{#post}}{{primary_author.name}}{{/post}}',
  'postPublished': '{{#post}}{{published_at}}{{/post}}',
  'postTags': [{{#post}}{{#foreach tags}}'{{name}}'{{#unless @last}},{{/unless}}{{/foreach}}{{/post}}],
  'memberStatus': '{{#if @member}}logged_in{{else}}logged_out{{/if}}'
});
</script>

Post-Specific Data Layer

Add to post.hbs for detailed article tracking:

{{!-- In post.hbs --}}
<script>
dataLayer.push({
  'event': 'postView',
  'content': {
    'id': '{{id}}',
    'title': '{{title}}',
    'author': '{{primary_author.name}}',
    'publishedDate': '{{published_at}}',
    'modifiedDate': '{{updated_at}}',
    'featured': {{#if featured}}true{{else}}false{{/if}},
    'primaryTag': '{{primary_tag.name}}',
    'tags': [{{#foreach tags}}'{{name}}'{{#unless @last}},{{/unless}}{{/foreach}}],
    'readingTime': '{{reading_time}}',
    'visibility': '{{visibility}}'
  }
});
</script>

Member Subscription Events

Track Ghost membership interactions:

<script>
// Member signup
document.addEventListener('member.signup', function(e) {
  dataLayer.push({
    'event': 'memberSignup',
    'memberTier': e.detail.tier
  });
});

// Member login
document.addEventListener('member.login', function() {
  dataLayer.push({
    'event': 'memberLogin'
  });
});

// Newsletter subscription
document.addEventListener('newsletter.subscribed', function(e) {
  dataLayer.push({
    'event': 'newsletterSubscription',
    'source': e.detail.source
  });
});
</script>

Content Engagement Tracking

<script>
// Scroll depth tracking
var scrollDepths = [25, 50, 75, 100];
var scrolledTo = [];

window.addEventListener('scroll', function() {
  var scrollPercentage = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;

  scrollDepths.forEach(function(depth) {
    if (scrollPercentage >= depth && scrolledTo.indexOf(depth) === -1) {
      scrolledTo.push(depth);
      dataLayer.push({
        'event': 'scrollDepth',
        'scrollDepth': depth,
        'postTitle': '{{title}}'
      });
    }
  });
});

// Reading time tracking
setTimeout(function() {
  dataLayer.push({
    'event': 'engagedReader',
    'timeOnPage': 30
  });
}, 30000);
</script>

Common Triggers and Tags

Essential Triggers for Ghost

Create these triggers in GTM:

  1. All Pages Trigger

    • Type: Page View
    • Fires on: All Pages
  2. Post View Trigger

    • Type: Custom Event
    • Event name: postView
  3. Member Signup Trigger

    • Type: Custom Event
    • Event name: memberSignup
  4. Newsletter Subscribe Trigger

    • Type: Custom Event
    • Event name: newsletterSubscription
  5. Scroll Depth Trigger

    • Type: Custom Event
    • Event name: scrollDepth
    • Conditions: Scroll Depth equals 25, 50, 75, or 100

Essential Tags

Set up these core tags:

  1. Google Analytics 4 Configuration

    • Tag type: GA4 Configuration
    • Measurement ID: G-XXXXXXXXXX
    • Trigger: All Pages
  2. Post View Event

    • Tag type: GA4 Event
    • Event name: view_item
    • Parameters: Post ID, title, author, tags
    • Trigger: Post View
  3. Member Signup Event

    • Tag type: GA4 Event
    • Event name: sign_up
    • Parameters: Member tier
    • Trigger: Member Signup
  4. Scroll Tracking Event

    • Tag type: GA4 Event
    • Event name: scroll
    • Parameters: Scroll depth, post title
    • Trigger: Scroll Depth

Variables Configuration

Create these variables for Ghost-specific data:

  1. Data Layer Variables:

    • DL - Page Type
    • DL - Post ID
    • DL - Post Title
    • DL - Post Author
    • DL - Post Tags
    • DL - Member Status
  2. Custom JavaScript Variables:

    // Get current post slug
    function() {
      return window.location.pathname.split('/').filter(Boolean).pop();
    }
    
  3. URL Variables:

    • Page Path
    • Page Hostname
    • Referrer

Preview and Debug Mode

Using GTM Preview with Ghost

  1. In GTM, click Preview button
  2. Enter your Ghost site URL (e.g., https://yoursite.com)
  3. Click Connect
  4. New window opens with GTM debug panel
  5. Navigate to different page types (posts, pages, tags, authors)
  6. Verify data layer populates for each page type

Debugging Ghost-Specific Data

Check Ghost context helpers are working:

// In browser console
console.log(dataLayer);

// Verify Ghost variables
console.log('Page Type:', dataLayer[0].pageType);
console.log('Post Data:', dataLayer[0].content);

Common Debug Checks

  • GTM loads on all page types (home, post, page, tag, author)
  • Post-specific data appears only on post pages
  • Member status tracked correctly
  • Newsletter events fire when subscribed
  • Handlebars helpers render correctly
  • No syntax errors in data layer
  • Code injection applied site-wide

Publishing Workflow

Pre-Publishing Checklist

  • Test GTM on all page types (home, post, page, tag, author)
  • Verify post data layer on multiple posts
  • Test member signup/login events
  • Check newsletter subscription tracking
  • Test scroll depth tracking
  • Verify on mobile and desktop
  • Check Ghost preview mode
  • Test with different Ghost themes (if applicable)

Publishing Steps

  1. In GTM, click Submit
  2. Name version: "Ghost Production v1.0"
  3. Describe changes
  4. Click Publish
  5. Monitor Ghost site for issues
  6. Check browser console for errors

Ghost Cache Considerations

Ghost caches content aggressively. After GTM changes:

  • Test in private/incognito window
  • Clear Ghost cache if using a caching plugin
  • CDN cache may need purging
  • Member-specific content may cache differently

Troubleshooting Common Issues

GTM Not Loading on All Pages

Symptoms: GTM works on some pages but not others

Solutions:

  • Verify code injection applies site-wide (not post/page specific)
  • Check theme default.hbs includes GTM on all templates
  • Ensure no conditional logic blocking GTM
  • Test with different Ghost themes
  • Review Ghost Admin > Settings > Code Injection

Handlebars Helpers Not Rendering

Symptoms: Data layer shows literal {{}} instead of values

Solutions:

  • Use correct Handlebars syntax for Ghost version
  • Verify helpers exist in your Ghost version
  • Use {{log}} helper to debug context
  • Check triple vs double braces: {{{raw}}} vs {{escaped}}
  • Review Ghost handlebars documentation
  • Test helpers in Ghost preview

Member Events Not Firing

Symptoms: Member signup/login events not tracked

Solutions:

  • Verify Ghost membership features enabled
  • Check JavaScript event listeners attached correctly
  • Enable member features in Ghost Labs (if needed)
  • Test with real member account
  • Check browser console for errors
  • Verify Portal is configured correctly

Data Layer Timing Issues

Symptoms: Data layer pushes after GTM loads

Solutions:

  • Move data layer code before GTM container
  • Use ghost_head for critical data layer code
  • Ensure synchronous execution order
  • Avoid loading data layer in footer
  • Check Code Injection execution order

Theme Conflicts

Symptoms: GTM breaks after theme change

Solutions:

  • Re-apply Code Injection method (survives theme changes)
  • Document theme customizations
  • Use partials for modularity
  • Test GTM after theme updates
  • Keep custom code separate from theme files

Newsletter Subscription Not Tracking

Symptoms: Newsletter events not captured

Solutions:

  • Verify Portal configuration in Ghost
  • Check event listener matches Portal version
  • Test with Ghost default Portal
  • Review Portal JavaScript API documentation
  • Check for custom Portal implementations

Advanced Implementation

Custom Post Types

Track custom post types or content structures:

<script>
dataLayer.push({
  'event': 'customContentView',
  'contentType': '{{#has tag="podcast"}}podcast{{else}}article{{/has}}',
  'customFields': {
    'season': '{{custom_season}}',
    'episode': '{{custom_episode}}'
  }
});
</script>

Multi-Language Support

Track language/locale for international sites:

<script>
dataLayer.push({
  'language': '{{@site.locale}}',
  'region': '{{@custom.region}}',
  'translatedFrom': '{{#if custom_translation}}{{custom_original_lang}}{{/if}}'
});
</script>

Ghost API Integration

Pull additional data via Ghost Content API:

<script>
fetch('/ghost/api/v3/content/posts/{{id}}/?key=YOUR_CONTENT_API_KEY')
  .then(response => response.json())
  .then(data => {
    dataLayer.push({
      'event': 'postDetailLoaded',
      'postData': {
        'wordCount': data.posts[0].html.split(' ').length,
        'imageCount': data.posts[0].html.match(/<img/g)?.length || 0
      }
    });
  });
</script>

Comment Tracking

If using Ghost commenting systems:

<script>
// Track comment submission
document.addEventListener('comment.submitted', function(e) {
  dataLayer.push({
    'event': 'commentSubmitted',
    'postId': '{{id}}',
    'postTitle': '{{title}}'
  });
});
</script>

Membership Tier Tracking

Track different membership tiers:

<script>
{{#if @member}}
dataLayer.push({
  'memberTier': '{{@member.tier}}',
  'memberSince': '{{@member.created_at}}',
  'subscriptionStatus': '{{@member.subscription.status}}'
});
{{/if}}
</script>

Performance Optimization

Async Loading

GTM loads asynchronously by default, but optimize further:

// Load GTM with minimal blocking
<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;
  setTimeout(function(){f.parentNode.insertBefore(j,f)},0);
})(window,document,'script','dataLayer','GTM-XXXXXXX');
</script>

Conditional Loading

Load GTM only where needed:

{{!-- Only load on public pages --}}
{{#unless @member.admin}}
  {{> gtm-head}}
{{/unless}}

{{!-- Only load on posts --}}
{{#is "post"}}
  {{> gtm-head}}
{{/is}}

Minimize Data Layer Size

Keep data layer lean for performance:

// Only include necessary data
dataLayer.push({
  'type': '{{page_type}}',
  'id': '{{id}}',
  'title': '{{title}}'
  // Avoid large arrays or objects
});