Google Publisher Tag (GPT) 不加载:修复空 slot

从 AdSense `<ins>` 换到 GPT 后 slot 一直是空的?10 秒诊断 googletag is not defined、id 不一致、cmd.push 时序竞争和 SPA 双初始化。

你从 AdSense Auto Ads(或 <ins class="adsbygoogle">)换到 Google Publisher Tag(GPT),因为 Ad Manager 给你广告调度、header bidding、line item 定向这些精细控制。gpt.js 接好了,defineSlotdisplay 都调了,部署上线——所有 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 definedgpt.js 根本没加载(网络或广告拦截)
googletag.apiReadyundefined(不是 truegpt.js 还在加载没完成,或根本没加载
googletag.pubads().getSlots() 返回 []defineSlot 没执行
getSlots() 有 slot 但 <div> 还是空defineSlot 的 id 和 DOM <div> 的 id 不一致
警告 googletag.defineSlot was called without a matching googletag.displaydefineSlot 跑了,但那个 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.pubadsReadypubads() 方法可调)。用 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() 清空全部),进来时再重新 defineSlotdisplay()。这是 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 自动塌缩,避免一大块空。

最短修复路径

按命中率排序:

  1. DevTools Network → 确认 gpt.js 真的加载了 → 没加载就是广告拦截 / CSP / 网络
  2. Console:googletag.pubads().getSlots() → 空说明 defineSlot 没跑;非空进下一步
  3. <div id>defineSlot(...)display(...) 三个 id 完全对齐 → 修掉绝大多数「脚本加载了但空着」
  4. 每个调用都包进 googletag.cmd.push(function(){ ... }) → 修掉时序竞争
  5. SPA 路由切换用 refresh() 而不是 redefine → 修掉「首页有广告后续页都空」

怎么确认修好了

在线上页面的 DevTools console 里跑下面四个检查,四个都应该通过:

  1. googletag.apiReady 返回 true——库加载并初始化完成。
  2. googletag.pubads().getSlots().length 等于你定义的 slot 数(不多不少——多了就是双初始化)。
  3. 对每个 slot,googletag.pubads().getSlots()[0].getSlotElementId() 都能对上 DOM 里真实存在的 <div id="...">
  4. 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)

相关阅读

外部参考:

标签: #AdSense #变现 #排查 #GPT Tag