Sanity Meta Pixel Integration | Blue Frog Docs

Sanity Meta Pixel Integration

Integrate Meta Pixel with Sanity-powered sites for Facebook and Instagram advertising, conversion tracking, and audience building.

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:

  1. Meta Business Manager account
  2. Meta Pixel created in Events Manager
  3. Pixel ID (format: 123456789012345)
  4. Sanity project with GROQ API access
  5. Frontend framework deployed
  6. (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
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

// 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

  1. Install Meta Pixel Helper Chrome Extension
  2. Visit your Sanity-powered site
  3. Click extension icon to see:
    • Pixel fires correctly
    • Event parameters
    • Warnings or errors

Test Events Tool

  1. Go to Meta Events Manager
  2. Click Test Events
  3. Enter your site URL
  4. Perform actions
  5. 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.

Additional Resources

// SYS.FOOTER