导致 Google 忽略我整个站点的 Next.js SEO Bug(以及我如何发现它)
Source: Dev.to
一些背景
我不是开发者。我喜欢动手做东西,尝试新工具。SEO 总是那件我会“以后再弄清楚”的事。著名的最后一句话。
我对 SEO 有很高层次的了解,但在营销绩效工作中,我明白一个被良好索引的网站和正确关键词的重要性。我原以为这基本上就是全部——研究查询并围绕它们创建优质内容。
这一次,我必须 弄清楚!
正如我所说,我通过构建东西来学习新工具和技术。
这个应用叫 MonkeyTravel。它使用 AI 生成个性化的旅行行程——逐日计划,包含活动、餐厅、酒店以及预算细分。它支持 英语、西班牙语和意大利语。我之所以创建它,是因为和朋友们一起规划团体旅行总是混乱,我想要更智能的方案。像往常一样,我最初是为自己构建的,但这一次我不想让它最终沦落到庞大的“项目墓地”。
应用本身运行良好。找到它的用户都很喜欢。
问题是什么? 没有人能找到它。
我必须弄清楚!而这仅仅是个开始。
第一阶段:“为什么 Google 没有显示我的站点?”
当我第一次查看 Google Search Console 时,我本以为会看到……什么东西。我已经上线好几周了。结果却是一条平直的线。
零展示。零点击。零已索引页面。
我的第一反应是责怪 Google。“需要时间,”我对自己说。于是又等了一周。仍然是零。
这时我真正检查了自己的设置:
- ❌ 过时的站点地图
- ❌ 没有规范标签(canonical tags)
- ❌ 没有 hreflang 标签(尽管有 3 种语言)
- ❌ 缺少结构化数据(structured data)
- ❌ 来自
create-next-app的默认robots.txt - ❌ 一半页面没有 meta 描述
基本上,我建了一座漂亮的房子,却忘了在门上贴号码。让我们把注意力放在 SEO 上,而不是我的糟糕决定上吧。 😄
Source: …
Phase 2: The Foundations (Boring but Necessary)
我花了一个周末来添加基础功能。没有什么革命性的东西,只是每个站点都需要的基本要素。
Sitemap
Next.js 用 app/sitemap.ts 让这件事变得很简单。我的站点会为所有静态页面、博客文章以及三个语言版本的目的地页面生成 URL。来自 Supabase 的动态内容也会被包含进来。
// Simplified version of my sitemap
export default async function sitemap(): Promise {
const baseUrl = "https://monkeytravel.app";
const locales = ["en", "es", "it"];
// Blog posts × 3 languages
const blogSlugs = getAllSlugs();
const blogPages = blogSlugs.flatMap(slug =>
locales.map(locale => ({
url:
locale === "en"
? `${baseUrl}/blog/${slug}`
: `${baseUrl}/${locale}/blog/${slug}`,
changeFrequency: "monthly" as const,
priority: 0.7,
}))
);
return [...staticPages, ...blogPages, ...destinationPages];
}
Canonical + Hreflang
多语言站点需要同时提供规范(canonical)URL 和一组备用语言 URL。我在每个页面中使用 generateMetadata():
alternates: {
canonical:
locale === "en"
? `${BASE_URL}/${slug}`
: `${BASE_URL}/${locale}/${slug}`,
languages: {
en: `${BASE_URL}/${slug}`,
es: `${BASE_URL}/es/${slug}`,
it: `${BASE_URL}/it/${slug}`,
"x-default": `${BASE_URL}/${slug}`,
},
},
Structured Data
为 Organization、WebSite、SoftwareApplication、Article 和 TouristDestination 编写 JSON‑LD schema。我为此构建了一个小工具:
export function jsonLdScriptProps(data: object) {
return {
type: "application/ld+json",
dangerouslySetInnerHTML: {
__html: JSON.stringify(data),
},
};
}
Result
提交站点地图后,Google 在 48 小时内发现了我的所有 URL。但“发现” ≠ “已索引”。大多数页面停留在 “Discovered — currently not indexed”(已发现 — 当前未索引)队列中。
一周后:12 个页面已索引。有进展,但进展非常缓慢。
真正的问题是?Google Search Console 并没有提供太多关于页面被拒的原因的反馈。
第三阶段:Google 实际想要的内容
我意识到我的站点大部分是登录墙后面的应用。Google 能够索引的公开内容非常少,于是我在内容上大力出击。
-
50 篇博客文章,涵盖真实的旅行主题、行程指南、目的地对比、预算旅行技巧、季节性推荐。
× 3 种语言 = 150 个博客页面。 -
20 个目的地着陆页(巴黎、东京、巴厘岛、巴塞罗那等),提供气候数据、AI 行程预览,并交叉链接到博客文章。
× 3 种语言 = 60 个页面。 -
5 个 SEO 着陆页,针对特定搜索意图:
/free-ai-trip-planner、/group-trip-planner、/budget-trip-planner等。
**让我惊讶的是:**内部链接的作用比内容本身更重要。那些被多个其他页面交叉链接的页面,索引速度 远快于孤立页面。于是我添加了:
- 在每个着陆页上加入 “来自博客” 区块
- 在每个目的地页面上加入 “相关目的地” 区块
- 博客 ↔ 目的地链接(双向)
- 在博客索引页添加地区筛选(欧洲、亚洲、美洲、非洲)
两周后:78 个页面已被索引。 索引曲线在加速上升。
Source: …
第四阶段:几乎毁掉一切的 Bug
然后是 Google Search Cons…
(故事继续…)
未使用用户选择的规范链接的重复内容
Google 正在拒绝我的首页。它把 www.monkeytravel.app 作为规范链接,而不是 monkeytravel.app,尽管我已经:
- 在中间件 以及 Vercel 配置中都做了 www → non‑www 的 301 重定向
- 在 HTML 中使用了正确的规范标签
- 在站点地图中所有 URL 都使用了非 www 形式
我仔细检查了一遍。重定向生效,HTML 中有正确的标签,curl 也证实了:
$ curl -s https://monkeytravel.app/ | grep canonical
那为什么 Google 会显示 “User‑declared canonical: None” 呢?
发现
我盯着这个问题看了好几个小时才恍然大悟。关键在于 我验证的方式。
curl会等待完整的响应返回。- Googlebot 不会。
在 Next.js 15.2+ 中,generateMetadata() 会 异步 流式输出元数据。<link rel="canonical"> 标签并不在最初的 HTML 负载中;它们是通过流在正文开始渲染后才注入的。当 Googlebot 解析初始响应时,规范标签根本不存在(至少这是我在翻阅 AI 回答和文档后得出的结论)。
我通过查看流式传输完成前的原始初始 HTML 进行确认——里面根本没有 <link rel="canonical">。
解决方案:一个配置选项
// next.config.ts
const nextConfig: NextConfig = {
// 为爬虫禁用流式输出,使完整的 HTML(包括元数据)同步发送
htmlLimitedBots: /Googlebot|Google-InspectionTool|Bingbot|Yandex/i,
trailingSlash: false,
};
export default nextConfig;
htmlLimitedBots 告诉 Next.js:“当爬虫访问时,禁用流式输出,同步发送包含所有元数据的完整 HTML。”
这一个正则表达式就解决了整个问题。
我还把根布局的规范链接从 "/" 改成了 "./",这样每个页面都会得到 自引用 的规范链接,而不是所有页面都指向首页(这是一个细微但重要的区别)。
部署并请求重新索引后,结果很快显现:
- 176 个页面在几天内被索引。
- 虽然还不到全部 230+ 页面,但趋势已经非常明确。
数字
| 指标 | 第 0 周 | 第 1 周 | 第 2 周 | 第 3 周 |
|---|---|---|---|---|
| 已索引页面 | 0 | 12 | 78 | 176 |
| 站点地图中的页面总数 | 0 | ~50 | ~225 | ~230 |
| 博客文章(每种语言) | 0 | 1 | 15 | 50 |
| 结构化数据模式 | 0 | 3 | 5 | 5 |
我犯的错误(让你免于犯同样的错误)
htmlLimitedBots从第一天起未设置 – 每个关注 SEO 的 Next.js 项目都应该配置它。元数据流是静默的;手动检查时看起来没问题,但爬虫看到的情况却不同。- 把 SEO 当作“以后再说”的问题 – 推迟生成站点地图和规范标签会每次浪费一周的潜在爬取时间。因为你急于求成,Google 的排队并不会加快。
- 低估内部链接的作用 – 交叉链接的页面比孤立页面的索引速度快 3‑4 倍。尽可能为相关内容添加链接。
- 缺少 hreflang 导致多语言支持不足 – 三个语言版本没有 hreflang,导致 Google 将它们视为重复内容而非翻译。
有帮助的 AI 技巧
- AI 辅助的博客内容 – 使用 AI 起草结构,然后进行编辑和本地化。对于 50 篇文章 × 3 种语言,这节省了数月的人工工作。
- 自动交叉链接 – 编写了一个脚本,分析主题并建议内部链接,效率远高于手动映射。
- 面向国际化的提示工程 – 我没有直接翻译英文文章,而是让 AI 为目标受众 撰写(例如,“为意大利受众写一篇关于巴黎的文章”),从而产生更自然的内容。
我会对过去的自己说
在第 1 天就开始做这些事,别等到写完任何功能后再去做:
htmlLimitedBots在next.config.ts中- 站点地图生成
- 每个页面都加上规范标签(canonical)
- 将站点提交到 Google Search Console
其他的——博客文章、结构化数据、内部链接——也很重要,但这四项是基础。跳过它们,后面的所有工作都无从谈起。
更新: 仍有 129 个页面在 Google 的队列中。按照目前的进度,它们将在几周内被收录(希望如此)。随后真正的挑战就来了:在竞争激烈的关键词上获得排名。那将会 很有趣。
MonkeyTravel 免费使用 —— 输入目的地,几秒钟内即可获得个性化的 AI 行程。使用 Next.js、Supabase 构建,部署在 Vercel。欢迎提供任何反馈!让我们一起学习新东西。