AdSense Layout Optimization for Hugo Blogs (CTR & RPM)
In-article placement, sidebar sticky, and auto-ads off. Real CTR lift without template fork.
Tested on: Hugo 0.139.4, PaperMod, AdSense auto ads OFF, 90-day compare Baseline RPM: $4.20. After layout: $6.85. CTR: 0.9% → 1.4%.
Turn off auto ads first
Auto ads injected random positions — crushed reading flow, RPM actually dropped.
AdSense dashboard → Ads → Auto ads → Off for blog property.
Manual units only via shortcodes/partials.
Layout zones that worked
- Below title, above content (display responsive)
- After 3rd H2 (in-article)
- End of post (multiplex or display)
Did not work: header leaderboard on mobile (CTR 0.1%), floating footer (policy risk).
Shortcode for responsive display
layouts/shortcodes/adsense.html:
{{- if hugo.IsProduction -}}
<div class="adsense-slot adsense-{{ .Get "slot" }}">
<ins class="adsbygoogle"
style="display:block"
data-ad-client="ca-pub-XXXXXXXX"
data-ad-slot="{{ .Get "id" }}"
data-ad-format="{{ .Get "format" | default "auto" }}"
data-full-width-responsive="true"></ins>
</div>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>
{{- end -}}
Better: single push in partial after all slots rendered — reduced script executions.
Inject after 3rd H2 via render hook
layouts/_default/_markup/render-heading.html:
{{- $level := .Level -}}
<h{{ $level }} id="{{ .Anchor | safeURL }}">
{{ .Text | safeHTML }}
<a class="anchor" href="#{{ .Anchor | safeURL }}" aria-label="Anchor">#</a>
</h{{ $level }}>
{{- if and (eq $level 2) (eq .Ordinal 3) (hugo.IsProduction) -}}
{{ partial "ads/in-article.html" .Page }}
{{- end -}}
.Ordinal counts headings per level — verify with test post.
Sidebar sticky (desktop)
layouts/partials/sidebar-ads.html:
<div class="sidebar-ad-sticky">
{{ partial "ads/sidebar.html" . }}
</div>
@media (min-width: 1024px) {
.sidebar-ad-sticky {
position: sticky;
top: 5rem;
min-height: 600px;
}
}
ads.txt
static/ads.txt:
google.com, pub-XXXXXXXX, DIRECT, f08c47fec0942fa0
Must be at https://myblog.com/ads.txt.
Policy errors I hit
Ad serving limited — screens without content
Placed ad on tag pages with only 2-line descriptions. Removed ads from list templates:
{{ if and .IsPage (gt .WordCount 600) }}
{{ partial "ads/..." }}
{{ end }}
More ads than content on mobile
Capped 3 units per page on viewports < 768px via CSS + conditional partials.
Core Web Vitals
Load AdSense script once in head with async:
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXX" crossorigin="anonymous"></script>
Defer in-article push until slot near viewport (IntersectionObserver) — optional advanced step.
Verify
- AdSense → Sites → myblog.com → Approved
- PageSpeed: no "excessive third-party"
- Heatmap (Microsoft Clarity): clicks cluster on in-article unit
hugo --minify
# count adsbygoogle in public/post/sample/index.html — max 3
A/B result (n=60 days)
| Placement | Impressions | CTR |
|---|---|---|
| Auto ads | 420k | 0.7% |
| Manual optimized | 380k | 1.4% |
Lower impressions, higher earnings — better traffic quality signals.