Sanity Meta Pixel Integration
Complete guide to implementing Meta Pixel (Facebook Pixel) on your Sanity-powered website for conversion tracking, audience building, and ad optimization.
Getting Started
Choose the implementation approach that best fits your needs:
Meta Pixel Setup Guide
Step-by-step instructions for installing Meta Pixel on Sanity-powered sites using Next.js, Gatsby, Remix, and other frameworks. Includes browser pixel and Conversions API setup.
Why Meta Pixel for Sanity?
Meta Pixel provides powerful advertising capabilities for headless CMS implementations:
- Conversion Tracking: Track purchases, leads, and custom conversions
- Audience Building: Create custom and lookalike audiences based on behavior
- Ad Optimization: Optimize campaigns using pixel data
- GROQ-Enhanced Events: Enrich pixel events with Sanity content metadata
- Cross-Platform Tracking: Track users across Facebook, Instagram, Messenger, Audience Network
- Conversions API: Server-side tracking for privacy and reliability
- Framework Compatibility: Works with Next.js, Gatsby, Remix, SvelteKit
Implementation Options
| Method | Best For | Complexity | Privacy | Ad-Blocker Resistant |
|---|---|---|---|---|
| Browser Pixel | Standard tracking | Simple | Low | No |
| GTM Integration | Multiple platforms | Moderate | Medium | No |
| Conversions API | Privacy-focused, reliable | Advanced | High | Yes |
| Hybrid (Browser + CAPI) | Best data quality | Advanced | High | Partial |
| Server-Side Only | Maximum privacy | Advanced | Very High | Yes |
Prerequisites
Before starting:
- Meta Business Manager account
- Meta Pixel created in Events Manager
- Pixel ID (format: 123456789012345)
- Sanity project with GROQ API access
- Frontend framework deployed
- (Optional) Conversions API access token
Meta Pixel Architecture for Sanity Sites
Browser-Based Tracking
User Interaction
↓
Frontend Component
↓
GROQ Query (fetch content metadata)
↓
fbq() Event Fire
↓
Meta Pixel
↓
Facebook Events Manager
Conversions API (Server-Side)
User Action
↓
Frontend sends event data to API route
↓
API route fetches Sanity content via GROQ
↓
Server sends event to Meta Conversions API
↓
Facebook Events Manager
Hybrid Approach (Recommended)
User Interaction
↓
├─→ Browser Pixel (fbq)
└─→ API Route → Conversions API
↓
Event Deduplication
↓
Facebook Events Manager
Sanity-Specific Pixel Features
Content Metadata in Events
Enrich pixel events with Sanity content data:
// Fetch content with GROQ
const content = await client.fetch(`
*[_type == "product" && slug.current == $slug][0]{
_id,
title,
price,
brand,
"category": category->title,
inventory
}
`, { slug })
// Track view with content metadata
fbq('track', 'ViewContent', {
content_ids: [content._id],
content_name: content.title,
content_category: content.category,
content_type: 'product',
value: content.price,
currency: 'USD'
})
Custom Events for Content Types
Track different Sanity document types:
// Generic content tracking
function trackSanityContent(content) {
const eventMap = {
product: 'ViewContent',
article: 'ViewContent',
event: 'Search', // Events as searchable content
contact: 'Lead'
}
const eventName = eventMap[content._type] || 'ViewContent'
fbq('track', eventName, {
content_ids: [content._id],
content_type: content._type,
content_name: content.title || content.name
})
}
Portable Text Engagement
Track user interaction with rich content:
// Track article reading depth
let readingDepth = 0
function trackReadingProgress() {
const scrollPercentage = Math.round(
(window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
)
if (scrollPercentage > readingDepth && scrollPercentage % 25 === 0) {
readingDepth = scrollPercentage
fbq('trackCustom', 'ArticleProgress', {
content_id: article._id,
content_title: article.title,
progress_percentage: scrollPercentage
})
}
}
window.addEventListener('scroll', trackReadingProgress)
eCommerce Product Tracking
// Fetch product data from Sanity
const product = await client.fetch(`
*[_type == "product" && _id == $id][0]{
_id,
title,
price,
compareAtPrice,
brand,
"category": category->title,
images,
inventory
}
`, { id })
// Track product view
fbq('track', 'ViewContent', {
content_ids: [product._id],
content_name: product.title,
content_type: 'product',
content_category: product.category,
value: product.price,
currency: 'USD'
})
// Track add to cart
function handleAddToCart() {
fbq('track', 'AddToCart', {
content_ids: [product._id],
content_name: product.title,
content_type: 'product',
value: product.price,
currency: 'USD'
})
}
Framework-Specific Examples
Next.js App Router
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }) {
return (
<html>
<head>
<Script id="meta-pixel" strategy="afterInteractive">
{`
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${process.env.NEXT_PUBLIC_META_PIXEL_ID}');
fbq('track', 'PageView');
`}
</Script>
<noscript>
<img
height="1"
width="1"
style={{ display: 'none' }}
src={`https://www.facebook.com/tr?id=${process.env.NEXT_PUBLIC_META_PIXEL_ID}&ev=PageView&noscript=1`}
/>
</noscript>
</head>
<body>{children}</body>
</html>
)
}
Next.js Pages Router
// pages/_app.js
import Script from 'next/script'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export default function App({ Component, pageProps }) {
const router = useRouter()
useEffect(() => {
// Track route changes
const handleRouteChange = () => {
if (typeof window !== 'undefined' && window.fbq) {
window.fbq('track', 'PageView')
}
}
router.events.on('routeChangeComplete', handleRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [router.events])
return (
<>
<Script id="meta-pixel" strategy="afterInteractive">
{`
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${process.env.NEXT_PUBLIC_META_PIXEL_ID}');
fbq('track', 'PageView');
`}
</Script>
<Component {...pageProps} />
</>
)
}
Gatsby
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-facebook-pixel`,
options: {
pixelId: process.env.META_PIXEL_ID,
},
},
],
}
// Custom tracking in components
import { useEffect } from 'react'
export default function ProductPage({ data }) {
useEffect(() => {
if (typeof window !== 'undefined' && window.fbq) {
window.fbq('track', 'ViewContent', {
content_ids: [data.sanityProduct._id],
content_name: data.sanityProduct.title,
content_type: 'product',
value: data.sanityProduct.price,
currency: 'USD'
})
}
}, [data])
return <ProductDisplay product={data.sanityProduct} />
}
Remix
// app/root.tsx
export default function Root() {
return (
<html>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${process.env.META_PIXEL_ID}');
fbq('track', 'PageView');
`
}}
/>
</head>
<body>
<Outlet />
</body>
</html>
)
}
Conversions API Implementation
Next.js API Route
// app/api/meta-conversion/route.ts
import { NextResponse } from 'next/server'
import crypto from 'crypto'
export async function POST(request: Request) {
const { event, eventData, userData } = await request.json()
// Hash user data for privacy
const hashedEmail = userData.email
? crypto.createHash('sha256').update(userData.email).digest('hex')
: null
const payload = {
data: [{
event_name: event,
event_time: Math.floor(Date.now() / 1000),
action_source: 'website',
event_source_url: userData.url,
user_data: {
em: hashedEmail,
client_ip_address: userData.ip,
client_user_agent: userData.userAgent,
fbp: userData.fbp, // Browser _fbp cookie
fbc: userData.fbc // Click ID from URL
},
custom_data: eventData
}]
}
const response = await fetch(
`https://graph.facebook.com/v18.0/${process.env.META_PIXEL_ID}/events`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...payload,
access_token: process.env.META_CAPI_ACCESS_TOKEN
})
}
)
const result = await response.json()
return NextResponse.json(result)
}
Frontend Event Sender
// Track both browser and server-side
async function trackConversion(event, eventData) {
// Browser pixel
if (typeof window !== 'undefined' && window.fbq) {
window.fbq('track', event, eventData)
}
// Server-side Conversions API
await fetch('/api/meta-conversion', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event,
eventData,
userData: {
email: user.email,
url: window.location.href,
ip: null, // Server will capture
userAgent: navigator.userAgent,
fbp: getCookie('_fbp'),
fbc: getCookie('_fbc')
}
})
})
}
Event Deduplication
// Generate unique event ID for deduplication
import { v4 as uuidv4 } from 'uuid'
function trackWithDeduplication(event, eventData) {
const eventId = uuidv4()
// Browser pixel with event_id
fbq('track', event, eventData, { eventID: eventId })
// Conversions API with same event_id
fetch('/api/meta-conversion', {
method: 'POST',
body: JSON.stringify({
event,
eventData,
eventId, // Same ID for deduplication
userData: {/* ... */}
})
})
}
Standard Events for Sanity Content
ViewContent
// Article or product view
fbq('track', 'ViewContent', {
content_ids: [content._id],
content_name: content.title,
content_type: content._type,
content_category: content.category?.title
})
AddToCart
// Product added to cart
fbq('track', 'AddToCart', {
content_ids: [product._id],
content_name: product.title,
content_type: 'product',
value: product.price,
currency: 'USD'
})
Purchase
// Order completed
fbq('track', 'Purchase', {
content_ids: order.items.map(item => item._id),
contents: order.items.map(item => ({
id: item._id,
quantity: item.quantity
})),
value: order.total,
currency: 'USD',
num_items: order.items.length
})
Lead
// Form submission
fbq('track', 'Lead', {
content_name: 'Contact Form',
content_category: 'Lead Generation'
})
Custom Events
// Track Sanity-specific actions
fbq('trackCustom', 'ArticleRead', {
content_id: article._id,
content_title: article.title,
author: article.author?.name,
reading_time: article.readingTime
})
Privacy and Compliance
Consent Management
// Initialize pixel with consent
if (userHasConsented) {
fbq('init', PIXEL_ID)
fbq('track', 'PageView')
} else {
// Don't initialize until consent granted
}
// Grant consent later
function handleConsentGranted() {
fbq('init', PIXEL_ID)
fbq('track', 'PageView')
}
Advanced Matching (Privacy-Safe)
// Hash user data client-side before sending
import crypto from 'crypto'
function hashData(data) {
return crypto.createHash('sha256').update(data.toLowerCase().trim()).digest('hex')
}
fbq('init', PIXEL_ID, {
em: hashData(user.email),
fn: hashData(user.firstName),
ln: hashData(user.lastName),
ct: hashData(user.city),
st: hashData(user.state),
zp: hashData(user.zip)
})
Exclude Preview Mode
// Next.js - don't track in preview
import { draftMode } from 'next/headers'
export default async function Page() {
const { isEnabled } = draftMode()
return (
<div>
{!isEnabled && <MetaPixel />}
<Content />
</div>
)
}
Testing and Debugging
Meta Pixel Helper
- Install Meta Pixel Helper Chrome Extension
- Visit your Sanity-powered site
- Click extension icon to see:
- Pixel fires correctly
- Event parameters
- Warnings or errors
Test Events Tool
- Go to Meta Events Manager
- Click Test Events
- Enter your site URL
- Perform actions
- Verify events appear with correct parameters
Console Debugging
// Log all pixel events
const originalFbq = window.fbq
window.fbq = function() {
console.log('Meta Pixel Event:', arguments)
return originalFbq.apply(window, arguments)
}
Performance Optimization
Lazy Load Pixel
// Load pixel after user interaction
let pixelLoaded = false
function loadMetaPixel() {
if (pixelLoaded) return
const script = document.createElement('script')
script.async = true
script.src = 'https://connect.facebook.net/en_US/fbevents.js'
document.head.appendChild(script)
script.onload = () => {
fbq('init', PIXEL_ID)
fbq('track', 'PageView')
pixelLoaded = true
}
}
// Load on first interaction
document.addEventListener('click', loadMetaPixel, { once: true })
Use Partytown
// Offload to web worker (Next.js)
import { Partytown } from '@builder.io/partytown/react'
export default function App() {
return (
<>
<Partytown forward={['fbq']} />
<Script type="text/partytown">
{/* Meta Pixel code */}
</Script>
</>
)
}
Common Issues
See Troubleshooting Events Not Firing for detailed solutions.