在 Astro、Next.js 静态导出、SvelteKit、Hugo 上,你已经放好了 AdSense 的 <ins> 块和 adsbygoogle.js 脚本。查看源代码一切都在。控制台 window.adsbygoogle.loaded 返回 false。位永远空白。这是静态站 / SPA 的 AdSense 经典坑。脚本加载了,但激活 slot 的 push({}) 调用要么没触发、要么太早、要么触发了两次——AdSense 对这三种都会静默拒绝。
修复就是让 push({}) 在 <ins> 进入 DOM之后、每个 slot 精确执行一次。听起来简单;但现代框架的生命周期让这不简单。
常见原因
按命中率从高到低。
1. push 在 <ins> 挂载前就触发(竞态)
你的组件:
useEffect(() => {
(window.adsbygoogle = window.adsbygoogle || []).push({});
}, []);
如果这个 effect 跑在一个比 <ins> 提交进 DOM 还早的 layout 里(或者 hydration 之前),AdSense 的队列收到了 push 但找不到对应的 <ins>。
怎么判断:控制台 document.querySelector('ins.adsbygoogle')。返回了元素但 data-ad-status 是 undefined,就是 push 太早了。
2. 同一 slot 触发多次 push
SPA 切路由让你的 useEffect 重新跑,但前一次渲染的 <ins> 还在。AdSense 看到一个 slot 有两次 push,拒绝填充。
怎么判断:控制台 Failed to load resource 或 TagError: All ins elements in the DOM with class=adsbygoogle already have ads in them——就是 double-push。
3. SSG 只渲染 HTML——没有客户端 push
Astro / Next 静态导出,广告组件忘了 client:load。HTML 里有 <ins> 但没有 JS 跑去 push。
怎么判断:页面加载完 window.adsbygoogle 还是 []——push() 从未执行。
4. 组件用 client:only 但时机不对
Astro 的 client:only 把渲染推迟到 JS 跑起来后。<ins> 出现得很晚;如果 AdSense 脚本之前就跑过 push,那次 push 找不到匹配的 slot。
怎么判断:Timeline 里 <ins> 在页面加载后 1-2 秒才出现。AdSense 脚本先跑、找到 0 个 slot。
5. AdSense 只在生产生效
本地开发 (localhost) 和 Vercel 预览 URL 永远不投真广告。slot 空是预期的,不是 bug。
怎么判断:同样的代码在生产域名工作、staging/preview 不工作。别在那调试——上生产测。
6. 容器拦截脚本(CSP、iframe sandbox)
如果你的页面 CSP 不允许 googlesyndication.com 或 pagead2.googlesyndication.com,脚本能加载但广告 iframe 渲染不出来。
怎么判断:控制台 “Refused to load script from ‘pagead2.googlesyndication.com’ because it violates the following Content Security Policy directive.”
最短修复路径
第 1 步:建一个统一的可复用 AdSense 组件
Astro + React island (client:load):
// AdSlot.jsx
import { useEffect, useRef } from 'react';
export default function AdSlot({ slotId, format = 'auto' }) {
const insRef = useRef(null);
useEffect(() => {
if (!insRef.current || insRef.current.dataset.adPushed === 'true') return;
try {
(window.adsbygoogle = window.adsbygoogle || []).push({});
insRef.current.dataset.adPushed = 'true';
} catch (e) {
console.warn('AdSense push failed', e);
}
}, []);
return (
<ins
ref={insRef}
className="adsbygoogle"
style={{ display: 'block' }}
data-ad-client={import.meta.env.PUBLIC_ADSENSE_CLIENT}
data-ad-slot={slotId}
data-ad-format={format}
data-full-width-responsive="true"
/>
);
}
用法 <AdSlot client:load slotId="1234567890" />。
纯 Astro(不用 React),用内联 <script>:
---
const { slotId } = Astro.props;
---
<ins class="adsbygoogle"
style="display:block"
data-ad-client={import.meta.env.PUBLIC_ADSENSE_CLIENT}
data-ad-slot={slotId}
data-ad-format="auto"
data-full-width-responsive="true"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
第 2 步:防 double-push
上面组件里的 data-ad-pushed="true" 标志位防止重新渲染时重复 push。SPA 路由切换尤其关键。
第 3 步:处理 SPA 路由切换
Astro 的 View Transitions 或 Next.js App Router:
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
useEffect(() => {
// 路由切换时给所有还没 push 过的新 <ins> push 一下
document.querySelectorAll('ins.adsbygoogle:not([data-ad-pushed])').forEach(() => {
(window.adsbygoogle = window.adsbygoogle || []).push({});
});
}, [pathname]);
第 4 步:上生产测,不要在本地测
AdSense 在 localhost、*.vercel.app 预览 URL 等任何未批准域名上不投广告。部署到真实域名再测。AdSense 论坛上一半的求助帖都是这个。
第 5 步:CSP 放行 AdSense
如果有 CSP 头,需要:
script-src 'self' 'unsafe-inline' https://pagead2.googlesyndication.com https://googleads.g.doubleclick.net;
frame-src https://googleads.g.doubleclick.net https://tpc.googlesyndication.com;
img-src 'self' data: https://*.googlesyndication.com https://*.doubleclick.net;
能不加 'unsafe-inline' 就别加。
第 6 步:等 24-48 小时让 AdSense 报告同步
哪怕修对了,AdSense 收益面板也要 24-48 小时才反映出新的填充。
哪些情况可能不是你操作错了
AdSense 对 SPA / 静态站的行为没文档,部分要靠论坛反馈推断。生产上耐心测。静态站社区在这方面已经做了大量试错。
容易误判的情况
以为广告 slot “坏了”,其实是 AdSense 客户端没在对的时机 push。脚本标签是加载了(Network 显示 200),但 push 没触发或触发不对。
预防建议
- 建一个可复用的
<AdSlot>组件。所有 slot 都走它。 - 每个元素都用 flag 防止重复 push。
- SPA 项目每次路由切换都重新扫描 + push。
- 上生产测,不要在本地测。
- 加一个简单的 CI 测试:部署 → curl 页面 → 确认
<ins class="adsbygoogle">出现。
FAQ
- 静态站到底能用 AdSense 吗? 能——push 时机对就行。大多数主流内容站都是静态的。
- 改用 Auto Ads 更简单吗? Auto Ads 自动处理这套——静态站确实更简单,减少 bug 表面积。