Skip to content
← Back to articles
7 min read

Privacy-First Analytics in Astro 6: A Zero-BS Guide

How to implement privacy-first, cookie-free Plausible analytics in Astro 6 without compromising your 100/100 Lighthouse score or annoying users with banners.

Privacy-First Analytics in Astro 6: A Zero-BS Guide
In this post

TL;DR: Modern analytics don’t require 500kb of JavaScript or invasive cookie banners. By leveraging Plausible, environment-gated components, and Astro 6, you can gather actionable intelligence while maintaining a minimalist, privacy-first architecture.


The default state of web analytics is broken. We trade user privacy, page load speed, and clean UI (hello, cookie banners) for vanity metrics we rarely act upon.

When I recently refactored the analytics stack for this portfolio, the mandate was simple: Zero cookies, zero banners, zero performance impact.

The solution? A privacy-first integration using Plausible Analytics and environment-gated Astro components.

Here is how to architect analytics that respect both your users and your Lighthouse score.

Table of Contents

The Problem with Legacy Analytics

Google Analytics (GA4) is a behemoth. It ships massive payloads, sets cross-site tracking cookies, and fundamentally requires you to implement annoying consent management platforms (CMPs) to remain GDPR compliant.

For an indie hacker or a developer portfolio, this is architectural overhead.

You don’t need a sledgehammer to count clicks.

You need:

  1. Pageviews.
  2. Referrers.
  3. Outbound link tracking.
  4. Core Web Vitals (optional).

Plausible delivers this in a <1kb script. It uses no cookies and stores no personal data.

Architecting the Astro 6 Integration

The key to a clean integration is ensuring the analytics script only runs in production, keeping your local development environment free from polluted data.

We achieve this using Astro’s built-in environment variables and a dedicated .astro component.

Step 1: The Environment Gate

First, define your domain in your production environment (e.g., Vercel, Netlify, or .env for local testing).

# .env
PUBLIC_PLAUSIBLE_DOMAIN="jordanthirkle.com"

Notice the PUBLIC_ prefix. This is critical in Astro to expose the variable to the client side if necessary, though we will primarily use it during the server-side build step.

Step 2: The Analytics Component

Instead of dumping a <script> tag directly into your base layout, abstract it. This gives you a single source of truth and makes it easy to swap providers later.

Create src/components/astro/Analytics.astro:

---
// src/components/astro/Analytics.astro

const domain = import.meta.env.PUBLIC_PLAUSIBLE_DOMAIN;
const isProd = import.meta.env.PROD;

// Only render the script if we have a domain AND we are in production
// (or if you explicitly want to test it locally)
const shouldRender = domain && isProd;
---

{shouldRender && <script defer data-domain={domain} src="https://plausible.io/js/script.js" />}

Why defer? Never block the main thread. By using defer, the browser downloads the script in the background and executes it only after the HTML parsing is complete. This protects your First Contentful Paint (FCP).

Step 3: Injection into the Main Layout

Now, simply drop the component into your <head> within your base layout.

---
// src/layouts/MainLayout.astro
import Analytics from '@/components/astro/Analytics.astro';
// ... other imports
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <!-- ... SEO tags, fonts, etc. -->

    <Analytics />
  </head>
  <body class="bg-zinc-950 text-zinc-300">
    <slot />
  </body>
</html>

Plausible’s base script tracks pageviews automatically. But as a developer, you want to know if people are actually clicking your GitHub links or hitting broken routes.

Plausible provides an extension script for this. Update your Analytics.astro component to use the script.outbound-links.js version.

---
// src/components/astro/Analytics.astro
const domain = import.meta.env.PUBLIC_PLAUSIBLE_DOMAIN;
const shouldRender = domain && import.meta.env.PROD;
---

{
  shouldRender && (
    <script defer data-domain={domain} src="https://plausible.io/js/script.outbound-links.js" />
  )
}

Now, every click leading away from your domain is automatically categorized as an “Outbound Link: Click” event in your dashboard. Zero manual event listeners required.

Tracking 404 Pages

To understand where your routing is failing, you need to fire a custom event when a user hits your 404.astro page.

Add a small inline script to your 404 page:

---
// src/pages/404.astro
import MainLayout from '@/layouts/MainLayout.astro';
---

<MainLayout title="404: Not Found">
  <div class="flex flex-col items-center justify-center min-h-[60vh]">
    <h1 class="text-4xl font-bold">404</h1>
    <p>We couldn't find that page.</p>
  </div>

  <!-- Fire Plausible 404 Event -->
  <script is:inline>
    if (window.plausible) {
      window.plausible('404', { props: { path: document.location.pathname } });
    }
  </script>
</MainLayout>

Note: We use is:inline here to ensure Astro doesn’t bundle this script. It needs to run exactly where it’s placed to fire the event immediately upon rendering the 404 page.

Architectural Diagrams

Here is a visual representation of how the conditional analytics injection works within the Astro 6 build pipeline.

graph TD
    A[Environment Variables] -->|import.meta.env| B(Analytics.astro Component)
    B --> C{isProd && Domain?}
    C -->|Yes| D[Inject Plausible Script]
    C -->|No| E[Render Empty Fragment]
    D --> F[MainLayout.astro]
    E --> F
    F --> G[Static HTML Output]

This diagram illustrates the simplicity of the approach. The check happens entirely on the server (or during the build process for SSG), meaning zero evaluation logic is ever shipped to the browser.

Dealing with Ad Blockers

It’s worth acknowledging that even privacy-first scripts like Plausible will often be blocked by aggressive ad-blockers (like uBlock Origin) if they use the standard plausible.io domain.

If achieving 100% data accuracy is critical, you can proxy the analytics requests through your own domain. Astro makes this relatively straightforward using edge functions or rewrite rules, though it falls outside the scope of a pure SSG deployment.

For most indie projects, accepting a 10-15% under-reporting rate is a fair trade-off for not having to manage a complex proxying infrastructure. The goal is directional accuracy, not perfect surveillance.

The Result: Clean Data, Clean Architecture

By implementing analytics this way, we achieve three critical outcomes:

  1. Perfect Performance: A <1kb deferred script does not impact Lighthouse scores. Your site remains instantaneous.
  2. User Respect: No cookie banners. No cross-site tracking. You respect your visitors’ privacy by default.
  3. Developer Ergonomics: Environment variables prevent local data pollution, and the component-based architecture keeps the MainLayout clean.

Analytics don’t have to be a necessary evil. When architected correctly, they become a silent, lightweight tool that helps you iterate without compromising your engineering standards.

Written by Jordan Thirkle

Stay-at-home dad building AI-accelerated products. I write code during naps and after bedtime — every post comes from real work, not theory.

X GITHUB LINKEDIN NEWSLETTER
0