This guide covers installing the Meta Pixel (formerly Facebook Pixel) on your Grav site for tracking conversions, retargeting, and measuring ad campaign performance.
Before You Begin
Create Meta Pixel
- Go to Meta Events Manager
- Create a new pixel
- Note your Pixel ID (format: 16-digit number)
Admin Access Required
- Access to Grav Admin Panel or file system
- Ability to edit theme templates or install plugins
Method 1: Twig Template Integration (Recommended)
Add Meta Pixel directly to your theme templates for complete control.
Add to Base Template
{# user/themes/your-theme/templates/partials/base.html.twig #}
<!DOCTYPE html>
<html lang="{{ grav.language.getActive ?: 'en' }}">
<head>
{% block head %}
<meta charset="utf-8" />
<title>{% if page.title %}{{ page.title }} | {% endif %}{{ site.title }}</title>
{# Meta Pixel Code #}
{% set pixel_id = grav.config.site.meta_pixel_id %}
{% if pixel_id %}
<!-- Meta Pixel Code -->
<script>
!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', '{{ pixel_id }}');
fbq('track', 'PageView');
</script>
<noscript>
<img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id={{ pixel_id }}&ev=PageView&noscript=1"/>
</noscript>
<!-- End Meta Pixel Code -->
{% endif %}
{{ assets.css()|raw }}
{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
{{ assets.js()|raw }}
</body>
</html>
Configure in site.yaml
# user/config/site.yaml
title: My Grav Site
default_lang: en
meta_pixel_id: '1234567890123456'
Add to Header Partial
If your theme uses a separate header partial:
{# user/themes/your-theme/templates/partials/head.html.twig #}
<meta charset="utf-8" />
<title>{% if page.title %}{{ page.title|e('html') }} | {% endif %}{{ site.title|e('html') }}</title>
{# Meta Pixel #}
{% set pixel_id = grav.config.site.meta_pixel_id %}
{% if pixel_id and grav.config.system.environment == 'production' %}
<!-- Meta Pixel Code -->
<script>
!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', '{{ pixel_id }}');
fbq('track', 'PageView');
</script>
<noscript>
<img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id={{ pixel_id }}&ev=PageView&noscript=1"/>
</noscript>
<!-- End Meta Pixel Code -->
{% endif %}
Method 2: Custom Plugin
Create a minimal Meta Pixel plugin for easier management.
Create Plugin Structure
# Create plugin directory
mkdir -p user/plugins/meta-pixel
# Create files
touch user/plugins/meta-pixel/meta-pixel.php
touch user/plugins/meta-pixel/meta-pixel.yaml
Plugin Code
<?php
// user/plugins/meta-pixel/meta-pixel.php
namespace Grav\Plugin;
use Grav\Common\Plugin;
class MetaPixelPlugin extends Plugin
{
public static function getSubscribedEvents()
{
return [
'onPluginsInitialized' => ['onPluginsInitialized', 0]
];
}
public function onPluginsInitialized()
{
// Don't run in admin
if ($this->isAdmin()) {
return;
}
$this->enable([
'onPageContentRaw' => ['onPageContentRaw', 0]
]);
}
public function onPageContentRaw()
{
$config = $this->config->get('plugins.meta-pixel');
if (!$config['enabled'] || empty($config['pixel_id'])) {
return;
}
$pixel_id = $config['pixel_id'];
$track_page_view = $config['track_page_view'] ? 'true' : 'false';
$pixel_code = <<<HTML
<!-- Meta Pixel Code -->
<script>
!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', '{$pixel_id}');
HTML;
if ($config['track_page_view']) {
$pixel_code .= "\nfbq('track', 'PageView');";
}
$pixel_code .= <<<HTML
</script>
<noscript>
<img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id={$pixel_id}&ev=PageView&noscript=1"/>
</noscript>
<!-- End Meta Pixel Code -->
HTML;
$this->grav['assets']->addInlineJs($pixel_code, ['position' => 'head']);
}
}
Plugin Configuration
# user/plugins/meta-pixel/meta-pixel.yaml
enabled: true
pixel_id: '1234567890123456'
track_page_view: true
Method 3: Multiple Pixels
Track with multiple pixels for different audiences or ad accounts.
{# Multiple Meta Pixels #}
{% set pixels = {
'main': '1234567890123456',
'remarketing': '6543210987654321'
} %}
<script>
!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');
{% for key, pixel_id in pixels %}
fbq('init', '{{ pixel_id }}');
{% endfor %}
fbq('track', 'PageView');
</script>
Conversion Tracking
Track Form Submissions
{# Contact form with Meta Pixel tracking #}
<form name="contact-form"
action="{{ page.route }}"
method="POST"
{{ form.fields|join|raw }}
<button type="submit">Send Message</button>
</form>
<script>
function trackFormSubmit() {
if (typeof fbq !== 'undefined') {
fbq('track', 'Contact', {
content_name: 'Contact Form',
content_category: 'Form Submission'
});
}
return true;
}
// Track successful submission
{% if grav.uri.query('sent') == 'true' %}
if (typeof fbq !== 'undefined') {
fbq('track', 'Lead', {
content_name: 'Contact Form',
value: 0.00,
currency: 'USD'
});
}
{% endif %}
</script>
Track Downloads
{# Track file downloads #}
<a href="{{ page.media['guide.pdf'].url }}" 'Lead Magnet')">
Download Free Guide (PDF)
</a>
<script>
function trackDownload(fileName, category) {
if (typeof fbq !== 'undefined') {
fbq('track', 'Lead', {
content_name: fileName,
content_category: category
});
}
}
</script>
Track Newsletter Signup
{# Newsletter signup form #}
<form id="newsletter-form" method="POST">
<input type="email" name="email" placeholder="Your email" required>
<button type="submit">Subscribe</button>
</form>
<script>
document.getElementById('newsletter-form').addEventListener('submit', function(e) {
if (typeof fbq !== 'undefined') {
fbq('track', 'CompleteRegistration', {
content_name: 'Newsletter',
status: 'subscribed'
});
}
});
</script>
E-commerce Tracking
If using Grav for e-commerce, track purchases and cart events.
View Content Event
{# Product page #}
{% block bottom %}
<script>
if (typeof fbq !== 'undefined') {
fbq('track', 'ViewContent', {
content_name: '{{ page.title }}',
content_ids: ['{{ page.header.product_id }}'],
content_type: 'product',
value: {{ page.header.price }},
currency: 'USD'
});
}
</script>
{% endblock %}
Add to Cart Event
<button product.id }}', '{{ product.title }}', {{ product.price }})">
Add to Cart
</button>
<script>
function addToCart(productId, productName, price) {
// Your cart logic here
// Track with Meta Pixel
if (typeof fbq !== 'undefined') {
fbq('track', 'AddToCart', {
content_ids: [productId],
content_name: productName,
content_type: 'product',
value: price,
currency: 'USD'
});
}
}
</script>
Purchase Event
{# Thank you page after purchase #}
{% if page.template == 'thank-you' %}
<script>
if (typeof fbq !== 'undefined') {
fbq('track', 'Purchase', {
content_ids: {{ order.items|map(i => i.id)|json_encode|raw }},
content_type: 'product',
value: {{ order.total }},
currency: 'USD',
num_items: {{ order.items|length }}
});
}
</script>
{% endif %}
Custom Events
Track custom events specific to your Grav site.
Blog Post Engagement
{# Track blog post reading #}
<script>
// Track when user scrolls to bottom
var trackingFired = false;
window.addEventListener('scroll', function() {
if (trackingFired) return;
var scrollPercentage = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
if (scrollPercentage > 90 && typeof fbq !== 'undefined') {
fbq('trackCustom', 'ArticleRead', {
article_title: '{{ page.title }}',
article_category: '{{ page.taxonomy.category|first ?: "General" }}'
});
trackingFired = true;
}
});
</script>
Video Views
{# Track video plays #}
<video id="featured-video" src="{{ page.media['video.mp4'].url }}" controls></video>
<script>
document.getElementById('featured-video').addEventListener('play', function() {
if (typeof fbq !== 'undefined') {
fbq('trackCustom', 'VideoView', {
video_title: '{{ page.title }}',
video_type: 'mp4'
});
}
});
</script>
Advanced Matching (Hashed User Data)
Improve matching with hashed customer information.
<script>
// Advanced Matching
{% set user_email = grav.user.email %}
{% if user_email %}
fbq('init', '{{ pixel_id }}', {
em: '{{ user_email|lower|md5 }}', // Hashed email
fn: '{{ grav.user.firstname|lower|md5 }}', // Hashed first name
ln: '{{ grav.user.lastname|lower|md5 }}' // Hashed last name
});
{% else %}
fbq('init', '{{ pixel_id }}');
{% endif %}
fbq('track', 'PageView');
</script>
GDPR Compliance
Conditional Loading with Consent
<script>
// Check for cookie consent
function hasMetaPixelConsent() {
return localStorage.getItem('meta_pixel_consent') === 'true';
}
// Load Meta Pixel only if consent given
if (hasMetaPixelConsent()) {
!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', '{{ pixel_id }}');
fbq('track', 'PageView');
}
</script>
Consent Management
{# Cookie consent banner #}
<div id="cookie-consent" style="display:none;">
<p>We use cookies for advertising. Accept to continue.</p>
<button
<button
</div>
<script>
function grantMetaPixelConsent() {
localStorage.setItem('meta_pixel_consent', 'true');
document.getElementById('cookie-consent').style.display = 'none';
location.reload(); // Reload to load pixel
}
function denyMetaPixelConsent() {
localStorage.setItem('meta_pixel_consent', 'false');
document.getElementById('cookie-consent').style.display = 'none';
}
// Show banner if no consent choice made
if (!localStorage.getItem('meta_pixel_consent')) {
document.getElementById('cookie-consent').style.display = 'block';
}
</script>
Testing & Verification
Use Meta Pixel Helper
Install Extension
- Install Meta Pixel Helper Chrome extension
Test Your Site
- Navigate to your Grav site
- Click the extension icon
- Verify pixel is firing
Check Events
- Look for green checkmarks
- Review event parameters
- Check for errors or warnings
Test Events Manager
- Go to Meta Events Manager
- Select your pixel
- Click Test Events
- Enter your website URL
- Interact with your site
- Verify events appear in real-time
Console Testing
// Check if Meta Pixel loaded
console.log(typeof fbq); // Should be 'function'
// Test custom event
fbq('trackCustom', 'TestEvent', {
test_param: 'test_value'
});
Debug Mode
<script>
// Enable Meta Pixel debug mode
{% if grav.config.system.debugger.enabled %}
if (typeof fbq !== 'undefined') {
fbq('track', 'PageView');
console.log('Meta Pixel fired: PageView');
}
{% endif %}
</script>
Common Issues
Pixel Not Firing
Possible Causes:
- Incorrect Pixel ID
- Ad blocker active
- JavaScript errors
- Pixel loaded after page content
Solutions:
- Verify Pixel ID in site.yaml or plugin config
- Test in incognito mode
- Check browser console for errors
- Ensure pixel loads in
<head>
Duplicate Events
Cause: Pixel code included multiple times.
Solution: Use conditional check:
{% if not pixel_loaded %}
{% set pixel_loaded = true %}
{# Pixel code here #}
{% endif %}
Events Not Matching
Cause: Advanced matching data incorrect.
Solution: Ensure proper hashing:
{# Correct hashing #}
em: '{{ user_email|lower|trim|md5 }}'
Multi-Language Sites
Track language in custom parameters:
{% set active_lang = grav.language.getActive ?: 'en' %}
<script>
fbq('track', 'PageView', {
content_language: '{{ active_lang }}',
content_locale: '{{ active_lang }}_{{ grav.config.site.default_lang|upper }}'
});
</script>
Next Steps
- Install Google Tag Manager - Manage multiple tracking pixels
- Set up Data Layer - Advanced tracking structure
- Troubleshoot Tracking - Debug tracking issues
For general Meta Pixel concepts, see Meta Pixel Guide.