NNicheStackTutorials
← All tutorials
Ad Monetization

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.

·12 min read·Ad Monetization
Embed AdSense in Hugo/Astro Markdown Without Layout Shift (CLS 0.01) cover

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

PositionRPM impactCLS risk
After 1st paragraphHighLow if min-height set
Mid-article (50%)HighestMedium
Before closing ## sectionMediumLow
Before 1st paragraphHigh RPM, kills LCPHigh

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.

Tags: AdSenseMarkdownHugoAstroCLS