Meta Pixel provides standard events for tracking conversions and custom events for Ghost-specific actions. This guide shows how to implement comprehensive event tracking for Ghost memberships, content engagement, and user interactions.
Meta Pixel Standard Events
Meta provides predefined standard events optimized for ad delivery and conversion tracking.
Recommended Events for Ghost
| Meta Event | Ghost Action | Use Case |
|---|---|---|
Lead |
Member signup (free) | Newsletter leads |
Subscribe |
Newsletter subscription | Email list growth |
Purchase |
Paid subscription | Membership revenue |
ViewContent |
Post/page view | Content engagement |
CompleteRegistration |
Account creation | User acquisition |
InitiateCheckout |
Portal checkout opened | Subscription funnel |
AddPaymentInfo |
Payment details entered | Purchase intent |
Search |
Site search usage | Content discovery |
Implementation Methods
Method 1: Ghost Portal Events (Client-Side)
Track Ghost Portal interactions using event listeners.
Member Signup (Lead Event)
{{!-- In default.hbs, before </body> --}}
<script>
// Track free member signup
window.addEventListener('portal-signup', function(event) {
if (typeof fbq !== 'undefined') {
fbq('track', 'Lead', {
content_name: 'Ghost Member Signup',
content_category: 'Membership',
{{#member}}
status: 'free',
{{/member}}
});
// Also track as CompleteRegistration
fbq('track', 'CompleteRegistration', {
content_name: 'Member Account Created',
value: 0,
currency: 'USD'
});
}
});
</script>
Newsletter Subscribe Event
<script>
// Track newsletter subscription
window.addEventListener('portal-subscribe', function(event) {
if (typeof fbq !== 'undefined') {
fbq('track', 'Subscribe', {
content_name: 'Newsletter Subscription',
predicted_ltv: 5.00, // Estimated lifetime value of email subscriber
value: 0,
currency: 'USD'
});
}
});
</script>
Member Login/Logout
<script>
// Track member login
window.addEventListener('portal-signin', function(event) {
if (typeof fbq !== 'undefined') {
fbq('trackCustom', 'MemberLogin', {
{{#member}}
member_type: '{{#if paid}}paid{{else}}free{{/if}}'
{{/member}}
});
}
});
// Track member logout
window.addEventListener('portal-signout', function(event) {
if (typeof fbq !== 'undefined') {
fbq('trackCustom', 'MemberLogout', {});
}
});
</script>
Portal Opened (Checkout Initiated)
<script>
// Track when Ghost Portal opens
window.addEventListener('portal-open', function(event) {
if (typeof fbq !== 'undefined') {
fbq('trackCustom', 'PortalOpened', {
{{#member}}
member_status: 'logged_in',
{{/member}}
{{^member}}
member_status: 'visitor',
{{/member}}
});
// If portal opens for checkout, track InitiateCheckout
{{^member}}
fbq('track', 'InitiateCheckout', {
content_name: 'Membership Checkout',
content_category: 'Subscription'
});
{{/member}}
}
});
</script>
Method 2: Paid Subscription Tracking (Purchase Event)
Track subscription purchases using member context:
{{!-- In default.hbs or post.hbs --}}
{{#if @member.subscriptions}}
<script>
// Track active paid subscription as Purchase
if (typeof fbq !== 'undefined') {
var subscription = {
id: '{{@member.subscriptions.[0].id}}',
tier: '{{@member.subscriptions.[0].tier.name}}',
price: {{@member.subscriptions.[0].tier.monthly_price}},
currency: '{{@member.subscriptions.[0].tier.currency}}',
cadence: '{{@member.subscriptions.[0].cadence}}'
};
// Only track purchase once per session
if (!sessionStorage.getItem('ghost_subscription_tracked')) {
fbq('track', 'Purchase', {
content_name: subscription.tier,
content_type: 'subscription',
content_ids: [subscription.id],
currency: subscription.currency.toUpperCase(),
value: subscription.price,
predicted_ltv: subscription.price * 12, // Annual value
num_items: 1
});
sessionStorage.setItem('ghost_subscription_tracked', 'true');
}
}
</script>
{{/if}}
Note: This tracks existing subscriptions. For new subscriptions, use Stripe webhooks (Method 3).
Method 3: Server-Side Conversion API (Recommended for Subscriptions)
For accurate subscription tracking, use Stripe webhooks with Meta Conversion API.
Stripe Webhook Handler (Node.js)
// stripe-webhook.js
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const crypto = require('crypto');
const axios = require('axios');
const app = express();
app.post('/webhook/stripe', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle subscription events
switch (event.type) {
case 'customer.subscription.created':
await trackSubscriptionPurchase(event.data.object);
break;
case 'invoice.payment_succeeded':
if (event.data.object.billing_reason === 'subscription_cycle') {
await trackRecurringPayment(event.data.object);
}
break;
case 'customer.subscription.deleted':
await trackSubscriptionCancellation(event.data.object);
break;
}
res.json({received: true});
});
async function trackSubscriptionPurchase(subscription) {
const pixelId = process.env.META_PIXEL_ID;
const accessToken = process.env.META_CAPI_ACCESS_TOKEN;
// Hash customer email
const customer = await stripe.customers.retrieve(subscription.customer);
const hashedEmail = crypto.createHash('sha256')
.update(customer.email.toLowerCase())
.digest('hex');
const payload = {
data: [{
event_name: 'Purchase',
event_time: Math.floor(Date.now() / 1000),
action_source: 'website',
user_data: {
em: hashedEmail,
client_user_agent: customer.metadata?.user_agent,
fbc: customer.metadata?.fbc,
fbp: customer.metadata?.fbp
},
custom_data: {
content_name: subscription.metadata?.tier_name || 'Paid Membership',
content_type: 'subscription',
content_ids: [subscription.items.data[0].price.product],
currency: subscription.currency.toUpperCase(),
value: (subscription.items.data[0].price.unit_amount / 100),
predicted_ltv: (subscription.items.data[0].price.unit_amount / 100) * 12
}
}]
};
try {
await axios.post(
`https://graph.facebook.com/v18.0/${pixelId}/events`,
payload,
{ params: { access_token: accessToken } }
);
console.log('Meta CAPI: Purchase tracked');
} catch (error) {
console.error('Meta CAPI Error:', error.response?.data);
}
}
async function trackRecurringPayment(invoice) {
// Similar implementation for recurring payments
console.log('Recurring payment tracked:', invoice.id);
}
async function trackSubscriptionCancellation(subscription) {
// Track cancellation as custom event
const pixelId = process.env.META_PIXEL_ID;
const accessToken = process.env.META_CAPI_ACCESS_TOKEN;
const customer = await stripe.customers.retrieve(subscription.customer);
const hashedEmail = crypto.createHash('sha256')
.update(customer.email.toLowerCase())
.digest('hex');
const payload = {
data: [{
event_name: 'SubscriptionCancelled',
event_time: Math.floor(Date.now() / 1000),
action_source: 'website',
user_data: {
em: hashedEmail
},
custom_data: {
subscription_id: subscription.id,
cancellation_reason: subscription.cancellation_details?.reason || 'unknown'
}
}]
};
try {
await axios.post(
`https://graph.facebook.com/v18.0/${pixelId}/events`,
payload,
{ params: { access_token: accessToken } }
);
} catch (error) {
console.error('Meta CAPI Error:', error.response?.data);
}
}
app.listen(3000);
Pass Client Data to Stripe
Modify Ghost theme to capture fbp and fbc values:
<script>
// Capture Facebook click and browser IDs
function getFBCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length === 2) return parts.pop().split(";").shift();
}
window.addEventListener('portal-checkout-start', function() {
var fbp = getFBCookie('_fbp');
var fbc = getFBCookie('_fbc');
// Store in sessionStorage to pass to Stripe
sessionStorage.setItem('meta_fbp', fbp);
sessionStorage.setItem('meta_fbc', fbc);
console.log('Meta tracking IDs:', {fbp, fbc});
});
</script>
Method 4: Content Engagement Tracking
Track how users interact with Ghost content:
ViewContent Event
{{!-- In post.hbs --}}
{{#post}}
<script>
// Track post view
if (typeof fbq !== 'undefined') {
fbq('track', 'ViewContent', {
content_name: '{{title}}',
content_category: '{{primary_tag.name}}',
content_type: 'article',
content_ids: ['{{id}}'],
{{#member}}
viewer_type: 'member',
member_status: '{{#if paid}}paid{{else}}free{{/if}}',
{{/member}}
{{^member}}
viewer_type: 'visitor',
{{/member}}
visibility: '{{visibility}}'
});
}
</script>
{{/post}}
Reading Progress Event
<script>
{{#post}}
// Track reading milestones
var milestones = [25, 50, 75, 100];
var tracked = [];
function trackReadingProgress() {
var article = document.querySelector('article');
if (!article) return;
var articleHeight = article.offsetHeight;
var windowHeight = window.innerHeight;
var scrollTop = window.pageYOffset;
var scrollPercent = ((scrollTop + windowHeight) / articleHeight) * 100;
milestones.forEach(function(milestone) {
if (scrollPercent >= milestone && tracked.indexOf(milestone) === -1) {
tracked.push(milestone);
if (typeof fbq !== 'undefined') {
fbq('trackCustom', 'ReadingProgress', {
scroll_depth: milestone,
post_id: '{{id}}',
post_title: '{{title}}'
});
}
}
});
}
var scrollTimeout;
window.addEventListener('scroll', function() {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(trackReadingProgress, 200);
});
{{/post}}
</script>
Member-Only Content Access
{{#has visibility="members,paid"}}
<script>
{{#member}}
// Track member accessing gated content
if (typeof fbq !== 'undefined') {
fbq('trackCustom', 'MemberContentAccess', {
content_id: '{{post.id}}',
content_title: '{{post.title}}',
content_visibility: '{{post.visibility}}',
member_tier: '{{#if paid}}paid{{else}}free{{/if}}'
});
}
{{/member}}
</script>
{{/has}}
Method 5: Search Tracking
If Ghost search is enabled:
<script>
// Track site search
var searchInput = document.querySelector('.search-input, [data-ghost-search]');
if (searchInput) {
searchInput.addEventListener('keyup', function(e) {
if (e.key === 'Enter' && this.value.length > 0) {
if (typeof fbq !== 'undefined') {
fbq('track', 'Search', {
search_string: this.value,
content_category: 'Site Search'
});
}
}
});
}
</script>
Method 6: Outbound Link Tracking
Track when users click external links:
<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 fbq !== 'undefined') {
fbq('trackCustom', 'OutboundClick', {
destination_url: href,
{{#post}}
source_post: '{{title}}',
{{/post}}
});
}
});
</script>
Custom Events for Ghost
Create custom events for Ghost-specific actions:
Tier Comparison Viewed
<script>
// Track when user views pricing/tiers page
{{#is "page"}}
{{#has slug="pricing,membership,subscribe"}}
if (typeof fbq !== 'undefined') {
fbq('trackCustom', 'ViewPricing', {
page_name: '{{title}}',
{{#member}}
current_tier: '{{#if paid}}paid{{else}}free{{/if}}',
{{/member}}
});
}
{{/has}}
{{/is}}
</script>
Comment Submitted
<script>
// Track comment submission
document.addEventListener('click', function(e) {
if (e.target && e.target.matches('.comment-submit, [data-comment-submit]')) {
if (typeof fbq !== 'undefined') {
fbq('trackCustom', 'CommentSubmitted', {
{{#post}}
post_id: '{{id}}',
post_title: '{{title}}',
{{/post}}
{{#member}}
member_status: '{{#if paid}}paid{{else}}free{{/if}}'
{{/member}}
});
}
}
});
</script>
Member Tier Upgrade
<script>
window.addEventListener('portal-subscription-created', function(event) {
{{#member}}
var previouslyPaid = {{#if paid}}true{{else}}false{{/if}};
// If previously free, now paid = upgrade
if (!previouslyPaid && typeof fbq !== 'undefined') {
fbq('trackCustom', 'TierUpgrade', {
from_tier: 'free',
to_tier: 'paid',
{{#if subscriptions}}
new_tier_name: '{{subscriptions.[0].tier.name}}',
subscription_value: {{subscriptions.[0].tier.monthly_price}},
{{/if}}
});
}
{{/member}}
});
</script>
Event Parameter Best Practices
Use Consistent Naming
// Good: Consistent parameter names
fbq('track', 'ViewContent', {
content_name: 'Post Title',
content_category: 'Technology',
content_type: 'article'
});
// Bad: Inconsistent parameter names
fbq('track', 'ViewContent', {
name: 'Post Title',
category: 'Technology',
type: 'article'
});
Include Currency for Value Events
// Always include currency with value
fbq('track', 'Purchase', {
value: 9.99,
currency: 'USD', // Required
content_name: 'Monthly Subscription'
});
Use Content IDs for Retargeting
// Include content_ids for dynamic ads
fbq('track', 'ViewContent', {
content_ids: ['post-123', 'post-456'],
content_type: 'article'
});
Testing Events
Meta Pixel Helper
- Install Meta Pixel Helper Chrome extension
- Navigate to your Ghost site
- Trigger events (signup, subscribe, etc.)
- Check extension popup for fired events
- Verify parameters are correct
Test Events Tool
- Navigate to Events Manager → Test Events
- Copy your test event code
- Add to Meta Pixel init:
fbq('init', 'YOUR_PIXEL_ID', {}, {
agent: 'test_event_code',
test_event_code: 'TEST12345'
});
- Trigger events on your site
- Verify events appear in Test Events dashboard
Browser Console
// Check if Meta Pixel loaded
console.log(typeof fbq);
// Expected: "function"
// Manually trigger test event
fbq('track', 'Lead', {
content_name: 'Test Lead'
});
// Check Meta queue
console.log(window._fbq);
Performance Optimization
Throttle High-Frequency Events
// Throttle scroll tracking
var lastScroll = 0;
var scrollThrottle = 1000; // 1 second
window.addEventListener('scroll', function() {
var now = Date.now();
if (now - lastScroll < scrollThrottle) return;
trackReadingProgress();
lastScroll = now;
});
Batch Custom Events
// Queue events and send in batches
var eventQueue = [];
function queueEvent(eventName, params) {
eventQueue.push({name: eventName, params: params});
if (eventQueue.length >= 5) {
flushEvents();
}
}
function flushEvents() {
eventQueue.forEach(function(event) {
fbq('trackCustom', event.name, event.params);
});
eventQueue = [];
}
// Flush on page unload
window.addEventListener('beforeunload', flushEvents);
Defer Non-Critical Events
// Defer reading progress tracking
window.addEventListener('load', function() {
setTimeout(function() {
initReadingProgressTracking();
}, 2000);
});
Common Issues
Events Not Recording
- Pixel Not Active - Verify pixel status in Events Manager
- Ad Blockers - Users with ad blockers won't track
- Incorrect Event Name - Standard events are case-sensitive
- Missing Currency - Value events require currency parameter
Duplicate Events
- Multiple Listeners - Remove duplicate event tracking code
- Portal Events Fire Twice - Use
{ once: true }on listeners - Code Injection + Theme - Consolidate in one location
iOS 14+ Tracking Issues
- Domain Verification - Verify domain in Meta Business Suite
- Aggregated Events - Configure top 8 events in Events Manager
- Conversion API - Implement server-side tracking for accuracy
Custom Events Not Appearing
- Event Name Format - Custom events can't match standard event names
- Parameter Limits - Max 25 custom parameters per event
- Delay - Custom events may take 24-48 hours to appear in reporting
Next Steps
- GA4 Event Tracking - Compare with GA4 events
- GTM Data Layer - Deploy events via GTM
- Troubleshooting Events - Debug event issues