Embed AdSense in Hugo/Astro Markdown Without Layout Shift (CLS 0.01)
Insert AdSense in static markdown posts via shortcodes and reserved div slots. Keeps CLS at 0.01 with 3 ad units per page.
Problem: Pasting AdSense code directly in Markdown broke on rebuild, caused CLS 0.22, and violated "don't edit generated HTML" rule. Solution: Shortcode (Hugo) or MDX component (Astro) with reserved container.
TL;DR
- Wrap ads in divs with fixed min-height before script loads.
- Hugo: shortcode partial; Astro: MDX component.
- Never inject ads without reserved space — causes CLS fail.
Hugo shortcode
layouts/shortcodes/adsense.html:
<div class="ad-slot ad-inarticle" style="min-height:280px;margin:1.5rem 0;text-align:center">
<ins class="adsbygoogle"
style="display:block"
data-ad-client="{{ .Site.Params.adsense.client }}"
data-ad-slot="{{ .Get 0 }}"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
</div>
<script>if(window.adsbygoogle)(adsbygoogle=window.adsbygoogle||[]).push({});</script>
In Markdown:
Intro paragraph...
{{< adsense "1234567890" >}}
More content...
Load main AdSense script once in baseof.html — not per shortcode.
Astro MDX component
src/components/AdSense.astro:
---
const { slot } = Astro.props;
const client = import.meta.env.PUBLIC_ADSENSE_CLIENT;
---
<div class="ad-slot" style="min-height:280px;margin:1.5rem 0">
<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>
MDX: <AdSense slot="1234567890" />
Placement rules that worked
| Position | RPM impact | CLS risk |
|---|---|---|
| After 1st paragraph | High | Low if min-height set |
| Mid-article (50%) | Highest | Medium |
Before closing ## section | Medium | Low |
| Before 1st paragraph | High RPM, kills LCP | High |
My sweet spot: after paragraph 2 + mid-article.
VitePress: custom container
::: adsense 1234567890
:::
Register in markdown.custom.containers with render function outputting reserved div.
ads.txt (don't forget)
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
Static file at static/ads.txt or public/ads.txt.
Auto ads vs manual units
I disabled Auto ads — they injected in unpredictable positions (CLS nightmare). Manual units only: sidebar + 2 in-article.
Common rejection reasons
- Insufficient content between ad units (< 300 words)
- Ads on 404 / tag pages
- Encouraging clicks ("support us by clicking ads")
Metrics
CLS with 3 units: 0.22 → 0.01
RPM: $3.20 → $4.85 (better placement + no auto ads chaos)
Ad fill rate: 91%
Treat ad slots like images: reserve space, load async, never surprise the layout.
FAQ
How do I add AdSense to Hugo markdown posts?
Create a layouts/shortcodes/adsense.html partial with a reserved-size container and async AdSense script.
Why does AdSense cause layout shift on static sites?
Empty divs collapse to 0 height until the ad loads, then expand. Set min-height matching ad unit size.
Can I use AdSense in Astro markdown?
Yes — use a custom MDX component or shortcode-style Astro component with client:visible for lazy loading.