Sitemap 和 robots.txt 是无聊的基础设施——直到 Google 说”已发现,尚未索引”,原因是你 sitemap 漏页或 robots 误屏 CSS。App Router 提供两种干净的写法,挑一种掌握。
问题背景
Google 仍然主要靠内链发现新内容,但 sitemap 对新站有明显加速作用——尤其和 Search Console 提交配合时。robots.txt 是给爬虫的礼貌信号,不是安全手段。两个文件都必须在站根能访问,content-type 分别是 text/plain(robots)和 application/xml(sitemap)。
判断标准
- 上线两周 Next.js 站文章索引率不到 30%。
- Search Console 提示 “Sitemap could not be read” 或 “0 discovered URLs”。
- 看到部署后 robots.txt 里
Disallow: /慌了——是的,这就是屏蔽所有。
实操步骤
-
先选静态还是动态。约 100 页以内,
public/robots.txt+public/sitemap.xml静态文件够用;build 时生成 URL 的用 App Router 的app/sitemap.ts和app/robots.ts。 -
静态 robots.txt——丢到
public/robots.txt:
User-agent: *
Allow: /
Disallow: /api/
Disallow: /preview/
Disallow: /_next/
Disallow: /drafts/
Sitemap: https://yourdomain.com/sitemap.xml
- 动态
app/robots.ts——App Router 的标准写法,类型完整:
// app/robots.ts
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: '*', allow: '/', disallow: ['/api/', '/drafts/', '/preview/'] },
{ userAgent: 'GPTBot', disallow: '/' }, // 拒绝 AI 抓取
],
sitemap: 'https://yourdomain.com/sitemap.xml',
host: 'https://yourdomain.com',
};
}
- 动态
app/sitemap.ts——从页面用的同一份内容源生成。双语 MDX 站示例:
// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { getAllArticles } from '@/lib/content';
const SITE = 'https://yourdomain.com';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const articles = await getAllArticles();
const staticPaths: MetadataRoute.Sitemap = [
{ url: `${SITE}/`, changeFrequency: 'daily', priority: 1.0 },
{ url: `${SITE}/about/`, changeFrequency: 'monthly', priority: 0.5 },
];
const articlePaths: MetadataRoute.Sitemap = articles.flatMap(a => ([
{
url: `${SITE}/en/articles/${a.slug}/`,
lastModified: a.updatedAt ?? a.publishedAt,
changeFrequency: 'weekly',
priority: 0.8,
alternates: {
languages: {
en: `${SITE}/en/articles/${a.slug}/`,
zh: `${SITE}/zh/articles/${a.slug}/`,
'x-default': `${SITE}/en/articles/${a.slug}/`,
},
},
},
]));
return [...staticPaths, ...articlePaths];
}
- 部署后验证 content-type 和数量:
curl -sI https://yourdomain.com/robots.txt | grep -i content-type
# content-type: text/plain; charset=utf-8
curl -sI https://yourdomain.com/sitemap.xml | grep -i content-type
# content-type: application/xml; charset=utf-8
curl -s https://yourdomain.com/sitemap.xml | grep -c '<loc>'
# 大致等于文章总数 + 静态页
- Next 输出的 sitemap XML 应该是这样——每条 URL 带 hreflang 替代:
<url>
<loc>https://yourdomain.com/en/articles/foo/</loc>
<lastmod>2026-05-22T00:00:00.000Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://yourdomain.com/en/articles/foo/" />
<xhtml:link rel="alternate" hreflang="zh" href="https://yourdomain.com/zh/articles/foo/" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://yourdomain.com/en/articles/foo/" />
</url>
-
Search Console → Sitemaps 提交
https://yourdomain.com/sitemap.xml。1-2 天内从 Pending 变 Success。 -
前一个月每周看一次 Search Console。覆盖率应该从几条爬到全站大部分。如果停滞,找一条 URL 在 URL Inspection 里查 Google 给的具体原因。
容易踩的坑
- 开发期 robots.txt 写
Disallow: /,上线忘删——经典史诗级 SEO 事故。 - sitemap route handler 写错返回 HTML 不是 XML——Google 默默拒绝。
- sitemap 和 canonical URL 结尾斜杠不一致——Google 当不同 URL。
- 手写 sitemap 然后烂掉——从内容源自动生成。
- 把分页 URL(
?page=2)或过滤 URL 也塞 sitemap——这些该noindex,不该宣传。 - 只放一种语言的 URL——
/en/foo和/zh/foo在 Google 眼里是两个 URL。
这篇适合谁
所有要被索引的 Next.js 内容站。新站强制要,老站也有帮助。
这篇不适合谁
故意不索引的站(staging、内部工具),应该 Disallow: / 并跳过 sitemap。
FAQ
- 内链做得好还需要 sitemap 吗?: 老站靠内链通常够。新站 sitemap 对首次发现有明显加速作用。
- sitemap 多久更新一次?: 每次发布。
app/sitemap.ts从 content collection 读取,每次 build 自动更新。 - 要不要带
lastModified?: 准的话要——帮 Google 优先重爬变了的内容。别每条都伪造成当前日期,否则 Google 学会忽略它。 - 能多个 sitemap 吗?: 能——做 sitemap index(
sitemap-index.xml)列多个子 sitemap。几千条 URL 之后有用。