Quick Answer
To add Google AdSense to an Astro static site: (1) get approved first—you need real content and Privacy Policy, About, and Contact pages live before Google will say yes; (2) put an ads.txt file in public/ so it serves at your domain root; (3) load the AdSense script in your layout <head>, ideally behind a config flag so you can turn ads on and off in one place; (4) build one reusable ad-slot component and place units manually so they stay out of your code blocks; and (5) fix your Content Security Policy—this is the step that silently breaks ads, because AdSense needs both script-src and frame-src entries. The rest of this guide is the actual code for each step, based on how this blog is wired.
Who This Guide Is For
- Developers running a blog or docs site on Astro (or any static host) who want AdSense without a CMS plugin doing it for them
- Solo developers and indie hackers who already have an audience and want the lowest-effort monetization that does not require a backend
- Anyone who got approved but is staring at blank ad slots and cannot figure out why
If you are deciding which static site generator to use in the first place, I compared the options in Astro vs Gatsby for Developer Blogs. This guide assumes you have already picked Astro and shipped some posts.
Before You Touch Code: Get Approved
AdSense is the rare integration where the hard part happens before you write a line of code. Google has to approve the site, and a static Astro blog has no special advantage in that review—the content does the work.
What the 2026 review actually checks for:
- Sufficient original content. No fixed count is published, but new sites clear review far more reliably with roughly 20–30 substantial posts. Thin or auto-generated filler is the most common rejection.
- Essential pages, linked in the nav or footer: a Privacy Policy that explicitly mentions cookies and third-party ad vendors, plus About and Contact.
- HTTPS with a valid certificate (every modern static host gives you this for free).
- A submitted XML sitemap and a sane
robots.txt. ads.txtat the domain root—now treated as a standard requirement, not an afterthought.
Astro generates the sitemap, robots.txt, and canonical tags for you if you use the standard integrations, so on the technical side you are mostly there. The content and the legal pages are what gate approval.
The Setup
Here is the file layout this adds to a typical Astro project:
my-astro-blog/
├── public/
│ └── ads.txt # served at https://yoursite.com/ads.txt
├── src/
│ ├── site.config.ts # one place to flip ads on/off
│ ├── layouts/
│ │ └── BaseLayout.astro # loads the AdSense script in <head>
│ └── components/
│ └── AdsenseSlot.astro # reusable ad unit
└── netlify.toml # CSP that allows AdSense (host-specific)Step 1: Add ads.txt
Anything in Astro’s public/ directory is copied verbatim to the build output, so a file at public/ads.txt ends up at https://yoursite.com/ads.txt. That is exactly where AdSense looks for it.
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0Replace pub-XXXXXXXXXXXXXXXX with your own publisher ID. The trailing f08c47fec0942fa0 is Google’s certification ID and is the same for every AdSense publisher. Skip this file and AdSense will eventually flash an “Earnings at risk” warning in your dashboard.
Step 2: Gate everything behind a config flag
Do not sprinkle your publisher ID across templates. Put it in one config object with an on/off switch, so enabling or disabling ads is a one-line change.
// src/site.config.ts
export const site = {
baseUrl: 'https://yoursite.com',
// ...
adsense: {
enabled: false, // flip to true after approval + CSP
client: 'ca-pub-XXXXXXXXXXXXXXXX',
},
} as const;The enabled: false default matters more than it looks. It means you can commit the entire AdSense integration—script, slots, CSP—and ship it dark. Nothing renders until you flip one boolean. That keeps half-finished ad plumbing out of production and gives you an instant kill switch if something looks wrong.
Step 3: Load the script conditionally in your layout
The AdSense library goes in <head>, but only when ads are enabled. In your BaseLayout.astro:
---
import { site } from '../site.config';
---
<head>
<!-- ...the rest of your head... -->
{site.adsense.enabled && (
<script
is:inline
async
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${site.adsense.client}`}
crossorigin="anonymous"
></script>
)}
</head>is:inline tells Astro to leave the tag exactly as written instead of trying to bundle it, and async keeps the third-party script off your critical rendering path.
Step 4: Build one reusable ad slot
Instead of pasting AdSense’s <ins> snippet into every template, wrap it in a single component. This one also renders a labelled placeholder during local development, so you can see where ads will land without loading real ads:
---
// src/components/AdsenseSlot.astro
import { site } from '../site.config';
interface Props {
slot: string;
format?: string;
className?: string;
}
const { slot, format = 'auto', className = '' } = Astro.props;
const { enabled, client } = site.adsense;
const showPlaceholder = !enabled && import.meta.env.DEV;
---
{enabled ? (
<div class={`ad-slot ${className}`.trim()}>
<ins
class="adsbygoogle"
style="display:block"
data-ad-client={client}
data-ad-slot={slot}
data-ad-format={format}
data-full-width-responsive="true"
></ins>
<script is:inline>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>
</div>
) : showPlaceholder ? (
<div class={`ad-slot ad-slot--placeholder ${className}`.trim()} aria-hidden="true">
Ad slot · {slot}
</div>
) : null}Now placing an ad anywhere is one line, and you create the slot ID in the AdSense dashboard under Ads → By ad unit:
<AdsenseSlot slot="1234567890" />Step 5: Place units without wrecking the reading experience
For a developer blog, manual placement beats auto ads. Auto ads are a single snippet that lets Google drop ads wherever it likes, which on a code-heavy page means ads wedged between code blocks and a layout that jumps as they load. Manual units let you reserve the space and keep ads out of the flow.
A restrained layout that does not bury the content:
- One unit after the first H2, once the reader is committed
- One unit mid-article, at a natural section break
- One unit before the FAQ / related links, never inside a code block
Reserve a fixed height for each .ad-slot container in CSS so the ad does not shove text down when it arrives—that is the difference between a calm page and a bad CLS score.
Step 6: Fix the Content Security Policy (the step that breaks everyone)
This is where most “I got approved but ads are blank” stories end. If you send a Content Security Policy header—and you should—it has to allow both the AdSense script and the ad iframe. People remember script-src and forget frame-src, so the script loads, requests an ad, and then the ad silently fails to render inside its blocked iframe.
On Netlify, that policy lives in netlify.toml. The AdSense-relevant directives:
[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' https://pagead2.googlesyndication.com; img-src 'self' data: https:; frame-src https://googleads.g.doubleclick.net https://tpc.googlesyndication.com; object-src 'none'"The three hosts that matter:
| Directive | Host | Why |
|---|---|---|
script-src | pagead2.googlesyndication.com | loads adsbygoogle.js |
frame-src | googleads.g.doubleclick.net | the rendered ad iframe |
frame-src | tpc.googlesyndication.com | ad creatives / safeframe |
On Vercel use vercel.json headers, on Cloudflare Pages use _headers—same three hosts, different file. After approval, flip adsense.enabled to true, redeploy, and the slots fill.
A Real Project Note
On this blog, the integration above is fully wired: the AdsenseSlot component exists, the config flag is in place, public/ads.txt is served, and the CSP in netlify.toml already lists all three AdSense hosts. The only thing set to false is adsense.enabled.
That is deliberate, and it is the honest version of this guide: I built the entire pipeline first and left it dark, so turning ads on is a one-line commit and turning them off again is just as fast. I am not going to quote you an RPM I have not earned. What I can tell you is that doing the plumbing behind a single flag—rather than pasting snippets into templates under deadline—is the part I would not skip. The day you do flip it on, you want the change to be boring.
If you are building the blog itself with an AI agent, the same “one source of truth, one switch” instinct shows up everywhere; I wrote about that workflow in How I Use Claude Code to Build Small Web Apps and How I Run a Claude Code Writing Project Like Software.
Common Mistakes
- Applying with thin content. A few short posts is the top rejection reason. Build the library before you apply.
- Forgetting
ads.txt. It is easy to skip because nothing breaks immediately—then “Earnings at risk” appears and serving gets throttled. - CSP blocks the iframe. Adding
script-srcbut notframe-srcgives you blank slots and no error most people notice. - Auto ads on a code-heavy blog. Ads land between code blocks and shift the layout. Go manual.
- No reserved height. Ad containers with no fixed size cause layout shift and tank your CLS.
- Publisher ID pasted everywhere. When you want to disable ads—for a redesign, an audit, or a policy scare—you are now hunting through templates instead of flipping one flag.
Checklist
- Incomplete task: 20–30+ substantial posts published 20–30+ substantial posts published
- Incomplete task: Privacy Policy (mentions cookies + third-party ad vendors), About, Contact live and linked Privacy Policy (mentions cookies + third-party ad vendors), About, Contact live and linked
- Incomplete task: HTTPS, sitemap, and robots.txt in place HTTPS, sitemap, and
robots.txtin place - Incomplete task: public/ads.txt with your real pub- ID
public/ads.txtwith your realpub-ID - Incomplete task: Publisher ID and an enabled flag in one config file Publisher ID and an
enabledflag in one config file - Incomplete task: AdSense script loaded in <head>, conditional on the flag, async AdSense script loaded in
<head>, conditional on the flag,async - Incomplete task: One reusable AdsenseSlot component with a dev placeholder One reusable
AdsenseSlotcomponent with a dev placeholder - Incomplete task: Manual units placed outside code blocks, with reserved height Manual units placed outside code blocks, with reserved height
- Incomplete task: CSP allows pagead2.googlesyndication.com (script) and googleads.g.doubleclick.net + tpc.googlesyndication.com (frame) CSP allows
pagead2.googlesyndication.com(script) andgoogleads.g.doubleclick.net+tpc.googlesyndication.com(frame) - Incomplete task: Approved → flip enabled: true → redeploy → confirm slots fill Approved → flip
enabled: true→ redeploy → confirm slots fill
When Not to Use This Approach
AdSense is not always the right call:
- Low traffic. Below a few thousand monthly visits, AdSense pays in cents. Your time is better spent on content or a more direct offer.
- Conversion-focused pages. On a product, app, or lead-gen page, ads compete with the action you actually want and usually lose you more than they earn.
- Pre-launch sites. If you do not have the content and legal pages yet, you will be rejected; come back later.
- Better-fit monetization. For a developer audience, affiliate links to tools you genuinely use, a paid template, or your own product often out-earn display ads per visitor. AdSense is the floor, not the ceiling.
If your review process for AI-written code or content needs tightening before you scale up posting, I keep a separate AI Code Review Checklist for Solo Developers.
Related Articles
- Astro vs Gatsby for Developer Blogs — picking the static site generator under all of this
- How I Use Claude Code to Build Small Web Apps — the build workflow for the blog itself
- How I Run a Claude Code Writing Project Like Software — single-source-of-truth thinking applied to content
- AI Code Review Checklist for Solo Developers — keeping quality up as you publish more
Sources: AdSense Program policies, Eligibility requirements for AdSense. Approval requirements verified June 2026; AdSense policies change, so confirm current rules in the official docs before you apply.
Last updated: June 27, 2026.