How to Integrate Pirsch with Astro: Complete Guide
Step-by-step guide to integrating Pirsch with your Astro website. Setup, configuration, and best practices.
How to Integrate Pirsch with Astro: Complete Guide
Pirsch is a privacy-friendly, cookie-free web analytics platform made in Germany. It tracks page views, referrers, devices, and other visitor metrics without using cookies or collecting personal data, making it GDPR compliant by default. Since Pirsch does not require cookie consent banners, it is a clean choice for Astro sites where you want analytics without the overhead of managing user consent flows.
This guide covers adding Pirsch to an Astro project using both the client-side script approach and the server-side API for more advanced tracking scenarios.
Prerequisites
Before starting, you will need:
- Node.js 22.12.0 or higher (Astro 6 dropped Node 18 and Node 20 support entirely; see the Astro upgrade guide cited below)
- An existing Astro project (this guide was checked against Astro 6.4.2)
- A Pirsch account (free trial available at pirsch.io)
- A website added to your Pirsch dashboard with an identification code (for the frontend script) and, for server-side tracking, an access key or a client ID + client secret
Pirsch works with static Astro sites. The default Astro output is fully static (prerendered), and the basic client-side script integration needs no adapter. Server-side tracking through middleware does require an adapter, covered in Method 2 below.
Installation
Pirsch does not require any npm packages for the basic script-based setup. You embed a lightweight tracking script directly in your HTML.
For server-side tracking, install the official SDK. The current version is pirsch-sdk 2.9.1:
npm install pirsch-sdk
Configuration
Method 1: Client-Side Script (Recommended)
The simplest integration method is adding the Pirsch tracking script to your base layout. The official docs say the snippet belongs in the <head> of your site. Get your identification code from the Pirsch dashboard integration settings.
---
// src/layouts/BaseLayout.astro
---
<html>
<head>
<meta charset="utf-8" />
<title>My Astro Site</title>
<script
defer
src="https://api.pirsch.io/pa.js"
id="pianjs"
data-code="YOUR_IDENTIFICATION_CODE"
></script>
</head>
<body>
<slot />
</body>
</html>
Replace YOUR_IDENTIFICATION_CODE with the code from your Pirsch dashboard. The id="pianjs" and data-code attributes are required by the script. It loads with defer, so it does not block page rendering.
Astro components are server-rendered to static HTML by default, so this <script> tag is emitted verbatim into every page that uses the layout. Astro does not bundle or process inline <script> tags that have a src pointing at an external origin, so no extra configuration is needed.
The script exposes several optional data-* attributes you can add to the same tag, including data-exclude and data-include (regex path filters), data-disable-query, data-disable-referrer, data-disable-resolution, data-disable-history (turns off automatic SPA navigation tracking), data-path-prefix / data-path-suffix, and data-dev (covered under Troubleshooting). See the frontend integration docs for the full list.
Using an Environment Variable
Store the identification code in .env:
PUBLIC_PIRSCH_CODE=YOUR_IDENTIFICATION_CODE
Then reference it in your layout:
---
const pirschCode = import.meta.env.PUBLIC_PIRSCH_CODE;
---
<html>
<head>
<script
defer
src="https://api.pirsch.io/pa.js"
id="pianjs"
data-code={pirschCode}
></script>
</head>
<body>
<slot />
</body>
</html>
The PUBLIC_ prefix is required: only variables that start with PUBLIC_ are exposed to client-side and template code through import.meta.env in Astro. The identification code is not a secret (it ships in the page source either way), so exposing it is expected.
Method 2: Server-Side Tracking
For Astro sites that run on demand, you can track page views server-side instead of using a client script. This approach is invisible to ad blockers and gives you more control over what gets tracked.
Server-side tracking only runs when a request actually hits your server, so the page must be rendered on demand rather than prerendered. In Astro 6 the entire site is prerendered (static) by default. To run middleware against real requests you need to either set output: 'server' in astro.config.mjs or add export const prerender = false to the routes you want rendered on demand, and you must install an adapter (such as @astrojs/node, @astrojs/vercel, @astrojs/cloudflare, or @astrojs/netlify). Note that output: 'hybrid' was removed in Astro 5: static is the default, and per-route control is done with the prerender export.
First, add your Pirsch credentials to .env. Pirsch lets you authenticate with either a single access key (write-only, recommended for sending hits) or a client ID plus client secret:
PIRSCH_ACCESS_KEY=your_access_key
PIRSCH_HOSTNAME=yourdomain.com
Create a Pirsch client utility. The SDK exposes a Pirsch class whose constructor accepts hostname plus either accessKey or clientId and clientSecret:
// src/lib/pirsch.ts
import { Pirsch } from 'pirsch-sdk';
let client: Pirsch | null = null;
export function getPirschClient() {
if (client) return client;
client = new Pirsch({
hostname: import.meta.env.PIRSCH_HOSTNAME,
accessKey: import.meta.env.PIRSCH_ACCESS_KEY,
// Or, with a client ID + secret instead of an access key:
// clientId: import.meta.env.PIRSCH_CLIENT_ID,
// clientSecret: import.meta.env.PIRSCH_CLIENT_SECRET,
});
return client;
}
The Pirsch docs recommend an access key over a client ID + secret when you only need write access, because it saves a roundtrip to refresh the access token.
Server-Side Middleware
Track every page view through Astro middleware. The SDK's hit() method takes a Hit object, and the hitFromRequest() helper builds that object from a Node-style request. Astro middleware gives you a standard Request, so the simplest portable approach is to construct the Hit fields directly from the request headers and pass them to hit():
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { getPirschClient } from './lib/pirsch';
export const onRequest = defineMiddleware(async ({ request }, next) => {
const response = await next();
// Only track HTML page views, not assets
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('text/html')) {
const pirsch = getPirschClient();
try {
await pirsch.hit({
url: request.url,
ip: request.headers.get('x-forwarded-for') || request.headers.get('cf-connecting-ip') || '',
user_agent: request.headers.get('user-agent') || '',
accept_language: request.headers.get('accept-language') || '',
referrer: request.headers.get('referer') || '',
});
} catch (error) {
// Silently fail, analytics should never break the page
console.error('Pirsch tracking error:', error);
}
}
return response;
});
Pass the full request.url rather than just the pathname so Pirsch can read query and UTM parameters. If you are running on Node with access to the raw IncomingMessage, you can replace the manual object with pirsch.hit(pirsch.hitFromRequest(req)) instead, which fills in the same fields for you.
Common Patterns
Tracking Custom Events
Pirsch supports custom events for tracking specific user interactions. When the pa.js snippet loads, it exposes a global pirsch() function. The first argument is the event name, followed by an options object whose recognized keys are duration (a number of seconds), meta (a key-value object), and non_interactive. The function returns a promise:
<button id="download-btn">Download Guide</button>
<script>
document.getElementById('download-btn')?.addEventListener('click', () => {
if (typeof pirsch === 'function') {
pirsch('Download Guide', {
duration: 0,
meta: { type: 'pdf' },
});
}
});
</script>
If you would rather send events through the SDK instead of the embedded snippet, the browser build lives at pirsch-sdk/web. Import the same Pirsch class, construct it with your identificationCode, and call client.event(name, durationSeconds, meta):
import { Pirsch } from 'pirsch-sdk/web';
const client = new Pirsch({ identificationCode: 'YOUR_IDENTIFICATION_CODE' });
await client.event('Download Guide', 0, { type: 'pdf' });
Handling Astro View Transitions
The Pirsch script tracks programmatic URL changes automatically by default, which the docs describe as useful for single-page applications and sites that use anchors. Astro's View Transitions (the <ClientRouter />) swap the page through the History API, so Pirsch records those navigations as new page views without any extra code. You generally do not need to wire up astro:page-load yourself, and adding a manual pirsch('pageview') call on top of the automatic tracking would double-count views. Only reach for a manual trigger if you have set data-disable-history to turn the automatic behavior off.
Excluding Routes from Tracking
If certain pages should not be tracked (admin pages, preview routes), filter them in your middleware:
const excludedPaths = ['/admin', '/preview', '/api'];
const shouldTrack = !excludedPaths.some(path => url.pathname.startsWith(path));
if (shouldTrack) {
await pirsch.hit({ /* ... */ });
}
Dashboard Filtering
Pirsch provides filtering in its dashboard for UTM parameters, referrers, countries, and custom events. Set up UTM tracking in your marketing links (e.g., ?utm_source=twitter&utm_campaign=launch) and Pirsch automatically parses and displays them.
Troubleshooting
No data in dashboard after setup: The Pirsch script ignores localhost requests by default. To test locally, add the data-dev attribute to the script and set it to a hostname configured on your dashboard, for example data-dev="example.com". Remove it before deploying so production hits are not rewritten to that hostname.
Script blocked by ad blockers: Some ad blockers may block api.pirsch.io. If this is a concern, use the server-side tracking approach which is completely invisible to browser extensions. Pirsch also offers a custom domain proxy feature for their script.
Page views counted twice: If you are using both the client-side script and server-side middleware, you will get duplicate hits. Choose one approach per project. The client-side script is simpler. Server-side tracking is more accurate but requires SSR.
Server-side IP address wrong: Behind reverse proxies, the IP address comes from headers like x-forwarded-for or cf-connecting-ip (Cloudflare). Make sure your middleware reads the correct header for your hosting setup. Pirsch needs the real visitor IP for geo-location and unique visitor counting.
Events not appearing: Custom events may take a few minutes to appear in the Pirsch dashboard. Also verify that the global pirsch function is available by checking that the script loaded successfully in your browser's network tab.
Common Errors and Fixes
import.meta.env.PUBLIC_PIRSCH_CODE is undefined in the browser: Astro only exposes environment variables prefixed with PUBLIC_ to client-side and template code. A variable named PIRSCH_CODE (no prefix) stays server-only and reads as undefined when the layout renders the script tag. Rename it to PUBLIC_PIRSCH_CODE.
Middleware never runs, or runs only at build time: Astro prerenders the whole site by default in Astro 6, and middleware does not execute for prerendered routes at request time. If your server-side hits never fire, confirm the route is rendered on demand (output: 'server', or export const prerender = false on the route) and that an adapter is installed. Without an adapter, astro build cannot produce a server entry at all.
Build fails after setting output: 'hybrid': That value was removed in Astro 5. Static is the default output, and you opt individual routes into on-demand rendering with export const prerender = false. Remove the hybrid value from astro.config.mjs.
Cannot find module 'astro:middleware': The astro:middleware virtual module is only available inside an Astro project at build time. It will not resolve in a plain Node script or a standalone test runner that is not running through Astro. Keep middleware logic inside src/middleware.ts.
Server hits rejected with an auth error: The SDK constructor needs hostname plus either an accessKey or both clientId and clientSecret. Mixing them up (for example passing an access key as clientSecret without a clientId) leads to failed token requests. Use a single access key for write-only tracking, which the docs recommend because it avoids the extra token-refresh roundtrip.
Duplicate page views: Running both the client-side script and server-side middleware double-counts. Pick one per project. The client script is simpler; server-side is ad-blocker-proof but needs an adapter.
Local hits never show up: Expected. The script ignores localhost unless you add data-dev (see Troubleshooting), and server-side hits from localhost are filtered too. Verify against a deployed environment.
Official Docs and Examples
- Pirsch frontend integration guide (the script snippet and all
data-*attributes): https://docs.pirsch.io/get-started/frontend-integration - Pirsch custom events reference (
pirsch()global function): https://docs.pirsch.io/advanced/events - Pirsch JavaScript SDK source and usage examples (Node and
pirsch-sdk/webbrowser builds): https://github.com/pirsch-analytics/pirsch-js-sdk - Astro on-demand rendering and adapters (static default,
prerenderexport): https://docs.astro.build/en/guides/on-demand-rendering/ - Astro middleware guide (
defineMiddleware,onRequest): https://docs.astro.build/en/guides/middleware/
Conclusion
Pirsch offers a straightforward analytics solution for Astro sites that values simplicity and privacy. The client-side script requires zero configuration beyond pasting a code snippet, while server-side tracking gives you ad-blocker-proof accuracy for on-demand routes. Since Pirsch does not use cookies, you can skip the consent banner entirely and focus on building your site. The dashboard provides the metrics that matter, such as page views, referrers, devices, and geographic data, without the complexity of Google Analytics.
Sources
Checked on 2026-05-29.
- pirsch-sdk latest version (2.9.1), npm registry: https://registry.npmjs.org/pirsch-sdk/latest
- pirsch-sdk on npm: https://www.npmjs.com/package/pirsch-sdk
- astro latest version (6.4.2), npm registry: https://registry.npmjs.org/astro/latest
- Astro v6 upgrade guide (Node 22.12.0 minimum, Node 18/20 dropped): https://docs.astro.build/en/guides/upgrade-to/v6/
- Pirsch frontend integration docs: https://docs.pirsch.io/get-started/frontend-integration
- Pirsch events docs: https://docs.pirsch.io/advanced/events
- Pirsch JavaScript SDK (GitHub): https://github.com/pirsch-analytics/pirsch-js-sdk
- Astro on-demand rendering guide: https://docs.astro.build/en/guides/on-demand-rendering/
- Astro middleware guide: https://docs.astro.build/en/guides/middleware/
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.