Ghost Event Tracking with Google Analytics 4 | OpsBlu Docs

Ghost Event Tracking with Google Analytics 4

Track Ghost member signups, newsletter subscriptions, comments, and content interactions with GA4

Ghost provides unique user interactions through its Members, Newsletter, and Portal systems. This guide shows how to track Ghost-specific events with GA4 for comprehensive user behavior analysis.

Ghost Event Categories

Member Events

  • Member Signup - New member registration via Ghost Portal
  • Member Login - Existing member authentication
  • Member Logout - Member session termination
  • Profile Update - Member profile or preference changes

Subscription Events

  • Newsletter Signup - Newsletter subscription via Portal
  • Newsletter Unsubscribe - Newsletter opt-out
  • Tier Upgrade - Free to paid member conversion
  • Tier Downgrade - Paid to free member change
  • Subscription Cancelled - Paid membership cancellation

Content Events

  • Post View - Individual post page view
  • Member Content Access - Member-only content viewed
  • Paid Content Access - Paid-tier content viewed
  • Comment Submitted - Native Ghost comment posted
  • Bookmark Added - Content saved/bookmarked

Portal Events

  • Portal Opened - Ghost Portal modal triggered
  • Portal Closed - Portal dismissed without action
  • Payment Form Viewed - Checkout step initiated

Event Tracking Implementation

Ghost Portal emits custom events you can capture with GA4. Add this to your theme's default.hbs:

{{!-- In default.hbs, before </body> --}}
<script>
  // Listen for Ghost Portal events
  window.addEventListener('portal-ready', function() {
    console.log('Ghost Portal initialized');
  });

  // Member signup event
  window.addEventListener('portal-signup', function(event) {
    if (typeof gtag !== 'undefined') {
      gtag('event', 'sign_up', {
        method: 'ghost_portal',
        event_category: 'member',
        event_label: 'member_signup'
      });
    }
  });

  // Member signin event
  window.addEventListener('portal-signin', function(event) {
    if (typeof gtag !== 'undefined') {
      gtag('event', 'login', {
        method: 'ghost_portal',
        event_category: 'member',
        event_label: 'member_login'
      });
    }
  });

  // Member signout event
  window.addEventListener('portal-signout', function(event) {
    if (typeof gtag !== 'undefined') {
      gtag('event', 'logout', {
        event_category: 'member',
        event_label: 'member_logout'
      });
    }
  });

  // Newsletter subscription
  window.addEventListener('portal-subscribe', function(event) {
    if (typeof gtag !== 'undefined') {
      gtag('event', 'newsletter_signup', {
        event_category: 'newsletter',
        event_label: 'newsletter_subscribe'
      });
    }
  });

  // Portal opened
  window.addEventListener('portal-open', function(event) {
    if (typeof gtag !== 'undefined') {
      gtag('event', 'portal_open', {
        event_category: 'engagement',
        event_label: 'ghost_portal_opened'
      });
    }
  });

  // Portal closed
  window.addEventListener('portal-close', function(event) {
    if (typeof gtag !== 'undefined') {
      gtag('event', 'portal_close', {
        event_category: 'engagement',
        event_label: 'ghost_portal_closed'
      });
    }
  });
</script>

Method 2: Content Interaction Tracking

Track content engagement specific to Ghost:

{{!-- In post.hbs template --}}
<script>
  {{#post}}
  // Track post view with metadata
  if (typeof gtag !== 'undefined') {
    gtag('event', 'view_item', {
      content_type: 'post',
      item_id: '{{id}}',
      item_name: '{{title}}',
      item_category: '{{primary_tag.name}}',
      item_brand: '{{primary_author.name}}',
      {{#if visibility}}
      content_visibility: '{{visibility}}',
      {{/if}}
      {{#member}}
      viewer_type: 'member',
      {{/member}}
      {{^member}}
      viewer_type: 'visitor',
      {{/member}}
    });
  }
  {{/post}}
</script>

Method 3: Member Content Access

Track when members access gated content:

{{!-- Track member-only content access --}}
{{#has visibility="members"}}
<script>
  {{#member}}
  // Member accessed members-only content
  if (typeof gtag !== 'undefined') {
    gtag('event', 'member_content_access', {
      event_category: 'content',
      event_label: 'members_only_viewed',
      content_id: '{{post.id}}',
      content_title: '{{post.title}}',
      member_tier: '{{#if @member.paid}}paid{{else}}free{{/if}}'
    });
  }
  {{/member}}
</script>
{{/has}}

{{!-- Track paid-only content access --}}
{{#has visibility="paid"}}
<script>
  {{#member}}
  {{#if @member.paid}}
  // Paid member accessed premium content
  if (typeof gtag !== 'undefined') {
    gtag('event', 'premium_content_access', {
      event_category: 'content',
      event_label: 'paid_content_viewed',
      content_id: '{{post.id}}',
      content_title: '{{post.title}}',
      {{#if @member.subscriptions}}
      subscription_tier: '{{@member.subscriptions.[0].tier.name}}',
      {{/if}}
    });
  }
  {{/if}}
  {{/member}}
</script>
{{/has}}

Method 4: Newsletter Signup Tracking

For email newsletter forms (separate from Portal):

{{!-- In theme where newsletter forms exist --}}
<form class="newsletter-form" id="ghost-newsletter-form">
  <input type="email" name="email" placeholder="Your email address" required>
  <button type="submit">Subscribe</button>
</form>

<script>
  document.getElementById('ghost-newsletter-form').addEventListener('submit', function(e) {
    // Track newsletter form submission
    if (typeof gtag !== 'undefined') {
      gtag('event', 'generate_lead', {
        event_category: 'newsletter',
        event_label: 'newsletter_form_submit',
        method: 'inline_form'
      });
    }
  });
</script>

Method 5: Reading Progress Tracking

Track how far users read posts:

{{!-- In post.hbs --}}
<script>
  {{#post}}
  (function() {
    var milestones = [25, 50, 75, 100];
    var tracked = [];

    function trackReadingProgress() {
      var articleHeight = document.querySelector('article').offsetHeight;
      var windowHeight = window.innerHeight;
      var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
      var scrollPercent = ((scrollTop + windowHeight) / articleHeight) * 100;

      milestones.forEach(function(milestone) {
        if (scrollPercent >= milestone && tracked.indexOf(milestone) === -1) {
          tracked.push(milestone);

          if (typeof gtag !== 'undefined') {
            gtag('event', 'scroll_depth', {
              event_category: 'engagement',
              event_label: 'reading_progress',
              value: milestone,
              post_id: '{{id}}',
              post_title: '{{title}}'
            });
          }
        }
      });
    }

    var scrollTimeout;
    window.addEventListener('scroll', function() {
      clearTimeout(scrollTimeout);
      scrollTimeout = setTimeout(trackReadingProgress, 100);
    });
  })();
  {{/post}}
</script>

Method 6: Comment Tracking

If using Ghost native comments:

<script>
  // Track comment submission (requires Ghost Comments feature)
  document.addEventListener('click', function(e) {
    if (e.target && e.target.matches('.comment-submit-button')) {
      if (typeof gtag !== 'undefined') {
        gtag('event', 'comment_submit', {
          event_category: 'engagement',
          event_label: 'ghost_comment_posted',
          {{#post}}
          post_id: '{{id}}',
          post_title: '{{title}}'
          {{/post}}
        });
      }
    }
  });
</script>

GA4 Custom Events Setup

Create these custom events in GA4 → Configure → Events → Create Event:

  1. member_signup

    • Event name: sign_up
    • Match condition: method = ghost_portal
  2. member_content_access

    • Event name: member_content_access
    • Parameter: content_visibility = members
  3. premium_content_access

    • Event name: premium_content_access
    • Parameter: content_visibility = paid
  4. newsletter_signup

    • Event name: newsletter_signup
    • Parameter: method exists
  5. reading_depth

    • Event name: scroll_depth
    • Parameter: value >= 75

Custom Dimensions Configuration

Set up these custom dimensions in GA4 → Configure → Custom Definitions:

Dimension Name Parameter Scope
Content Visibility content_visibility Event
Member Tier member_tier User
Subscription Tier subscription_tier User
Content ID content_id Event
Author Name item_brand Event
Primary Tag item_category Event
Viewer Type viewer_type Event

Advanced Event Tracking

Track Member Journey

Complete member lifecycle tracking:

<script>
  {{#member}}
  // Set user ID for cross-session tracking
  if (typeof gtag !== 'undefined') {
    gtag('set', 'user_id', '{{uuid}}');

    // Set member properties
    gtag('set', 'user_properties', {
      'member_status': '{{#if paid}}paid{{else}}free{{/if}}',
      'member_created': '{{created_at}}',
      {{#if subscriptions}}
      'subscription_tier': '{{subscriptions.[0].tier.name}}',
      'subscription_status': '{{subscriptions.[0].status}}',
      {{/if}}
    });
  }
  {{/member}}
</script>

Monitor external links in Ghost content:

<script>
  document.addEventListener('click', function(e) {
    var link = e.target.closest('a');
    if (!link) return;

    var href = link.getAttribute('href');
    var isExternal = href && (href.indexOf('http') === 0) && (href.indexOf(location.hostname) === -1);

    if (isExternal && typeof gtag !== 'undefined') {
      gtag('event', 'click', {
        event_category: 'outbound',
        event_label: href,
        transport_type: 'beacon',
        {{#post}}
        post_id: '{{id}}',
        post_title: '{{title}}'
        {{/post}}
      });
    }
  });
</script>

Track Search Usage

If Ghost search is enabled:

<script>
  // Track Ghost search usage
  document.querySelector('.search-input')?.addEventListener('keyup', function(e) {
    if (e.key === 'Enter' && this.value.length > 0) {
      if (typeof gtag !== 'undefined') {
        gtag('event', 'search', {
          search_term: this.value,
          event_category: 'site_search'
        });
      }
    }
  });
</script>

Testing Event Tracking

Debug Mode

Enable GA4 debug mode to see events in console:

gtag('config', 'G-XXXXXXXXXX', {
  'debug_mode': true
});

DebugView in GA4

  1. Navigate to Admin → DebugView in GA4
  2. Trigger events on your Ghost site
  3. Watch events appear in real-time
  4. Verify parameters and values

Browser Console Testing

Test individual events manually:

// Test member signup event
gtag('event', 'sign_up', {
  method: 'ghost_portal',
  event_category: 'member'
});

// Verify dataLayer
console.log(window.dataLayer);

Performance Considerations

Throttle Scroll Events

Prevent excessive scroll tracking:

var scrollTimeout;
var lastScrollEvent = 0;
var scrollThrottle = 1000; // 1 second

window.addEventListener('scroll', function() {
  var now = Date.now();
  if (now - lastScrollEvent < scrollThrottle) return;

  clearTimeout(scrollTimeout);
  scrollTimeout = setTimeout(function() {
    trackReadingProgress();
    lastScrollEvent = now;
  }, 100);
});

Batch Events

For high-frequency events, batch before sending:

var eventQueue = [];
var eventBatchSize = 5;

function queueEvent(eventName, params) {
  eventQueue.push({name: eventName, params: params});

  if (eventQueue.length >= eventBatchSize) {
    flushEvents();
  }
}

function flushEvents() {
  eventQueue.forEach(function(event) {
    gtag('event', event.name, event.params);
  });
  eventQueue = [];
}

// Flush on page unload
window.addEventListener('beforeunload', flushEvents);

Common Issues

Events Not Appearing in GA4

  • Delay - Events can take 24-48 hours to appear in standard reports (use DebugView for real-time)
  • Filters - Check GA4 data filters aren't excluding events
  • Ad Blockers - Test in incognito without extensions
  • Undefined gtag - Ensure GA4 loads before event tracking code

Duplicate Events

  • Multiple Listeners - Remove duplicate event listeners
  • Code Injection + Theme - Consolidate tracking code in one location
  • Portal Events Firing Twice - Use { once: true } option on event listeners

Member Context Missing

  • Cache Issues - Ghost caches pages; member context may be stale
  • Code Injection Limitation - Handlebars helpers limited in code injection
  • Use Theme Integration - Move to custom theme for full member access

Performance Degradation

  • Too Many Events - Limit scroll/interaction tracking
  • Unthrottled Listeners - Add debounce/throttle to scroll and resize
  • Large Payloads - Keep event parameters minimal

Next Steps