You switched from the AdSense auto-ads snippet (or <ins class="adsbygoogle">) to Google Publisher Tag (GPT) because Ad Manager gives you ad scheduling, header bidding, and line-item targeting. You wired up gpt.js, called defineSlot and display, deployed — and every slot stays empty. The DevTools console either shows googletag is not defined, defineSlot was not called for slot, or nothing at all. GPT failures cluster into five buckets: script never loads, slot DOM id does not match defineSlot, googletag.cmd.push runs before gpt.js, double-initialization from SPA navigation, and ad blockers. This article walks the five cases with concrete console diagnostics and the minimal correct snippet.
Identify which case you are in (10 seconds)
Open DevTools console and type each line:
| You see | Likely cause |
|---|---|
Uncaught ReferenceError: googletag is not defined | gpt.js never loaded (network failure or ad blocker) |
googletag.pubads().getSlots() returns [] | defineSlot never executed |
getSlots() returns slots but <div> is empty | DOM id mismatch between defineSlot and the slot div |
defineSlot was not called for slot ... warning | display() called before defineSlot, or wrong slot id |
Two googletag ad iframes load and one stays blank | Double init (script included twice, or SPA route change re-ran setup) |
Common causes, ranked by hit rate
1. gpt.js blocked by adblocker / CSP / network
uBlock Origin, Brave Shields, AdGuard, and corporate networks block securepubads.g.doubleclick.net by default. Your Content Security Policy may also reject it if you did not allowlist Google’s ad domains.
How to spot it: DevTools → Network tab → filter gpt. If gpt.js is red / status (blocked:csp) / (blocked:other), the script never loaded. Console shows googletag is not defined on any subsequent access.
Fix:
- CSP: allowlist
https://securepubads.g.doubleclick.net(script-src) andhttps://*.googlesyndication.com(frame-src + img-src). - Test in a clean Chrome profile with all extensions disabled — confirms the issue is client-side blocking, not your code.
- Do not try to “fix” adblocker-induced blank slots. Render a graceful fallback (your own content or whitespace) when GPT does not load.
2. googletag.cmd.push pattern not used
The correct pattern wraps every GPT call in googletag.cmd.push(function() { ... }). The cmd array is a command queue that gets drained when gpt.js finishes loading. If you call googletag.pubads() directly before the script loads, it throws.
<!-- Wrong: race condition -->
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>
<script>
googletag.defineSlot('/123/leaderboard', [728, 90], 'banner').addService(googletag.pubads());
googletag.enableServices();
</script>
<!-- Right: use cmd.push queue -->
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>
<script>
window.googletag = window.googletag || { cmd: [] };
googletag.cmd.push(function() {
googletag.defineSlot('/123/leaderboard', [728, 90], 'banner').addService(googletag.pubads());
googletag.pubads().enableSingleRequest();
googletag.enableServices();
});
</script>
Why: googletag.cmd = [] is initialized inline so cmd.push works synchronously. Once gpt.js loads, it replaces cmd with a function that runs each pushed callback immediately. This is the standard async-loader pattern.
3. Slot div id does not match defineSlot
defineSlot(adUnitPath, sizes, divId) and googletag.display(divId) must both reference the exact same id as the slot’s <div>. Any mismatch — even capitalization — leaves the div empty.
<!-- Body -->
<div id="div-gpt-ad-leaderboard"></div>
<!-- Script -->
<script>
googletag.cmd.push(function() {
googletag.defineSlot('/123/leaderboard', [728, 90], 'div-gpt-ad-leaderboard')
.addService(googletag.pubads());
googletag.enableServices();
googletag.display('div-gpt-ad-leaderboard');
});
</script>
How to spot it: in console, googletag.pubads().getSlots()[0].getSlotElementId() returns the id GPT thinks it should target. Compare to the actual <div id="..."> in the DOM. Any difference and the slot will not fill.
4. Double initialization from SPA route change
In React / Vue / Astro client-side routing, your component remounts when the route changes. If defineSlot runs every mount, you accumulate duplicate slot definitions and GPT silently refuses to register the duplicates. Worse, enableServices() should only fire once per page lifetime.
How to spot it: googletag.pubads().getSlots().length keeps growing on every route change; only the first slot fills, the rest stay empty.
Fix: define slots once at the app entry, then call googletag.pubads().refresh([slot]) on route changes to reload ads without redefining:
// App entry — runs once
window.googletag = window.googletag || { cmd: [] };
googletag.cmd.push(function() {
window._adSlot = googletag.defineSlot('/123/leaderboard', [728, 90], 'div-gpt-ad-leaderboard')
.addService(googletag.pubads());
googletag.pubads().enableSingleRequest();
googletag.enableServices();
});
// Route change handler
function onRouteChange() {
googletag.cmd.push(function() {
googletag.pubads().refresh([window._adSlot]);
});
}
5. Wrong product entirely — you wanted AdSense, not GPT
GPT belongs to Google Ad Manager (formerly DFP). If you only have an AdSense account, you cannot use GPT — your ad unit path /12345/slot-name will not resolve to any inventory. Ad Manager network code starts at 21+ digits for self-serve and shorter for legacy accounts. AdSense uses <ins class="adsbygoogle"> and a different pub-XXXX client id.
How to spot it: console request to securepubads... returns 200 but ad servers return 204 No Content. You only have AdSense in your Google account, not Ad Manager.
Fix: either link AdSense as a demand source inside Ad Manager (sign up for Ad Manager, link the account), or stay on AdSense <ins> tags. Do not mix AdSense Auto Ads with GPT on the same page — they fight over inventory.
Minimal correct GPT snippet (copy-ready)
<head>
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>
<script>
window.googletag = window.googletag || { cmd: [] };
googletag.cmd.push(function() {
googletag
.defineSlot('/NETWORK_CODE/leaderboard', [[728, 90], [970, 90]], 'div-gpt-ad-leaderboard')
.addService(googletag.pubads());
googletag.pubads().enableSingleRequest();
googletag.pubads().collapseEmptyDivs();
googletag.enableServices();
});
</script>
</head>
<body>
<div id="div-gpt-ad-leaderboard" style="min-width: 728px; min-height: 90px;">
<script>
googletag.cmd.push(function() { googletag.display('div-gpt-ad-leaderboard'); });
</script>
</div>
</body>
Replace NETWORK_CODE with your Ad Manager network code. collapseEmptyDivs() hides the slot when no ad fills, preventing a giant empty box.
Shortest fix path
In hit-rate order:
- DevTools Network → confirm
gpt.jsactually loaded → if blocked, root cause is adblocker / CSP / network - Console:
googletag.pubads().getSlots()→ empty means defineSlot never ran; non-empty means proceed to step 3 - Match slot id between
<div id>,defineSlot(...), anddisplay(...)→ fixes most “script loaded but empty” cases - Wrap every call in
googletag.cmd.push(function(){ ... })→ fixes the race condition - Refresh slots on SPA route change instead of redefining → fixes the “ads work on first page but not subsequent” pattern
Prevention
- One ad system per site: AdSense
<ins>tags OR Google Ad Manager GPT. Never both. - Define slots once at the app root; use
refresh()on route changes. - Add a small dev-mode console banner showing slot count and fill rate so blank slots are obvious during QA.
- Keep
min-width/min-heighton every slot div equal to the smallest size you defined. Prevents CLS and makes empty slots visually identifiable. - Test in an incognito window with all extensions disabled before declaring a slot broken.
FAQ
Q: Can I use GPT with AdSense ad units? A: Yes — link AdSense as a backfill demand source inside Ad Manager. The actual page tag is GPT; AdSense fills inventory when no Ad Manager line item matches.
Q: Why use GPT instead of AdSense? A: GPT gives you direct-sold line items, ad scheduling, header bidding, audience targeting, frequency caps. AdSense is set-and-forget; GPT is for publishers who actively manage demand.
Q: My ads work locally but break on production. A: Three likely causes: (a) production CSP blocks Google ad domains, (b) you forgot to update the network code from staging to production, (c) your production domain is not approved in the Ad Manager site list. Check console for the specific error.
Q: googletag.pubads().getSlots() returns slots but they are still blank.
A: Inventory issue, not code issue. Either no line item targets that ad unit path, or your account is too new and has no demand yet. Check Ad Manager → Delivery → Line Items for the matching slot.
Q: First slot fills, second slot stays empty on the same page.
A: Almost always enableSingleRequest() is missing, so the second slot’s request is racing the first. Add googletag.pubads().enableSingleRequest() before enableServices(). Or you defined the second slot but never called googletag.display(secondDivId).