adsensethemes

The Stack

How to add AdSense to Nuxt — and the default that quietly costs you money

Nuxt has something no other framework in this series does: an official AdSense integration. It's genuinely good. It also ships with a privacy proxy enabled by default that routes your ad traffic through your own server and strips visitor IPs before Google sees them — which is exactly what you don't want if the ads are paying your bills.

Living reference · Updated Jul 25, 2026

Every other framework in this series makes you wire AdSense up yourself. Nuxt doesn’t — @nuxt/scripts has a Google AdSense entry in its script registry, with a composable, a component, and automatic site verification. It’s the best out-of-the-box story of any framework we’ve covered.

It also has one default that is wrong for ad publishers specifically, and it’s on unless you turn it off. Start with the setup, then read Step 4 before you deploy.

Verified against Nuxt 4.5 (released 18 July 2026) and the current Nuxt Scripts registry.

Step 1 — ads.txt in public/

Anything in Nuxt’s public/ directory is served from the domain root, so create public/ads.txt:

google.com, pub-0000000000000000, DIRECT, f08c47fec0942fa0

After deploy, confirm it resolves at https://yoursite.com/ads.txt. AdSense checks the root of your registered domain, so make sure apex and www both resolve or 301 one to the other. This is a static file — don’t try to serve it from a server/ route.

Step 2 — Install the module and register AdSense

npx nuxi module add scripts

Then configure the registry entry in nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['@nuxt/scripts'],
  scripts: {
    registry: {
      googleAdsense: {
        client: 'ca-pub-0000000000000000',
        trigger: 'onNuxtReady',
        // See Step 4 — these two matter more than they look.
        proxy: false,
        bundle: false,
      },
    },
  },
})

Two things happen for free here. Providing client makes Nuxt auto-insert the AdSense site-verification meta tag, so you can skip that step of onboarding. And trigger: 'onNuxtReady' loads the script globally once the app is ready — Nuxt’s equivalent of Next.js’s afterInteractive, and the right tier for an ad loader. Without a trigger, the registry entry stays dormant until you call the composable yourself.

Step 3 — A reserved-space ad unit

Use the <ScriptGoogleAdsense> component, and reserve the slot’s height:

<!-- components/AdUnit.vue -->
<script setup lang="ts">
const { public: { adsenseClient } } = useRuntimeConfig()
defineProps<{ slot: string }>()
</script>

<template>
  <div :style="{ minHeight: '280px' }">
    <ScriptGoogleAdsense
      :data-ad-client="adsenseClient"
      :data-ad-slot="slot"
      data-ad-format="auto"
      data-full-width-responsive="true"
    >
      <template #error>
        <!-- Shown when an ad blocker stops the script. Keep it quiet. -->
        <span class="text-sm opacity-60">Ad could not load.</span>
      </template>
    </ScriptGoogleAdsense>
  </div>
</template>

The minHeight wrapper is doing the Core Web Vitals work: an ad that arrives into zero reserved height shoves the page down and wrecks CLS, which is the metric that gates viewability and therefore revenue. Match the reserve to the unit you expect — the placement guide has the reserve-height table.

The #error slot is a genuinely nice touch that other frameworks don’t hand you: a declarative fallback for ad-blocked visitors, instead of a collapsed empty box.

Note the explicit data-ad-client. You already set client globally in Step 2, and it looks redundant — it isn’t. The global value is read by the loader, but the component doesn’t read it, so it warns Missing required prop: 'dataAdClient' and renders nothing useful. This was filed as nuxt/scripts issue #345 and closed without a fix, so treat it as intended: pass it per instance. Pull it from runtimeConfig so the id still lives in exactly one place.

Step 4 — Turn off the privacy proxy (the important one)

This is the part that has real money attached, and it’s why this page exists.

@nuxt/scripts has a first-party mode: at build time it downloads the third-party script, serves it from your own domain as /_scripts/assets/[hash].js, and reverse-proxies the runtime requests through Nitro routes at /_scripts/p/ — rewriting vendor domains in the bundled source and patching fetch, sendBeacon, XMLHttpRequest, and Image to catch dynamic URLs. The stated goal is to reduce fingerprinting and keep visitor IPs away from the vendor. Nuxt’s docs place Google AdSense in a privacy tier where this is enabled by default.

For Plausible or a heatmap tool, that’s a reasonable trade and arguably the point of the feature. For AdSense it works directly against you:

  • Ad pricing runs on the signals you’d be stripping. Geography and device are a large part of what an impression is worth — it’s why RPM varies so widely by country (see CPC vs CPM vs RPM). Anonymising visitor data before Google sees it degrades exactly the inputs that set your rate.
  • Every ad request arrives from one IP — your server’s. Invalid-traffic detection is pattern-matching on request behaviour. A single origin issuing all of a site’s ad traffic is not the shape of a normal audience.
  • The loader is rewritten, not just relocated. Bundling rewrites domains inside the script source. AdSense’s terms are unambiguous about not modifying the ad code.
  • Google says no to this pattern elsewhere. Nuxt’s own bundling docs carve out Stripe and reCAPTCHA as must-not-bundle, citing Google’s position that self-hosted copies of its scripts are unsupported. We found no Google statement approving it for AdSense either.

To be straight about the limits of that list: Google has not, as far as we can find, published a ruling on proxying adsbygoogle.js specifically. So this is a reasoned call, not a citation of a policy line. But the downside is asymmetric — degraded targeting and an odd traffic signature against a small privacy gain on a script whose entire job is to sell your visitors’ attention — so the default here should be off:

googleAdsense: {
  client: 'ca-pub-0000000000000000',
  trigger: 'onNuxtReady',
  proxy: false,   // don't route ad requests through your server
  bundle: false,  // don't rewrite and self-host the loader
}

proxy: false stops the request proxying; bundle: false also prevents the build-time bundling and its URL rewrites. If you’d rather kill anonymisation across every registry script at once, set scripts: { privacy: false }.

Serving ads to the EEA, UK, or Switzerland requires a Google-certified CMP (IAB TCF), and the consent signal has to be established before the ad loader runs. Load your CMP on an earlier trigger than onNuxtReady and keep AdSense behind it. Skipping this throttles serving to unconsented EEA traffic, which makes it a revenue problem and not only a compliance one.

Doing it without the module

If you’d rather not add @nuxt/scripts, the loader is one useHead call in app.vue:

useHead({
  script: [{
    src: `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${client}`,
    async: true,
    crossorigin: 'anonymous',
  }],
})

Then render <ins class="adsbygoogle"> yourself and push in onMounted. Two Nuxt-specific cautions if you go this route: push only on the client, since adsbygoogle doesn’t exist during SSR and calling it there throws; and because Nuxt navigates client-side, give the <ins> a :key tied to useRoute().fullPath so each route mounts a fresh, empty slot for the loader to fill — the same fix the Next.js guide uses, for the same reason.

Nuxt 4.5 moved head management to unhead v3, which narrowed useHead’s types. If you’re upgrading and your script entry suddenly fails to typecheck, that’s why — the runtime behaviour is unchanged.

One note on Auto ads

Set autoAds: true on the registry entry and manage placements from the AdSense dashboard. The tradeoff is the usual one: Auto ads inserts elements dynamically and can shift layout, where a manual unit with a reserved min-height gives you control over CLS. On a content site where speed is the revenue lever, that control is worth keeping.

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

Does Nuxt have an official AdSense module?
Yes. @nuxt/scripts ships a Google AdSense registry script — configure it under scripts.registry.googleAdsense in nuxt.config.ts, then use the useScriptGoogleAdsense() composable or the <ScriptGoogleAdsense> component. Nuxt is the only framework in this series with first-party AdSense support rather than a community plugin or a hand-rolled script tag. It also auto-inserts the AdSense site-verification meta tag once you provide a client id.
Should I use Nuxt Scripts' first-party mode with AdSense?
We'd turn it off. First-party mode bundles the AdSense loader at build time, serves it from your domain, and reverse-proxies the runtime requests through your server while anonymising visitor data — Nuxt's docs place AdSense in a privacy tier that has this enabled by default. For analytics that's a reasonable trade. For ads it removes the visitor signals Google prices inventory on, and routes every ad request through one server IP. Set proxy: false and bundle: false on the googleAdsense registry entry, or scripts.privacy = false globally.
Why does ScriptGoogleAdsense say 'Missing required prop: dataAdClient' when I set the client in nuxt.config?
Because the global client value is read by the loader script but not by the component. This was reported as nuxt/scripts issue #345 and closed without a fix, so treat it as intended behaviour: pass data-ad-client on every <ScriptGoogleAdsense> instance in addition to setting client in nuxt.config.ts. Keep the id in a runtime config or public env var so it lives in one place.
Where does ads.txt go in a Nuxt project?
In public/ads.txt. Nuxt serves everything in public/ from the domain root, so it resolves at yourdomain.com/ads.txt, which is where AdSense looks. Don't put it in server/ or a route handler — it's a static file. Confirm both apex and www resolve to it, or 301 one to the other.

Keep reading