How to Integrate Google Analytics with Astro: Complete Guide
Step-by-step guide to integrating Google Analytics with your Astro website. Setup, configuration, and best practices.
How to Integrate Google Analytics with Astro: Complete Guide
Google Analytics 4 (GA4) is the standard web analytics platform for tracking user behavior, traffic sources, conversions, and engagement metrics. Adding GA4 to your Astro site lets you understand how visitors interact with your content, which pages perform best, and where your traffic comes from. Since Astro outputs static HTML by default, the integration involves adding the GA4 tracking script to your pages and optionally configuring it for more advanced tracking scenarios.
This guide covers adding GA4 to an Astro project, handling view transitions, setting up custom events, and respecting user privacy preferences.
Prerequisites
You will need:
- Node.js 22.12.0 or higher installed. Astro 6 dropped Node 18 and Node 20 support entirely, so older runtimes will fail to install or build. If you are still on Astro 4 or 5, Node 18.20.8+ or 20.3.0+ is acceptable, but matching the Astro 6 requirement keeps you future-proof.
- An existing Astro project. The snippets here work on Astro 4, 5, and 6.
- A Google Analytics 4 property created at analytics.google.com
- Your GA4 Measurement ID (starts with
G-, found in Admin > Data Streams > Web)
No SSR adapter is required. Google Analytics works with the default static output. Note that since Astro 5, output: 'static' is the default and the old output: 'hybrid' mode was removed (its mixed prerender behavior was folded into 'static'). You only need an adapter if you opt specific routes out of prerendering with export const prerender = false, which analytics does not require.
Installation
There is no npm package required to load GA4 itself. GA4 uses a script tag loaded directly from Google's servers. The official, supported way to keep that script off the main thread is the first-party Astro Partytown integration:
npx astro add partytown
That command installs @astrojs/partytown (current version 2.1.7), wires it into astro.config.mjs, and pulls in its runtime dependency @qwik.dev/partytown (resolved version 0.13.2). The Partytown runtime moved from Builder.io to the Qwik team, so the older @builder.io/partytown package is deprecated; the integration now depends on @qwik.dev/partytown. The integration is published by the Astro core team, so prefer it over any third-party analytics plugin.
If you would rather add it by hand, install the package and register the integration yourself:
npm install @astrojs/partytown@2.1.7
Partytown is optional but recommended. It relocates the analytics script into a web worker, preventing it from blocking the main thread during page render. This improves Core Web Vitals such as Total Blocking Time.
Configuration
Basic Setup Without Partytown
The simplest approach is adding the GA4 script directly to your layout component:
---
// src/layouts/BaseLayout.astro
const GA_ID = 'G-XXXXXXXXXX'; // Replace with your Measurement ID
---
<html>
<head>
<script async src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}></script>
<script define:vars={{ GA_ID }}>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', GA_ID);
</script>
</head>
<body>
<slot />
</body>
</html>
The define:vars directive passes the Astro variable into the inline script without breaking the build.
Setup With Partytown
For better performance, use Partytown to run the analytics script off the main thread:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import partytown from '@astrojs/partytown';
export default defineConfig({
integrations: [
partytown({
config: {
forward: ['dataLayer.push'],
},
}),
],
});
Then update your layout to use the type="text/partytown" attribute:
---
// src/layouts/BaseLayout.astro
const GA_ID = 'G-XXXXXXXXXX';
---
<html>
<head>
<script
type="text/partytown"
async
src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
></script>
<script type="text/partytown" define:vars={{ GA_ID }}>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', GA_ID);
</script>
</head>
<body>
<slot />
</body>
</html>
Partytown intercepts the scripts and runs them in a web worker, keeping the main thread free for user interactions.
Using Environment Variables
Store the measurement ID in your .env file to keep it out of source code:
PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
Then reference it in your layout:
---
const GA_ID = import.meta.env.PUBLIC_GA_MEASUREMENT_ID;
---
Common Patterns
Handling Astro Client-Side Routing (View Transitions)
If your Astro site uses client-side routing, page navigations happen without full page reloads, so GA4 will not automatically register them as new page views. In current Astro the relevant component is <ClientRouter />, imported from astro:transitions. It was named <ViewTransitions /> in Astro 3 and was renamed to <ClientRouter /> in Astro 5, so update any older copy-pasted snippet accordingly. Add the component to your shared head, then listen for Astro's lifecycle event:
---
// src/layouts/BaseLayout.astro
import { ClientRouter } from 'astro:transitions';
---
<head>
<ClientRouter />
</head>
<script>
document.addEventListener('astro:page-load', () => {
if (typeof gtag === 'function') {
gtag('event', 'page_view', {
page_title: document.title,
page_location: window.location.href,
});
}
});
</script>
Place the listener in your base layout so it runs on every page. The astro:page-load event fires on both the initial page load and every subsequent client-side navigation (forward or backward), which makes it the right hook for a single-page-application style page view.
Custom Event Tracking
Track specific user interactions by sending custom events:
<button id="signup-btn">Sign Up Free</button>
<script>
document.getElementById('signup-btn')?.addEventListener('click', () => {
gtag('event', 'sign_up_click', {
event_category: 'engagement',
event_label: 'hero_section',
});
});
</script>
Cookie Consent Integration
To comply with GDPR and similar regulations, use Google Consent Mode. The key rule from Google's own guidance is that gtag('consent', 'default', ...) must run before any config or event command and before the gtag.js library loads, so the tag knows to hold storage until the user decides:
<script define:vars={{ GA_ID }}>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
// Set defaults to denied BEFORE loading gtag.js
gtag('consent', 'default', {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
});
// Now load gtag.js
const script = document.createElement('script');
script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`;
script.async = true;
document.head.appendChild(script);
gtag('js', new Date());
gtag('config', GA_ID);
// Call this function when the user accepts in your cookie banner
window.grantAnalyticsConsent = function () {
gtag('consent', 'update', {
analytics_storage: 'granted',
});
};
</script>
This loads GA4 in a cookieless, consent-pending state and only enables analytics storage after the user grants consent. When the user clicks "Accept" in your cookie banner, call window.grantAnalyticsConsent(). If you also run Google Ads, include the ad_storage, ad_user_data, and ad_personalization keys in the update call as well, matching the defaults you declared above.
Troubleshooting
No data appearing in GA4 dashboard: GA4 has a processing delay of up to 24-48 hours for standard reports. Use the Realtime report to verify the script is working. Also check that you are using the correct Measurement ID (starts with G-, not UA-).
Partytown script not executing: Partytown requires the forward config to include dataLayer.push. Without this, gtag calls silently fail. Also make sure your build output includes the Partytown library files in the ~partytown directory.
Duplicate page views: If you are using <ClientRouter /> along with the standard GA4 setup, you can get double counting on the first load (gtag's automatic page view plus your manual astro:page-load event). Either pass send_page_view: false inside gtag('config', GA_ID, { send_page_view: false }) so the manual listener is the only source, or drop the custom listener and let GA4 handle it. Do not do both at full strength.
GA4 blocked by ad blockers: Many users run ad blockers that prevent GA4 from loading. Consider a server-side analytics solution like Plausible or Umami as a supplement if you need more accurate numbers. GA4 data should be treated as a sample rather than an exact count.
Core Web Vitals impact: Without Partytown, the GA4 gtag.js script is heavy, on the order of 135KB gzipped over the wire per Plausible's measurement, and it runs on the main thread. If your Lighthouse scores drop after adding analytics, enable Partytown or load the script with a setTimeout to defer it after page interaction.
Common Errors and Fixes
npx astro add @astrojs/partytown does nothing or errors: The astro add command takes the short integration name, not the full package name. Run npx astro add partytown. The command resolves and installs @astrojs/partytown for you and edits astro.config.mjs.
gtag is not defined inside a Partytown script: Partytown runs scripts in a web worker, so the worker cannot reach the main-thread dataLayer unless you forward it. Make sure your config includes forward: ['dataLayer.push']. Without it the gtag calls are dropped silently.
Partytown library files return 404 in production: The integration writes its runtime into a ~partytown directory at build time. If your host rewrites or strips that path, the worker fails to boot. Confirm ~partytown/ is present in your build output and served as static files. During local debugging, Partytown's debug logging is on by default in dev and preview, so check the console there before deploying.
output: 'hybrid' throws "invalid option": That mode was removed in Astro 5. The default output: 'static' already supports mixing prerendered and on-demand pages, so simply remove the output: 'hybrid' line. Analytics needs no output change at all.
Build fails right after upgrading to Astro 6: Astro 6 requires Node 22.12.0 or higher and drops Node 18 and 20. Check node -v locally and confirm your deploy platform's runtime. Pinning 22.12.0 in an .nvmrc keeps local and CI in sync.
<ViewTransitions /> import not found: The component was renamed to <ClientRouter /> (imported from astro:transitions) in Astro 5. Update the import and the tag.
Consent Mode not holding cookies back: The gtag('consent', 'default', ...) call must run before gtag.js loads and before any config/event call. If it runs after, the defaults arrive too late and the tag may already have written storage.
No data in GA4: Standard reports lag up to 24 to 48 hours. Use the Realtime report to confirm hits, and double-check you pasted a G- Measurement ID, not a legacy UA- Universal Analytics ID (Universal Analytics stopped processing data on 2023-07-01).
Official Docs and Examples
- Astro Partytown integration guide: https://docs.astro.build/en/guides/integrations-guide/partytown/
- Partytown integration source (example config and code): https://github.com/withastro/astro/tree/main/packages/integrations/partytown
- Astro client-side routing and
astro:page-loadevent: https://docs.astro.build/en/guides/view-transitions/ - Astro v6 upgrade guide (Node requirement, adapter changes): https://docs.astro.build/en/guides/upgrade-to/v6/
- Google gtag.js install snippet: https://developers.google.com/tag-platform/gtagjs
- Google Consent Mode setup: https://developers.google.com/tag-platform/security/guides/consent
Conclusion
Google Analytics 4 integrates cleanly with Astro through a simple gtag.js script tag in your base layout. Use the official Partytown integration for better performance, handle client-side routing with the astro:page-load event and the <ClientRouter /> component, and implement Consent Mode for privacy compliance. For most Astro sites, the environment variable approach with the basic script tag provides everything you need. Start with the Realtime report to verify your setup before diving into the full analytics dashboard.
Sources
All package versions and technical claims were verified on 2026-05-29 against the following sources:
- npm registry,
@astrojs/partytownlatest (version 2.1.7): https://registry.npmjs.org/@astrojs/partytown/latest - npm registry,
@qwik.dev/partytown(runtime dependency of@astrojs/partytown, resolved version 0.13.2): https://registry.npmjs.org/@qwik.dev/partytown - npm registry,
astrolatest (version 6.4.2, engines node >=22.12.0): https://registry.npmjs.org/astro/latest - Astro Partytown integration guide (install command, forward/debug config): https://docs.astro.build/en/guides/integrations-guide/partytown/
- Astro v6 upgrade guide (Node 22.12.0 requirement, drops Node 18/20): https://docs.astro.build/en/guides/upgrade-to/v6/
- Astro v5 upgrade guide (
output: 'hybrid'removed, merged into'static'): https://docs.astro.build/en/guides/upgrade-to/v5/ - Astro client-side routing docs (
<ClientRouter />,astro:page-load): https://docs.astro.build/en/guides/view-transitions/ - Google gtag.js install snippet: https://developers.google.com/tag-platform/gtagjs
- Google Consent Mode parameters and ordering: https://developers.google.com/tag-platform/security/guides/consent
- GA4 gtag.js script size (135KB gzipped over the wire): https://plausible.io/google-analytics-script-size
- Astro Partytown integration source / example: https://github.com/withastro/astro/tree/main/packages/integrations/partytown
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.