Quick answer: Create a PostHog project, copy its web snippet from Project Settings, and add it across your site with a code manager plugin. Visit a page, then open Activity in PostHog and check that your pageview arrived.
We installed PostHog on our own WordPress site with HFCM in 2024. 99Ways now runs on Astro.
In this guide
How to Set Up PostHog A/B Testing on WordPress
Keep control visible until a variant is ready, and check every target before reading the flag so visitors who cannot receive the change stay out of the experiment.
What you need before you start
- Create your PostHog account, organization, and project.
- WordPress admin access to install and activate Header Footer Code Manager.
On WordPress.com, plugin installation is available on all paid plans: Personal, Premium, Business, and Commerce. (+) Free sites need a plan upgrade for this route.
The steps below use HFCM. To compare it with theme code, a dedicated plugin or a sync platform, see Which method fits your site? after Step 4.
Step 1: Grab the PostHog snippet
Copy the PostHog snippet from your project’s settings:
- Log in to your PostHog account.
- Navigate to Project Settings.
- Find the Web snippet section and copy the HTML snippet provided.

Copy the whole loader and its posthog.init call. Use your project’s token and region-specific ingestion host: https://us.i.posthog.com for US Cloud or https://eu.i.posthog.com for EU Cloud. (+) Keep the defaults date supplied by your project’s snippet; the example below uses 2026-05-30. (+) Use the public project token here, and keep personal API keys out of browser code. (+)
In 2024 we changed person_profiles to 'always'. We would now keep 'identified_only': it captures anonymous events without creating a person profile for every visitor. (+) Setting 'always' creates profiles for anonymous visitors too and raises the cost of billable events; see how to reduce identified-event costs.
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],Object.defineProperty(u,"toString",{configurable:!0,enumerable:!0,writable:!0,value:function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e}}),Object.defineProperty(u.people,"toString",{configurable:!0,enumerable:!0,writable:!0,value:function(){return u.toString(1)+".people (stub)"}}),o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagResult isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('YOUR_POSTHOG_PROJECT_TOKEN', {
api_host: 'https://us.i.posthog.com',
defaults: '2026-05-30',
person_profiles: 'identified_only',
// If capture must wait for consent, uncomment both lines (see Step 2):
// opt_out_capturing_by_default: true,
// opt_out_persistence_by_default: true,
})
</script>
With these settings PostHog captures pageviews and autocaptured clicks and form submissions; session replay, heatmaps and web vitals follow the toggles in your project settings.
Step 2: Add the snippet with HFCM
We used HFCM to add the snippet without editing theme files, so a theme update would not remove it.
The plugin adds the snippet at the WordPress level, so it does not depend on how individual pages are built. Your theme still needs to call wp_head() for header injection. (+)
Here are the HFCM settings we used:
- Install and activate HFCM, then open HFCM → Add New Snippet.
- Name the snippet PostHog and choose HTML as the Snippet Type.
- Set Site Display to Site Wide, Location to Header, and Device Display to Show on All Devices.
- Paste the full snippet, including the
<script>tags. Exclude pages that should not send analytics. - Connect the consent controls below before setting Status to Active and saving.

Connect the consent manager
HFCM adds the script, but it does not check whether a visitor has agreed to analytics. Connect your consent manager before turning capture on.
To require permission before capture, uncomment opt_out_capturing_by_default: true and opt_out_persistence_by_default: true in the snippet from Step 1, so nothing is captured and no analytics identifiers are stored before a grant. (+) Then add this function to the same HFCM snippet, in a second <script> block after the PostHog code. Call it from your consent manager’s consent-change callback, and apply the saved choice on each page load:
window.analyticsConsent = new Promise(resolve => {
window.allowAnalytics = resolve;
});
function onAnalyticsConsentChange(hasConsented) {
if (hasConsented) {
posthog.opt_in_capturing({ captureEventName: false });
window.allowAnalytics();
} else {
posthog.opt_out_capturing();
}
}
captureEventName: false stops each call from sending a $opt_in event, which would otherwise repeat on every page; to count acceptances, capture your own event from the banner’s accept button. (+) window.analyticsConsent stays pending until capture is allowed; our WordPress A/B testing guide uses it to hold a page experiment until then. If your policy also requires permission before the SDK loads, use the consent manager’s script blocking to hold back the full snippet instead. Then put the function’s first three lines in a separate HFCM snippet that the consent manager does not block, and call window.allowAnalytics() after posthog.init in the blocked one.
Test a first visit, refusal, a saved grant, a new grant, and withdrawal. Before a grant, check that the browser SDK sends no events or replay and stores no analytics identifiers; after a grant, expect capture to start. On withdrawal, confirm capture stops while the page is still open and stays stopped after a reload, and that PostHog’s cookies and localStorage data are removed. The consent choice itself stays stored, under __ph_opt_in_out_ followed by your project token, and a per-tab window ID remains in sessionStorage until the tab closes. Check server-side integrations separately: WP Zinc’s WooCommerce events follow PostHog’s cookie, not your consent manager.
Step 3: Verify the installation
Visit your website while logged out of WordPress, then open Activity in PostHog and find your pageview.
- Grant analytics consent if required. For HFCM, view the public page’s source and search for
posthog.initin the<head>. - Open your browser’s Network tab. Check that
array.jsloads and that event requests to your ingestion host or reverse proxy succeed. - Find your
$pageviewin Activity by its time and URL. Confirm one expected pageview per navigation, then repeat on mobile and a cached public page.
Keep your snippet in one place. Avoid adding duplicate PostHog snippets via multiple plugins, theme settings, or GTM; identify what an existing installation provides before disabling it.
After testing, filter your own visits out of reports so they do not get counted as customer activity.
Step 4: Enhance your analytics (optional)
We later added these PostHog transformations:
- User Agent Populator fills browser and device properties from a raw user agent. (+) It is useful for events that lack those details, such as server-side events; the browser snippet already sends them. (+)
- URL Normalizer, the older transformation we used to tidy URLs, is deprecated. (+) For a new setup, URL Normalization groups dynamic paths by replacing matching segments with
:id. (+) Test its rules so they preserve the URL differences your funnels need. - URL Parameters to Event Properties copies chosen query parameters into event properties. (+) UTM parameters already arrive with browser events, so use it for other parameters. Allowlist only the parameters you need: copying every query parameter can collect emails or tokens. Keep sensitive values out of the URL you send too.
Track a successful lead
Call posthog.capture from your form plugin’s confirmed-success callback, after it accepts the submission. A button click or generic form submit can fail validation or delivery. Define the success condition in your event tracking plan, then send the event. With Contact Form 7, listen for wpcf7mailsent: its wpcf7submit event also fires for invalid, spam and failed submissions. (+)
// Contact Form 7: runs only after the submission is accepted and sent.
document.addEventListener('wpcf7mailsent', event => {
window.posthog?.capture('lead_submitted', {
form_type: 'contact',
form_id: event.detail.contactFormId,
});
});
PostHog already adds $current_url and $pathname, so this example needs no extra page property. For another form plugin, use its equivalent success event. Check that one accepted submission produces one lead_submitted event, and that a rejected submission or a revisited confirmation page does not add another.
If your site has member logins and you call identify(), call posthog.reset() when the member logs out, so the next person on that browser starts as a new visitor. (+)
The same browser SDK supports session replay and heatmaps. Configure their capture settings in PostHog and verify recordings, masking, and heatmap data separately from your pageview check.
Once tracking is verified, learn how to run a PostHog A/B test on WordPress.
Which method fits your site?
There are four main approaches: a code manager, theme code, a dedicated integration plugin, or an external sync platform. A code manager leaves the complete SDK configuration under your control; a dedicated integration offers selected settings and prebuilt events. Google Tag Manager is another place to manage the browser snippet if your site already uses it. The comparison below is for October 2026.
| Method and maintainer | Version, updated, active installs | What it adds | WooCommerce events | Cookieless option | Limits |
|---|---|---|---|---|---|
| HFCM DraftPress Team | 1.1.46 15 July 2026 600,000+ 98/100 from 309 ratings | Free code manager; injects your full browser snippet. | Custom code or another integration required. | Set persistence: 'memory' or cookieless_mode: 'always' in your snippet. | You configure consent and custom events. |
| WPCode Syed Balkhi / WPCode | 2.4.0 8 October 2026 3,000,000+ | Free header/snippet route; injects your full browser snippet. | Custom code or another integration required. | Set persistence: 'memory' or cookieless_mode: 'always' in your snippet. | You configure consent and custom events. |
functions.php hookYour developer | Part of your theme | Prints the full snippet through wp_head. | Implement the events in code. | Set persistence: 'memory' or cookieless_mode: 'always' in your snippet. | A child theme protects parent-theme updates; switching themes removes the hook. |
| Integrate PostHog Web Analytics WP Zinc; unofficial | 1.1.6 20 May 2026 300+ | Generates the browser SDK configuration; optional reverse-proxy domain. | Product, cart, and checkout events, sent only when PostHog’s cookie exists. | Choose Persistence → Memory; memory does not supply the identity cookie its WooCommerce events require. | Settings expose fewer SDK options than a full snippet. |
| PostHog for WP Saad Malik; community-maintained | 1.4.1 27 May 2026 100+ | Injects the snippet you paste; tracking on/off toggle. | No built-in WooCommerce events in this version. | Edit the pasted snippet to use memory persistence. | You still configure consent and business events. |
| WP Full Picture Krzysztof Planeta | 10.1.2.3 13 October 2026 3,000+ | Tracking and consent manager with a PostHog installation module. | PostHog integration is installation only. | No memory-persistence setting in its PostHog module. | Custom PostHog events and SDK options need separate code. |
| Google Tag Manager Google; your team owns the tag | Not a WordPress plugin | Loads the browser snippet through a tag. | Add event instrumentation separately. | Set memory persistence in the snippet/configuration. | Requires the GTM container and consent setup on the site. |
| Zapier or another sync The sync provider | External service | Sends selected backend events. | Requires a matching WooCommerce trigger or custom workflow. | Does not install browser cookies; review the event data you send. | No browser SDK: no replay, heatmaps, or autocapture. |
We’d start with a code manager, or use functions.php when a developer maintains the theme in version control. Choose a dedicated plugin for WooCommerce events without code if you accept an unofficial maintainer. Use a sync when you only need backend events.
Adding the snippet through functions.php
Add this wrapper to your child theme’s functions.php, replacing the comment with the full snippet from Step 1 and its consent configuration. The add_action('wp_head', 'add_posthog', 999) hook places it before the closing head tag. (+) Use this instead of a second installation in HFCM; parent-theme updates preserve child-theme code, but a theme switch still needs a new placement.
if ( ! function_exists( 'add_posthog' ) ) :
function add_posthog() {
?>
<!-- Paste the full, consent-configured PostHog snippet here. -->
<?php
}
endif;
add_action('wp_head', 'add_posthog', 999);
Using a dedicated plugin
With Integrate PostHog Web Analytics, enter your Project API Key, Project ID, and Project region, then choose persistence. (+) Match the region to your PostHog project’s hosting region. Version 1.1.6 also has a Reverse Proxy Domain field, added after the screenshot below.

WP Full Picture combines analytics integrations and consent controls. For PostHog, set the project API key and the EU-hosting switch to match your project. (+) Its PostHog module is for installation; it does not supply the WooCommerce event mapping described below.

Sending events through a sync platform
You can use Zapier’s Capture Event action to send an event such as a new WordPress user to PostHog. This sends the event from the integration; it does not install PostHog in the browser, so it cannot provide session recordings, heatmaps or autocapture.
Track WooCommerce orders
The browser snippet captures pageviews and autocaptured interactions on WooCommerce pages. Order events need a PostHog-aware plugin or custom code.
For events without custom code, install and activate WooCommerce, then enable the events in WP Zinc’s WooCommerce tab. Integrate PostHog Web Analytics supports product views, cart additions and updates, cart views, checkout started, and checkout completed. (+) Its server-side events take the visitor ID from PostHog’s ph_phc_ cookie and are skipped when it is missing, so they need cookie persistence. (+) We have not tested version 1.1.6’s WooCommerce events; reconcile a test order in Activity with the order in WooCommerce before relying on them.
The plugin sends its checkout-completed event from WooCommerce’s thank-you page hook for the existing order. (+) It does not check payment or prevent a second event on a repeat visit, so revisit the thank-you page when testing. If you implement paid-order tracking yourself, use a server hook such as woocommerce_payment_complete and send the event from your backend. (+) In either case, prevent duplicates using the order ID and treat order creation and payment as separate actions in your tracking plan.
For an add-to-cart click goal, create a no-code action with the PostHog toolbar. Use the order or payment event to measure successful purchases.
Consent, caching, and troubleshooting
Clear caches and check script optimization
Clear WordPress, page-builder, optimization-plugin, and CDN caches after saving. Script-delay settings can postpone analytics until the visitor interacts, so test a logged-out page before clicking or scrolling. (+) If delay or script combination breaks delivery, exclude the PostHog snippet from that optimization while keeping consent handling in place.
| Symptom | Check |
|---|---|
| No events after permission is granted | Compare the project token and US/EU ingestion host with your project’s snippet. Check HFCM is active, the consent callback ran, and Network shows no blocked requests. Inspect ad blockers and Content Security Policy errors. |
| Duplicate pageviews | Look for duplicate snippets in HFCM, WPCode, theme settings, dedicated plugins, or GTM. Check whether custom code also sends a pageview alongside automatic capture. |
| Only admin or desktop visits appear | Check page/device scope, logged-out cached pages, and mobile delivery. |
| Events begin only after a click or scroll | Check the optimization plugin’s JavaScript-delay settings. |
| Form clicks appear but leads do not | Send the business event from the form’s success callback, then confirm the form system accepted the lead. |
| Pageviews arrive but replay or heatmaps do not | Check the feature’s capture settings, consent, and SDK configuration separately. |
To roll back, disable the new snippet, clear caches, and confirm capture stops. Once installation works, use our PostHog audit to check that events belong to the right visitors and conversion counts match your records.
Frequently asked questions
Is there an official PostHog WordPress plugin?
Does PostHog work with WooCommerce?
Yes. The same browser snippet works on shop pages. Choose a WooCommerce event integration or server-side order hook when you need product, cart, and purchase events.
Can PostHog run without cookies on WordPress?
Yes. Set persistence: 'memory' in your snippet, or choose Persistence → Memory in WP Zinc’s settings; memory does not supply the identity cookie that plugin’s WooCommerce events require. (+) Memory resets on each full page load, so anonymous visitors lose both returning-visitor history and continuity between pages. PostHog’s cookieless_mode is a different approach: enable Cookieless server hash mode in project settings and set cookieless_mode: 'always' in the snippet, and PostHog stores nothing in the browser and counts visitors with a hash calculated on its servers. (+) Consent still needs its own configuration.
Will a theme change remove PostHog?
A plugin-injected snippet survives the change. Code in the old theme’s header.php or functions.php stays with that theme, so you must add it to the replacement. A child theme protects custom code from parent-theme updates; verify the new theme renders the snippet through the expected WordPress hooks.
Does this work with WordPress Multisite and subdomains?
Yes, with site-by-site configuration: activate HFCM on each subsite, not network-wide. (+) Use one PostHog project for shared analysis or separate projects for separate datasets. With localStorage+cookie persistence and cross_subdomain_cookie: true (the defaults), the identity cookie can span sibling subdomains. (+) Consent choices are not shared by default; configure them separately. (+)
Will PostHog slow down my WordPress site?
The script loads asynchronously, so the browser can keep parsing the page while it downloads. (+) It still uses network and CPU time. Measure your pages before and after installation, including the capture features you enable.
