How to Use PostHog with Astro: Complete Guide
Step-by-step guide to integrating PostHog with your Astro website.
PostHog is an all-in-one product analytics platform that goes far beyond pageview tracking. It includes session replay, feature flags, A/B testing, surveys, and a data warehouse, all in one tool. The free tier gives you 1 million events per month, which is generous enough that most startups and side projects never need to pay. For Astro developers building products (not just content sites), PostHog provides the kind of deep user insights that help you make better product decisions.
The integration involves adding PostHog's JavaScript snippet to your Astro site. From there, it automatically captures pageviews, clicks, and session data. You then add custom events for specific user actions you want to track.
Prerequisites
- Node.js 22.12.0 or newer. Astro 6 dropped support for Node 18 and 20 (both reached or are nearing end of life), so an older runtime will fail to install. Confirm with
node --version. - An Astro project (
npm create astro@latest). These instructions are written against Astro 6.4.2. - A PostHog account (free for 1 million events/mo, then usage-based pricing)
- Your PostHog project API key and instance host (US or EU cloud, or your self-hosted URL)
Installation
Install the PostHog JavaScript client. The current published version on npm is posthog-js@1.376.4:
npm install posthog-js
PostHog also ships a one-command setup wizard that drops the snippet in for you and can run inside Cursor, Bolt, or your terminal. It is the fastest path if you would rather not wire things by hand:
npx @posthog/wizard@latest
If you prefer a packaged Astro integration over a hand-rolled component, the community astro-posthog integration (astro-posthog@1.0.2, GPL-3.0, maintained by jop-software) wraps the same client behind npx astro add astro-posthog. It is not an official PostHog or Astro package, so weigh that before adopting it. This guide uses the official hand-rolled approach below, which PostHog documents as the recommended manual path.
Configuration
Get your API key and instance address from PostHog's project settings.
Add them to .env:
PUBLIC_POSTHOG_KEY=phc_your_api_key_here
PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
If you are self-hosting PostHog, replace the host with your instance URL.
Create a PostHog initialization component. PostHog's official Astro docs are specific about one thing: the snippet must run inside a <script is:inline> block. Without is:inline, Astro processes and bundles the script, which strips the global posthog reference and produces TypeScript and build errors. Inline scripts cannot read import.meta.env after bundling, so PostHog's documented pattern hardcodes the token and host directly in the inline snippet. If you want to keep the key in an env var, expose it through define:vars on a non-inline script instead, but note that inline is the approach PostHog itself recommends.
---
// src/components/PostHogAnalytics.astro
---
<script is:inline>
!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.async=!0,p.src=s.api_host+"/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||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing startSessionRecording stopSessionRecording sessionRecordingStarted loadToolbar get_property getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSurvey getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep".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('phc_your_api_key_here', {
api_host: 'https://us.i.posthog.com',
defaults: '2026-01-30',
});
</script>
A note on the defaults option. PostHog's current docs describe it as "a date, such as 2026-01-30, for a configuration snapshot used as defaults to initialize PostHog." It pins a point-in-time set of sensible defaults (including how pageviews and person profiles are handled) so future SDK releases do not silently change your tracking behavior. Any option you set explicitly overrides the snapshot, so you can still pass person_profiles: 'identified_only' or capture_pageview if you want to be explicit. Swap the host for https://eu.i.posthog.com if your project lives on EU Cloud, or your own URL if you self-host.
Add this to your base layout:
---
// src/layouts/BaseLayout.astro
import PostHogAnalytics from "../components/PostHogAnalytics.astro";
---
<html>
<head>
<PostHogAnalytics />
</head>
<body>
<slot />
</body>
</html>
Basic Usage
With PostHog initialized, it automatically captures pageviews and basic interactions. For custom event tracking, use the PostHog client in your components:
---
// src/components/PricingCard.astro
const { plan, price } = Astro.props;
---
<div class="pricing-card">
<h3>{plan}</h3>
<p class="price">${price}/mo</p>
<button class="subscribe-btn" data-plan={plan} data-price={price}>
Subscribe
</button>
</div>
<script>
document.querySelectorAll(".subscribe-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
const target = e.currentTarget as HTMLElement;
window.posthog?.capture("pricing_click", {
plan: target.dataset.plan,
price: target.dataset.price,
});
});
});
</script>
Using feature flags to conditionally show content:
---
// src/pages/index.astro
---
<div id="hero-section">
<h1 id="hero-title">Default Hero</h1>
<p id="hero-subtitle"></p>
</div>
<script>
window.posthog?.onFeatureFlags(() => {
const isNewHero = window.posthog?.isFeatureEnabled("new-hero-design");
if (isNewHero) {
const title = document.getElementById("hero-title");
const subtitle = document.getElementById("hero-subtitle");
if (title) title.textContent = "New Hero Design";
if (subtitle) subtitle.textContent = "This is the A/B test variant";
}
});
</script>
If you use Astro's client-side router (<ClientRouter />, the component formerly called <ViewTransitions />), the page never fully reloads on navigation, so the inline init snippet can run more than once and double-count. PostHog's docs recommend guarding initialization with a one-time flag and letting the SDK capture route changes itself:
---
// src/components/PostHogAnalytics.astro (ClientRouter variant)
---
<script is:inline>
if (!window.__posthog_initialized) {
window.__posthog_initialized = true;
// ...the same PostHog loader snippet as above...
posthog.init('phc_your_api_key_here', {
api_host: 'https://us.i.posthog.com',
defaults: '2026-01-30',
capture_pageview: 'history_change',
});
}
</script>
Setting capture_pageview: 'history_change' tells PostHog to fire a $pageview whenever the browser history changes, which is exactly what the client router triggers. This is cleaner than manually re-capturing on astro:after-swap and avoids the risk of double pageviews if both fire.
Identify users after authentication:
// After login
window.posthog?.identify(userId, {
email: user.email,
name: user.name,
plan: user.subscription,
});
// After logout
window.posthog?.reset();
Production Tips
Use
person_profiles: 'identified_only'to save on events. By default, PostHog creates anonymous person profiles for every visitor. Setting this toidentified_onlymeans person profiles are only created when you callidentify(), which dramatically reduces your event count on the free tier.Enable session replay selectively. Session replay is powerful for debugging but uses significant event quota. Enable it only for specific pages or user segments using PostHog's configuration options rather than globally across your entire site.
Use feature flags for gradual rollouts. Instead of deploying features and hoping for the best, use PostHog feature flags to roll out to 10% of users first. Monitor the impact in analytics, then increase to 100% when confident.
Set up key funnels. Define conversion funnels in the PostHog dashboard (e.g., landing page to signup to first action to subscription). This tells you where users drop off and what to fix first.
Self-host for data control. PostHog offers a self-hosted option via Docker. If you need full data ownership or have strict compliance requirements, self-hosting gives you all the features without sending data to a third party. Point api_host at your instance URL instead of the US or EU cloud hosts.
You do not need on-demand rendering just for PostHog. Because the snippet is a client-side inline script, it runs in the browser on a fully static page. Astro 6 keeps output: 'static' as the default and pre-renders everything, which is ideal for analytics. You only need a server adapter and on-demand rendering if you want to call PostHog server-side (for example, evaluating feature flags before sending HTML). See the rendering note below.
A Note on Astro 6 Rendering Modes
Older PostHog and Astro guides tell you to set output: 'hybrid' to mix static and server-rendered pages. That option no longer exists. Astro removed output: 'hybrid' in Astro 5, and Astro 6 continues with just two modes: 'static' (the default, where every page is pre-rendered to HTML at build time) and 'server' (where pages are rendered on demand by default). On-demand rendering is now controlled per page rather than through a third global mode.
To server-render a single route while keeping the rest of the site static, install an adapter and opt that page in:
npx astro add node # or netlify, vercel, cloudflare
---
// src/pages/dashboard.astro
export const prerender = false;
---
In the default 'static' build, pages are pre-rendered unless you set export const prerender = false. If you flip the whole project to output: 'server', the logic inverts and you mark static pages with export const prerender = true. The Astro docs advise staying on the default 'static' mode until you are sure most pages need on-demand rendering. For the client-side PostHog snippet in this guide, you do not need any of this; it works on a fully static build.
Common Errors and Fixes
- Build or TypeScript errors about a missing
posthogproperty. This is the single most common mistake. Astro bundles<script>tags by default, which breaks the globalwindow.posthogthe snippet relies on. The fix is theis:inlinedirective on the script tag, which tells Astro to leave the script untouched. PostHog's docs call this out explicitly. import.meta.envis undefined inside the inline snippet. Inline scripts are not processed by Astro's bundler, so they cannot readimport.meta.env. Either hardcode the token and host in the inline snippet (PostHog's documented pattern) or pass values in throughdefine:varson a separate non-inline script.- Double-counted pageviews with the client router. When you use
<ClientRouter />(formerly<ViewTransitions />), the document does not reload between pages, so the init snippet can run again and re-fire pageviews. Guard initialization with awindow.__posthog_initializedflag and let the SDK capture navigations viacapture_pageview: 'history_change'rather than re-runninginit. - Events going to the wrong region or never arriving. Match
api_hostto your project's data region:https://us.i.posthog.comfor US Cloud,https://eu.i.posthog.comfor EU Cloud, or your own URL when self-hosting. A US key pointed at the EU host (or vice versa) will silently fail to record events. npm installfails on the Astro project. Astro 6 requires Node 22.12.0 or newer. Node 18 reached end of life in April 2025 and Node 20 is scheduled to follow in April 2026, so upgrade your runtime if installs error out on the engine check.- No
$pageviewevents anywhere. Confirm the component is actually mounted in the<head>of your base layout and that the key starts withphc_. Open the browser network tab and look for requests to/static/array.jsand to yourapi_host; if they are absent, the snippet did not run.
Official Docs and Examples
- PostHog Astro library docs (manual setup,
is:inline,<ClientRouter>guard): https://posthog.com/docs/libraries/astro - PostHog Astro tutorial (analytics, feature flags, identify): https://posthog.com/tutorials/astro-analytics
- PostHog web analytics installation for Astro: https://posthog.com/docs/web-analytics/installation/astro
- PostHog JavaScript SDK reference (init options,
defaultssnapshot): https://posthog.com/docs/libraries/js - Astro on-demand rendering guide (output modes,
prerender, adapters): https://docs.astro.build/en/guides/on-demand-rendering/ - Astro 6 upgrade guide (Node 22 requirement, breaking changes): https://docs.astro.build/en/guides/upgrade-to/v6/
- Community Astro integration
astro-posthog(real example repo, GPL-3.0): https://github.com/jop-software/astro-posthog
Alternatives to Consider
- Fathom or Plausible if you only need pageview analytics and want a simpler, privacy-focused tool without the product analytics features.
- Mixpanel if you want a more established product analytics platform with deeper analysis tools and a focus on event-based tracking.
- Google Analytics if you need free analytics with advertising integration, though at the cost of more complexity and privacy concerns.
Wrapping Up
PostHog is the analytics tool for Astro developers building products, not just content sites. The combination of event tracking, session replay, feature flags, and A/B testing in one platform means fewer tools to manage and better data to act on. The free tier at 1 million events per month covers most early-stage projects comfortably, and the self-hosted option means you can scale without worrying about costs or data residency. If you are making product decisions and need more than pageview counts, PostHog gives you the depth to understand what your users actually do.
Sources
All versions and facts below were checked on 2026-05-29.
- posthog-js latest version (1.376.4): https://registry.npmjs.org/posthog-js/latest
- astro latest version (6.4.2): https://registry.npmjs.org/astro/latest
- astro-posthog latest version (1.0.2): https://registry.npmjs.org/astro-posthog/latest
- PostHog Astro library docs (
is:inline,defaults,<ClientRouter>guard, wizard): https://posthog.com/docs/libraries/astro - PostHog Astro tutorial (component, feature flags, identify): https://posthog.com/tutorials/astro-analytics
- PostHog JavaScript SDK docs (
defaultssnapshot definition, init): https://posthog.com/docs/libraries/js - Astro on-demand rendering docs (only
staticandservermodes,prerender, adapters): https://docs.astro.build/en/guides/on-demand-rendering/ - Astro 6 upgrade guide (Node 22.12.0 minimum): https://docs.astro.build/en/guides/upgrade-to/v6/
- Community astro-posthog integration repo (v1.0.2, GPL-3.0): https://github.com/jop-software/astro-posthog
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.