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
Method 1: Code Injection (Recommended)
Ghost's built-in code injection feature is the easiest installation method:
Site-Wide Installation:
- Log into your Ghost Admin panel
- Navigate to Settings > Code Injection
- 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 -->
- 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) -->
- Click Save to apply changes
- 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:
- Access your Ghost theme files via FTP or Ghost Admin
- Navigate to Settings > Design > Change theme > Advanced > Download current theme
- Extract the theme ZIP file
- Open
default.hbsin a code editor - 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 -->
- 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) -->
- Re-zip the theme files
- 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:
- 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 -->
- 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) -->
- Include partials in
default.hbs:
{{ghost_head}}
{{> gtm-head}}
<body>
{{> gtm-body}}
- Store Container ID in Code Injection or config
Container ID Configuration
Finding Your Container ID
- Log into Google Tag Manager
- Select your container (or create a new one)
- Container ID appears in the format GTM-XXXXXXX
- 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:
All Pages Trigger
- Type: Page View
- Fires on: All Pages
Post View Trigger
- Type: Custom Event
- Event name: postView
Member Signup Trigger
- Type: Custom Event
- Event name: memberSignup
Newsletter Subscribe Trigger
- Type: Custom Event
- Event name: newsletterSubscription
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:
Google Analytics 4 Configuration
- Tag type: GA4 Configuration
- Measurement ID: G-XXXXXXXXXX
- Trigger: All Pages
Post View Event
- Tag type: GA4 Event
- Event name: view_item
- Parameters: Post ID, title, author, tags
- Trigger: Post View
Member Signup Event
- Tag type: GA4 Event
- Event name: sign_up
- Parameters: Member tier
- Trigger: Member Signup
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:
Data Layer Variables:
- DL - Page Type
- DL - Post ID
- DL - Post Title
- DL - Post Author
- DL - Post Tags
- DL - Member Status
Custom JavaScript Variables:
// Get current post slug function() { return window.location.pathname.split('/').filter(Boolean).pop(); }URL Variables:
- Page Path
- Page Hostname
- Referrer
Preview and Debug Mode
Using GTM Preview with Ghost
- In GTM, click Preview button
- Enter your Ghost site URL (e.g., https://yoursite.com)
- Click Connect
- New window opens with GTM debug panel
- Navigate to different page types (posts, pages, tags, authors)
- 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
- In GTM, click Submit
- Name version: "Ghost Production v1.0"
- Describe changes
- Click Publish
- Monitor Ghost site for issues
- 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
});