adsensethemes

The Stack

How to add AdSense to Eleventy without the ads.txt 404

Eleventy is a near-perfect ad-site engine — dead simple, fully static, no routing to fight. But it has one sharp edge that sends people in circles: your ads.txt silently never ships, because Eleventy doesn't copy static files unless you tell it to.

Living reference · Updated Jul 15, 2026

Eleventy is one of the simplest places to run an ad site: it outputs plain static HTML, ships nothing you didn’t write, and — because it’s fully static with hard page navigations — the “ads only load on the first page” bug that haunts SPA frameworks simply doesn’t exist here. Ad init just runs on every page load.

There’s exactly one thing that reliably sends people in circles, so let’s kill it first.

The gotcha: your ads.txt won’t ship by default

AdSense throttles fill until it can verify you own the inventory via ads.txt at your site root. On Eleventy, you create the file, deploy, and get a 404 — because Eleventy does not copy arbitrary static files to the output. It only processes files matching your templateFormats; everything else (ads.txt, robots.txt, images) is ignored unless you explicitly register a passthrough copy. (This is deliberate — blindly copying the whole project root is a leak risk, so it’s opt-in.)

Put ads.txt in your project root and register it in your Eleventy config:

// eleventy.config.js  (ESM — Eleventy 3.x)
export default function (eleventyConfig) {
  eleventyConfig.addPassthroughCopy("ads.txt");
}
// eleventy.config.js  (CommonJS)
module.exports = function (eleventyConfig) {
  eleventyConfig.addPassthroughCopy("ads.txt");
};

That lands the file at your output root, served at https://yoursite.com/ads.txt. A cleaner pattern if you have several root assets (favicons, robots.txt, ads.txt) is a public/ passthrough that maps to the root:

eleventyConfig.addPassthroughCopy({ "public": "." });

Then drop public/ads.txt and everything in public/ reaches the root.

Config filename note: Eleventy checks .eleventy.js first, then eleventy.config.js / .mjs / .cjs. .eleventy.js is the historical default, but eleventy.config.js is the modern, recommended name the current docs use — all are valid. On Eleventy 3.x you can use ESM (export default) throughout.

google.com, pub-0000000000000000, DIRECT, f08c47fec0942fa0

Step 1 — Load AdSense in your base layout

Eleventy is template-engine agnostic; Nunjucks is the common default. Edit your base layout (e.g. _includes/layouts/base.njk) and add the loader in <head>:

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>{{ title }}</title>

  {# Consent Mode / CMP goes here, before the loader (see below) #}

  <script async
    src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-0000000000000000"
    crossorigin="anonymous"></script>
</head>
<body>
  {{ content | safe }}
</body>

The async attribute keeps it off the critical path — that’s your CWV lever. Note the | safe on {{ content }}: Nunjucks auto-escapes by default, so layout body content needs it.

Step 2 — A reserved-space ad shortcode

A universal shortcode is the tidiest way to drop a parameterized unit into any template or markdown post. Register it in your config:

eleventyConfig.addShortcode("adunit", function (slot, minHeight = "280px") {
  return `<div class="ad-slot" style="min-height:${minHeight};display:block;text-align:center;">
  <ins class="adsbygoogle"
    style="display:block"
    data-ad-client="ca-pub-0000000000000000"
    data-ad-slot="${slot}"
    data-ad-format="auto"
    data-full-width-responsive="true"></ins>
  <script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
</div>`;
});

Call it from Nunjucks or markdown — shortcodes work in both:

{% adunit "1234567890" %}
{% adunit "1234567890", "336px" %}

The min-height wrapper reserves the slot so the ad doesn’t shove content down when it loads — the single biggest thing you control for CLS. Match it to the unit you expect (the placement guide has the reserve table). Shortcode output isn’t auto-escaped, so there’s no | safe needed at the call site. Prefer a plain include? An _includes/adunit.njk partial with the same markup works too, called with {% include "adunit.njk" %}.

Since 2024, Google requires a certified CMP (IAB TCF v2.2) plus Consent Mode v2 signals to serve ads to EEA, UK, and Swiss users. Place the CMP and a denied-by-default consent state in the base-layout <head> above the AdSense loader, so consent defaults are set before ads initialize. The low-friction path is AdSense’s own Privacy & messaging CMP, which injects the banner once you configure it in the dashboard. A common practitioner pattern: keep the loader in raw HTML so Google’s verification crawler always sees it, and let the CMP gate whether personalized ads actually serve.

Why Eleventy is a great fit for this

Because the two levers that make ads pay — speed and simplicity — are Eleventy’s whole personality. Static output behind a CDN gives you the Core Web Vitals that lift viewability, and full-page navigation means adsbygoogle.js initializes on every page with zero SPA gymnastics. Once you’ve cleared the passthrough-copy hurdle, it’s about as low-friction as an ad stack gets — the cost-inversion thesis in a few lines of config. Want the same static speed with a component model? Here’s the same setup on Astro; prefer Go templates? Hugo.

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

Why is my ads.txt returning 404 on Eleventy?
Because Eleventy does not copy arbitrary static files to the output directory by default — it only processes files matching your configured template formats. Anything else, including ads.txt, is silently ignored unless you register a passthrough copy with eleventyConfig.addPassthroughCopy("ads.txt") in your config. This is the single most common cause of a 404ing ads.txt on Eleventy.
What is the Eleventy config file called now?
Eleventy checks for the config in priority order: .eleventy.js first, then eleventy.config.js, eleventy.config.mjs, and eleventy.config.cjs. So .eleventy.js is still the historical default, but eleventy.config.js is the modern, recommended name that current 11ty docs use throughout — all four are valid. Eleventy 3.x also supports ESM config with export default.
Do I need to re-fire ads on navigation in Eleventy?
No — and that's an advantage. Eleventy outputs plain static HTML with full page loads, so there's no client-side router to fight. Each navigation is a fresh document, so the inline adsbygoogle.push({}) runs naturally on load with no re-init workarounds — unlike SPA frameworks such as Next.js or Nuxt.

Keep reading