adsensethemes

The Stack

How to add AdSense to Hugo without layout shift

Hugo is a near-ideal ad-site engine — fully static, blazing fast, no client-side routing to fight. The one thing that trips people up in 2026 is that Hugo moved its template directories, so every older AdSense-on-Hugo guide points you at paths that no longer exist.

Living reference · Updated Jul 15, 2026

Hugo is quietly one of the best engines for an ad-supported site: it renders to static HTML, ships nothing to the client you didn’t ask for, and — because it’s fully static with hard page navigations — it sidesteps the “ads only load on the first page” bug that plagues SPA frameworks. Ad init is genuinely simple here.

The one thing that trips people up in 2026 is a moving target the tutorials haven’t caught up to.

First: Hugo moved its template directories (v0.146)

Hugo v0.146.0 (March 2025) re-implemented the template system, and almost every AdSense-on-Hugo guide still ranking shows the old paths. For a current site, use the new ones:

PurposeNew (v0.146+)Legacy (pre-0.146)
Base templatelayouts/baseof.htmllayouts/_default/baseof.html
Partialslayouts/_partials/layouts/partials/
Shortcodeslayouts/_shortcodes/layouts/shortcodes/

The _default/ folder is gone (move its files up to layouts/), and partials/shortcodes gained an underscore prefix. Hugo maps old paths to new for backward compatibility, so many themes still build — but it isn’t fully transparent, so author new files at the new paths. On Hugo < 0.146, use the legacy paths in the right column.

Step 1 — Load AdSense in the head

Add the loader once, site-wide, in your head partial — layouts/_partials/head.html (called from layouts/baseof.html):

<script async
  src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-0000000000000000"
  crossorigin="anonymous"></script>

The async attribute keeps it off the critical rendering path — that’s your CWV lever. Load it once; don’t repeat it per unit. On a third-party theme (PaperMod, Blowfish, Docsy…), don’t edit the theme directly — use its head-extend hook (e.g. layouts/_partials/extend_head.html) so a theme update doesn’t wipe your change.

Step 2 — ads.txt in static/

Drop the file at static/ads.txt; Hugo copies everything in static/ to the site root at build:

google.com, pub-0000000000000000, DIRECT, f08c47fec0942fa0

Confirm it resolves at https://yoursite.com/ads.txt after deploy.

Step 3 — A reserved-space ad shortcode

Shortcodes are the right Hugo primitive for dropping an ad into markdown — and they have a hidden advantage (see the note below). Create layouts/_shortcodes/adunit.html:

<div class="ad-slot" style="min-height:280px;display:block;text-align:center;margin:1.5rem 0;">
  <ins class="adsbygoogle"
       style="display:block"
       data-ad-client="ca-pub-0000000000000000"
       data-ad-slot="{{ .Get "slot" }}"
       data-ad-format="{{ .Get "format" | default "auto" }}"
       data-full-width-responsive="{{ .Get "responsive" | default "true" }}"></ins>
  <script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
</div>

Call it from any post’s markdown:

Some paragraph before the ad.

{{</* adunit slot="1234567890" */>}}

Paragraph after.

Two things make this work. The wrapping <div> with an explicit min-height reserves the slot so the ad doesn’t shove content down when it loads — the single biggest thing you control for CLS. And .Get "slot" reads the named param you pass, with | default supplying fallbacks. Match the min-height to the unit you expect at each breakpoint (the placement guide has the reserve table).

The unsafe trap. goldmark strips raw HTML from markdown by default (markup.goldmark.renderer.unsafe = false). If you paste a raw <ins> block straight into a .md file, it vanishes — and the usual “fix” is to flip unsafe = true, which lowers your guard site-wide. You don’t need to: shortcode output isn’t subject to the raw-HTML filter, so the shortcode above renders with unsafe left safely off. Prefer the shortcode for exactly this reason.

EEA, UK, and Swiss traffic requires a Google-certified CMP, and the consent signal must load before the AdSense loader. In layouts/_partials/head.html, put the CMP (or AdSense’s own built-in Privacy & messaging banner) above the Step-1 loader. If you use AdSense’s built-in CMP, the existing loader handles the banner once you configure it in the dashboard.

Why Hugo is a strong choice for this

Because the two things that make ads pay — speed and simplicity — are Hugo’s defaults. Static output behind a CDN gives you the Core Web Vitals that lift viewability (fast pages average far higher ad viewability than slow ones), and full-page navigation means each adsbygoogle.js init just works, with no SPA re-init dance. Keep the loader on Google’s CDN via the async tag — don’t run it through Hugo Pipes or self-host it. That’s the cost-inversion thesis in practice: when the fast stack is also nearly free to host, it wins on margin and revenue. Prefer components and content collections? Here’s the same setup on Astro.

Free guide

Get the playbook

Drop your email and we'll send the AI-era publisher playbook — all nine chapters — straight to your inbox.

No spam. Unsubscribe anytime.

FAQ

Where do I put the AdSense script in Hugo?
In the head of your base template, loaded once site-wide. On Hugo v0.146 and later, add the async loader in layouts/_partials/head.html (called from layouts/baseof.html); on older Hugo it's layouts/partials/head.html. If you use a third-party theme like PaperMod, don't edit the theme — use its head-extend hook, such as layouts/_partials/extend_head.html, instead.
Do I need goldmark unsafe = true to add ads in Hugo?
Only if you paste raw <ins> HTML directly into a markdown file, because goldmark strips raw HTML by default. If you place ads with a shortcode instead — the idiomatic Hugo way — you do NOT need unsafe rendering, because shortcode output isn't subject to the raw-HTML filter. Prefer the shortcode and leave unsafe = false.
Where does ads.txt go in Hugo?
In the static/ directory as static/ads.txt. Everything in static/ is copied verbatim to the site root at build, so it publishes at yoursite.com/ads.txt where AdSense expects it. This behavior was unchanged by the v0.146 template overhaul, which only touched the layouts/ directory.

Keep reading