你从 AdSense Auto Ads(或 <ins class="adsbygoogle">)换到 Google Publisher Tag(GPT),因为 Ad Manager 给你广告调度、header bidding、line item 定向这些精细控制。gpt.js 接好了,defineSlot、display 都调了,部署上线——所有 slot 仍然是空的。DevTools console 要么报 googletag is not defined,要么报 googletag.defineSlot was called without a matching googletag.display,要么什么也没有。
最快的修法: 打开 DevTools,切到 Network 面板,过滤 gpt,刷新页面。如果 gpt.js 标红或显示 (blocked:...),说明广告拦截器或你的 CSP 把它挡掉了——这是最常见的单一原因。如果它是绿的(状态 200),在 console 里粘 googletag.pubads().getSlots():返回空数组 [] 说明你的 defineSlot 根本没跑;返回非空数组但 div 还是空的,说明是 slot id 不一致。这三步能解决绝大多数情况。
GPT 失败基本归到五类:脚本根本没加载、slot DOM id 和 defineSlot 不一致、没用 googletag.cmd.push(时序竞争)、SPA 路由切换导致双初始化、以及用错了 Google 产品(AdSense 和 Ad Manager 搞混)。这篇按命中率排序走完每一类,给出具体的 console 诊断和最小可用代码,所有内容均对照 Google GPT 官方文档核对(截至 2026 年 6 月)。
10 秒判断你是哪种情况
DevTools console 依次敲:
| 看到的提示 | 最可能的原因 |
|---|---|
Uncaught ReferenceError: googletag is not defined | gpt.js 根本没加载(网络或广告拦截) |
googletag.apiReady 是 undefined(不是 true) | gpt.js 还在加载没完成,或根本没加载 |
googletag.pubads().getSlots() 返回 [] | defineSlot 没执行 |
getSlots() 有 slot 但 <div> 还是空 | defineSlot 的 id 和 DOM <div> 的 id 不一致 |
警告 googletag.defineSlot was called without a matching googletag.display | defineSlot 跑了,但那个 id 从没调过 display(divId) |
| 两个 googletag iframe 加载、一个空白 | 双初始化(脚本写了两份,或 SPA 路由切换重跑了 setup) |
常见原因(按命中率排序)
1. gpt.js 被广告拦截器 / CSP / 网络挡了
uBlock Origin、Brave Shields、AdGuard、企业网络默认都拦 securepubads.g.doubleclick.net。如果你没在 Content Security Policy 里白名单 Google 广告域名,CSP 也会挡。
怎么判断:DevTools → Network → 筛 gpt。如果 gpt.js 标红、状态 (blocked:csp) 或 (blocked:other),脚本压根没下载,后续访问 googletag 必然报 is not defined。
修复:
- CSP:script-src 白名单
https://securepubads.g.doubleclick.net,frame-src 和 img-src 白名单https://*.googlesyndication.com。当前官方源是https://securepubads.g.doubleclick.net/tag/js/gpt.js——如果你抄了老片段、指向https://www.googletagservices.com/tag/js/gpt.js,那个域名已经弃用,可能被拦或被重定向,换掉它。 - 用全新的 Chrome profile(关掉所有扩展)测一遍,确认是客户端拦截不是代码问题。
- 别想着「绕开」广告拦截器——优雅降级:GPT 没加载就渲染你自己的内容或干净的空白。
2. 没用 googletag.cmd.push 模式
正确写法是每个 GPT 调用都放进 googletag.cmd.push(function() { ... })。cmd 是个命令队列,gpt.js 加载完后会清空。如果你在脚本加载前直接调 googletag.pubads(),会抛异常。
<!-- 错: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>
<!-- 对:用 cmd.push 队列 -->
<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>
原理:googletag.cmd = [] 是内联同步初始化的,cmd.push 可以立即调;gpt.js 一加载完就把 cmd 替换成一个会立刻执行 callback 的函数。这是标准异步 loader 模式。
不要用 typeof googletag !== 'undefined' 来判断能不能调 API。 googletag 对象几乎瞬间就存在(是你内联创建的),但库没加载完之前它的 API 方法是不能调的。Google 官方的就绪标志是 googletag.apiReady(核心 API 可调)和 googletag.pubadsReady(pubads() 方法可调)。用 cmd.push 的意义就在于你根本不用自己去查这些——GPT 就绪那一刻会自动清空队列。靠 typeof googletag 来决定何时调 pubads(),是 Google 点名的头号错误。
调用顺序也要对:页面级设置和定向在前,然后 defineSlot,再 enableServices(),最后 display()。在 enableServices() 之后再设定向会静默无效。
3. slot div 的 id 和 defineSlot 不一致
defineSlot(adUnitPath, sizes, divId) 和 googletag.display(divId) 必须引用和 <div> 完全一样的 id——大小写也算。任何不一致 slot 就是空的。
<!-- HTML -->
<div id="div-gpt-ad-leaderboard"></div>
<!-- 脚本 -->
<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>
怎么判断:console 敲 googletag.pubads().getSlots()[0].getSlotElementId() 看 GPT 认为要塞哪个 id,再对照 DOM 里 <div id="...">。有差异 slot 就填不进去。
4. SPA 路由切换导致双初始化
React / Vue / Astro 客户端路由切换时组件会 remount。如果 defineSlot 每次 mount 都跑,重复定义会被 GPT 静默拒绝。更糟的是 enableServices() 整个页面生命周期只应该调一次。
怎么判断:每次切路由 googletag.pubads().getSlots().length 都在涨;只有第一个 slot 填了,后面的都空。
有两种正确写法,用哪种取决于 slot 的 <div> 是否在路由切换中存活:
- 同一个 div 跨路由一直挂着(比如常驻侧边栏 slot):slot 只定义一次,然后调
googletag.pubads().refresh([slot])拉新广告,不重新定义。 - slot 的
<div>会卸载再重新挂载(常见情况——广告在页面组件里):每个 div id 整个页面生命周期只能 display 一次,同 id 重新挂载是填不进去的。离开时调googletag.destroySlots([oldSlot])(或googletag.destroySlots()清空全部),进来时再重新defineSlot并display()。这是 Google 文档针对单页应用给出的做法。
// App 入口——只跑一次
window.googletag = window.googletag || { cmd: [] };
googletag.cmd.push(function() {
googletag.pubads().enableSingleRequest();
googletag.enableServices();
});
// 常驻 slot:定义一次,路由切换时 refresh
let persistentSlot;
function mountPersistentSlot() {
googletag.cmd.push(function() {
if (!persistentSlot) {
persistentSlot = googletag.defineSlot('/123/leaderboard', [728, 90], 'div-gpt-ad-leaderboard')
.addService(googletag.pubads());
}
googletag.display('div-gpt-ad-leaderboard');
googletag.pubads().refresh([persistentSlot]);
});
}
// 会卸载的逐页 slot:每次切路由先 destroy 再重新定义
function onRouteChange(slot) {
googletag.cmd.push(function() {
if (slot) googletag.destroySlots([slot]);
// 在这里为新页面的 div 重新跑 defineSlot + display
});
}
5. 你其实想用的是 AdSense 不是 GPT
GPT 属于 Google Ad Manager(原 DFP)。只有 AdSense 账号是用不了 GPT 的——你的广告位路径在 Ad Manager 找不到任何 inventory。广告位路径格式是 /[network_code]/[ad_unit],例如 /123456789/leaderboard;network code 是你 Ad Manager 网络的数字 id(在 Ad Manager → Admin → Global settings → Network code 看,或直接从任何能用的 GPT tag 上读)。AdSense 根本没有 network code——它用 <ins class="adsbygoogle"> + data-ad-client="ca-pub-XXXXXXXXXX",完全是另一套系统。
怎么判断:console 看 securepubads... 请求返回 200 但广告服务器回 204 No Content。你 Google 账号下只有 AdSense 没有 Ad Manager。
修复:要么在 Ad Manager 把 AdSense 接成 demand source(先开 Ad Manager,再 link 账号),要么继续用 AdSense <ins>。不要同一页面混用 AdSense Auto Ads 和 GPT——它们会抢库存。
最小可用 GPT 模板(可直接复制)
<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>
NETWORK_CODE 换成你 Ad Manager 的 network code;collapseEmptyDivs() 让填不到广告的 slot 自动塌缩,避免一大块空。
最短修复路径
按命中率排序:
- DevTools Network → 确认
gpt.js真的加载了 → 没加载就是广告拦截 / CSP / 网络 - Console:
googletag.pubads().getSlots()→ 空说明 defineSlot 没跑;非空进下一步 <div id>、defineSlot(...)、display(...)三个 id 完全对齐 → 修掉绝大多数「脚本加载了但空着」- 每个调用都包进
googletag.cmd.push(function(){ ... })→ 修掉时序竞争 - SPA 路由切换用
refresh()而不是 redefine → 修掉「首页有广告后续页都空」
怎么确认修好了
在线上页面的 DevTools console 里跑下面四个检查,四个都应该通过:
googletag.apiReady返回true——库加载并初始化完成。googletag.pubads().getSlots().length等于你定义的 slot 数(不多不少——多了就是双初始化)。- 对每个 slot,
googletag.pubads().getSlots()[0].getSlotElementId()都能对上 DOM 里真实存在的<div id="...">。 - Network 面板里,发往
securepubads.g.doubleclick.net/gampad/ads的请求返回200且响应非空。如果200但响应是空的(或gampad/ads请求返回 204),说明代码是对的、只是没有匹配的 demand——这是库存问题不是 tag 问题(见下面 FAQ)。
如果你有 Ad Manager 权限,官方的 Google Publisher Console(在 URL query 里加 googfc,或用 Ad Manager 浏览器扩展)能显示每个 slot 的填充状态和 GPT 实际请求的广告位路径。
预防建议
- 一个站只用一个广告系统:AdSense
<ins>或 Ad Manager GPT,永远别混。 - slot 在 app 根定义一次,路由切换调
refresh()。 - dev 模式加个小 console banner 显示 slot 数和填充率,QA 时空 slot 一目了然。
- 每个 slot 的
<div>设min-width/min-height等于你定义的最小尺寸——防 CLS,空 slot 也能直观看到。 - 测试前用无痕窗口、关掉所有扩展,再下结论说「slot 坏了」。
FAQ
Q:GPT 能用 AdSense 的广告位吗? A:能——在 Ad Manager 里把 AdSense 接成 backfill demand source。页面上挂的是 GPT,Ad Manager 没匹配 line item 时 AdSense 兜底填充。
Q:为什么用 GPT 而不是 AdSense? A:GPT 给你直销 line item、广告调度、header bidding、人群定向、频次封顶;AdSense 是设完不管的产品;GPT 适合主动管理 demand 的发布者。
Q:本地 OK 生产挂。 A:三种可能:(a)生产 CSP 挡了 Google 广告域名;(b)忘了把 staging 的 network code 改成生产的;(c)生产域名没在 Ad Manager 站点白名单。看 console 具体错。
Q:googletag.pubads().getSlots() 有 slot 但还是空的。
A:库存问题不是代码问题。要么没有 line item 定向到这个广告位路径,要么账号太新还没 demand。去 Ad Manager → Delivery → Line Items 查匹配。
Q:同一页第一个 slot 填了,第二个空着。
A:基本是漏了 enableSingleRequest(),第二个 slot 请求在抢第一个;在 enableServices() 前加上 googletag.pubads().enableSingleRequest()。或者第二个 slot 你 defineSlot 了但忘 googletag.display(secondDivId)。
相关阅读
外部参考:
- GPT:避免常见实现错误(Google for Developers)
- Google Publisher Tag 上手(Google for Developers)