The Stack
How to add AdSense to Astro without wrecking Core Web Vitals
Astro is one of the best places to run an ad site — fast by default, cheap at the edge. But almost every 'add third-party scripts to Astro' guide gives you advice that silently kills AdSense. Here's the version that actually works.
Astro is quietly one of the best foundations for an ad-supported site: it ships near-zero JavaScript, renders to static HTML you can host free at the edge, and gives you fast Core Web Vitals — which directly raise ad viewability and therefore RPM. This whole site runs on exactly that stack.
But there’s a landmine. Search “third-party scripts in Astro” and every result points you at Partytown. Follow that advice with AdSense and your ads will silently fail to appear. Let’s do it correctly instead.
Step 1 — Add and verify ads.txt
AdSense won’t serve at full fill rate until it can confirm you own the inventory. Astro serves anything in public/ at the site root, so create public/ads.txt:
google.com, pub-0000000000000000, DIRECT, f08c47fec0942fa0
Replace the publisher ID with your own (the pub-… value from your AdSense account). After deploy, confirm it resolves at https://yoursite.com/ads.txt.
Step 2 — Load the AdSense script (the right way)
Add the loader once, site-wide — in your base layout’s <head>. Use a normal async script:
---
// src/layouts/BaseLayout.astro
const ADSENSE_CLIENT = "ca-pub-0000000000000000";
---
<head>
<!-- ...existing head... -->
<script
async
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${ADSENSE_CLIENT}`}
crossorigin="anonymous"
></script>
</head>
That’s it for Auto ads. If you enabled Auto ads in your AdSense dashboard, Google now places units automatically.
Do NOT wrap this in Partytown
This is the mistake the generic guides bake in.
Partytown relocates third-party scripts into a web worker to keep the main thread free — genuinely useful for analytics. But ad scripts are DOM-dependent: AdSense needs the real window, needs to measure element widths, and needs to write ad markup into the page. A web worker has none of that, so the ads simply never render.
| Script | Partytown? | Why |
|---|---|---|
| Google Analytics | Fine | Fire-and-forget, no DOM writes |
| AdSense loader | Never | Needs DOM + full window to place ads |
| Consent Management | Never | Must run before ads, on the main thread |
Keep AdSense on the main thread. The async attribute already prevents it from blocking render — that’s the correct performance lever here, not Partytown.
Step 3 — A reserved-space ad unit (kills CLS)
For manual placements, wrap the <ins> tag in a component that reserves the slot’s height before the ad loads. This is the single biggest thing you control for Cumulative Layout Shift:
---
// src/components/AdUnit.astro
interface Props { slot: string; minHeight?: number; }
const { slot, minHeight = 280 } = Astro.props;
const client = "ca-pub-0000000000000000";
---
<div class="ad-slot" style={`min-height:${minHeight}px`}>
<ins
class="adsbygoogle"
style="display:block"
data-ad-client={client}
data-ad-slot={slot}
data-ad-format="auto"
data-full-width-responsive="true"
></ins>
</div>
<script is:inline>
(window.adsbygoogle = window.adsbygoogle || []).push({});
</script>
<style>
.ad-slot { display: grid; place-items: center; overflow: hidden; }
</style>
Use it in any page or post:
<AdUnit slot="1234567890" minHeight={280} />
Two things make this work: is:inline keeps Astro from bundling/deferring the push() call (it must run inline, per unit), and the min-height holds the space so content never jumps. Match the min-height to the unit you expect at each breakpoint:
| Unit | Typical render | Reserve (min-height) |
|---|---|---|
| Mobile anchor / 320×50 | 50–100px | 100px |
| In-content rectangle | 250–280px | 280px |
| Large mobile 336×280 | 280px | 300px |
| Desktop 300×600 sidebar | 600px | 600px |
The exact reserve is a judgment call — too little and you still shift, too much and you get an empty gap on unfilled impressions. Our ad-layout & CLS planner lets you toggle slots and watch CLS move before you ship.
Step 4 — Consent Mode v2 for EEA/UK traffic
If you get any European or UK visitors, AdSense requires a Google-certified consent banner, and the consent signal must load before the ad script. Put your CMP inline in <head>, above the AdSense loader — and, again, never in Partytown. Skipping this doesn’t just risk compliance; Google will throttle ad serving to unconsented EEA traffic, so it’s a revenue issue too.
One gotcha: View Transitions
Astro is multi-page by default, so each navigation is a fresh page load and ads initialize normally. But if you enable Astro’s View Transitions / client-side routing, the browser doesn’t reload — so your per-unit push() never re-fires and ads on the new page stay blank. Re-run it on navigation:
<script>
document.addEventListener("astro:page-load", () => {
document.querySelectorAll("ins.adsbygoogle:not([data-adsbygoogle-status])")
.forEach(() => (window.adsbygoogle = window.adsbygoogle || []).push({}));
});
</script>
Why bother doing this on Astro at all
Because speed is money in ads, twice over. A lean Astro build gives you the Core Web Vitals that lift viewability — and pages with sub-2.5s LCP average far higher ad viewability than slow ones (see the placement guide) — while costing you almost nothing to host at the edge. That’s the whole cost-inversion thesis: when infrastructure is nearly free, the fast stack wins on margin and on revenue. AdSense on Astro is that thesis in one file.
FAQ
- Can you run Google AdSense on an Astro site?
- Yes. Astro outputs static HTML, which AdSense supports fully — you add the AdSense loader script, a verified ads.txt file, and either Auto ads or manual <ins> ad units. Because Astro ships almost no JavaScript by default, an Astro ad site typically has better Core Web Vitals than a comparable WordPress site, which makes each ad impression more viewable and more valuable.
- Should I use Partytown for AdSense in Astro?
- No. Partytown moves third-party scripts into a web worker, and AdSense needs direct access to the DOM and the full window object to render ads and detect ad slots. In a worker it fails — ads don't appear. Partytown is fine for analytics like Google Analytics, but load the AdSense script normally with the async attribute on the main thread.
- How do I stop AdSense from causing layout shift (CLS) in Astro?
- Reserve the ad slot's space before the ad loads. Wrap each unit in a container with an explicit min-height matching the ad size for that breakpoint, so the page doesn't jump when the ad fills in. Unreserved ad slots are the number-one cause of poor CLS on ad sites.
Keep reading
- Add AdSense to Next.js The React equivalent — the next/script strategy and the routing fix.
- Add AdSense to Ghost Publisher-native, but watch the ads.txt and theme-reupload traps.
- Add AdSense to Hugo The other static-first stack, plus the v0.146 directory change.
- Add AdSense to Nuxt The only framework with a first-party module — and the proxy default to turn off.
- Add AdSense to SvelteKit Why its built-in CSP fights the ad code, and the afterNavigate double-push.
- Where to put ads (without wrecking your page) The placements and reserved-space rules that protect Core Web Vitals.
- You don't need WordPress to run an ad site Why a lean static/edge stack beats WordPress on cost and speed.