内容站做久了,sitemap.xml 很容易从一个小文件变成一个需要认真设计的入口。
文章少时,手写几条 URL 就够了;文章、分类、标签、专题页变多以后,就会遇到几个问题:数据要不要请求后端,缓存多久合适,要不要拆多个 sitemap,为什么开发时能访问、构建或部署后又提示静态生成冲突。
这篇只讲 App Router 里 sitemap 的日常处理思路,重点是把静态、动态、缓存和拆分几个概念先分清。
小站可以直接放静态 sitemap
如果站点页面很少,最简单的方式就是在 app 目录下放一个静态的 sitemap.xml。
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com</loc>
<lastmod>2026-06-11T00:00:00.000Z</lastmod>
</url>
<url>
<loc>https://example.com/about</loc>
<lastmod>2026-06-11T00:00:00.000Z</lastmod>
</url>
</urlset>
这种方式几乎没有运行时成本,也不需要考虑接口、缓存和数据库。但它只适合页面数量稳定的小站。
只要 URL 来自数据库、CMS、博客后端或搜索索引,就不太适合长期手写。
用 `app/sitemap.ts` 生成动态列表
Next.js 的 App Router 支持用 app/sitemap.ts 生成 sitemap。常见写法是导出一个函数,返回 URL 数组。
import type { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await fetch('https://api.example.com/posts').then((res) => res.json());
return posts.map((post) => ({
url: `https://example.com/posts/${post.slug}`,
lastModified: post.updatedAt
}));
}
这个文件很适合内容站:文章列表来自后端,生成结果交给 Next.js 输出成 XML。
但这里有一个关键点:它看起来像普通函数,本质上仍然是一个特殊的 Route Handler。也就是说,它会受到 Next.js 缓存、静态生成和动态渲染规则影响。
先决定:静态、ISR,还是每次请求生成
做 sitemap 前先问一个问题:这个 sitemap 到底多久需要更新?
如果文章一天才发几篇,可以定时缓存,没必要每次请求都打后端。
如果站点内容变化很频繁,或者后端已经有自己的缓存层,可以考虑请求时生成。
可以按这三类判断:
- 静态生成:URL 很少,构建时确定,后续很少变化。
- ISR:URL 会变化,但允许几分钟或几小时后再刷新。
- 请求时生成:URL 必须尽量实时,或者 sitemap 本身只是转发后端结果。
很多 sitemap 报错,根因不是 XML 写错,而是代码里一边用了动态数据,一边又让框架按静态页面处理。
`revalidate` 适合大多数内容站
对博客、文档站、资源站来说,revalidate 通常是比较稳的选择。
export const revalidate = 3600;
export default async function sitemap() {
const posts = await fetch('https://api.example.com/posts').then((res) => res.json());
return posts.map((post) => ({
url: `https://example.com/posts/${post.slug}`,
lastModified: post.updatedAt
}));
}
这表示 sitemap 不必每次请求都重新生成。搜索引擎访问时拿到的是缓存结果,过期后再重新生成。
好处是:
- 不把搜索引擎爬虫流量全部打到后端。
- 文章更新后仍然可以在可接受时间内反映到 sitemap。
- 比完全动态生成更容易稳定部署。
缓存时间不需要一开始就追求极致。普通内容站可以先用 10 分钟、1 小时、6 小时这类粒度,再根据发布频率调整。
碰到静态生成冲突时看这几件事
如果你在 sitemap 里请求后端,又看到类似“这个路由需要动态渲染,但当前被静态生成处理”的问题,可以按顺序排查:
- 代码里是否用了
fetch的revalidate: 0。 - 文件里是否显式声明了合适的缓存策略。
- 是否用了 cookies、headers 或其他请求时 API。
- 部署环境是否开启了新的缓存模型或实验能力。
- 当前 Next.js 版本对应的 sitemap 和 Route Segment Config 文档是否已经变化。
有些旧项目会直接加:
export const dynamic = 'force-dynamic';
这能表达“这个路由每次请求动态生成”的意图。但不要把它当成所有版本、所有配置下的唯一答案。Next.js 的缓存模型一直在调整,尤其新版本启用 Cache Components 后,一部分旧的 route segment 配置会有变化。
更稳的理解是:先确定 sitemap 的新鲜度要求,再选择静态、ISR 或请求时生成,而不是看到报错就机械套 force-dynamic。
多 sitemap 不要硬塞进一个文件
搜索引擎并不要求全站所有 URL 都挤在一个 sitemap 里。内容一多,拆分反而更清楚。
常见拆分方式:
/sitemap.xml:站点地图索引。/posts/sitemap.xml:文章 URL。/categories/sitemap.xml:分类 URL。/tags/sitemap.xml:标签 URL。/pages/sitemap.xml:独立页面 URL。
Next.js 可以通过不同路由段放多个 sitemap.ts,也可以使用 generateSitemaps 给大列表按 ID 拆分。
拆分的好处是:
- 单个文件更小,生成更快。
- 某类内容异常时更容易定位。
- 后续某个频道迁移,不会影响全站 sitemap。
对内容站来说,我更倾向于按内容类型拆,而不是按数据库分页数字裸拆。搜索工具里看问题时,posts、tags、categories 比 sitemap-1、sitemap-2 更容易判断。
sitemap 不只要能访问,还要能被搜索引擎理解
生成 sitemap 后,至少检查这些点:
/sitemap.xml能通过浏览器或curl正常访问。- 响应头是 XML 相关类型,至少不要返回 HTML 错误页。
- URL 使用最终规范域名,不要混用
http、测试域名或本地地址。 - URL 不要重复,不要带无意义参数。
lastmod来自真实更新时间,不要所有页面永远写当前时间。- robots.txt 里能指向 sitemap。
- Search Console 或站长平台能成功读取。
这里最容易漏的是域名。开发时接口里可能返回 localhost、内网域名或旧域名;上线后 sitemap 虽然能打开,但里面全是错误 URL,这对收录没有帮助。
和后端接口配合时的一个小建议
如果 sitemap 依赖后端,后端最好提供专门的轻量接口,而不是让前端拉完整文章列表再自己裁剪。
接口只需要返回:
- slug 或 path
- 更新时间
- 内容类型
- 是否允许收录
不要把正文、作者详情、评论数量、推荐列表一起返回。搜索引擎抓 sitemap 时,前端需要的是 URL 清单,不是文章详情页的数据包。
后端也可以按频道提供多个接口,例如:
/sitemap/posts
/sitemap/categories
/sitemap/tags
这样前端生成多个 sitemap 时会更自然。
小结
Next.js App Router 里的 sitemap 可以按这个顺序处理:
- 页面少,用静态
app/sitemap.xml。 - 页面来自后端,用
app/sitemap.ts。 - 普通内容站优先考虑
revalidate,别急着每次请求都动态生成。 - 内容多了就拆多个 sitemap,按文章、分类、标签这类业务类型拆。
- 报静态/动态冲突时,先看缓存策略和当前 Next.js 版本,不要只机械套旧配置。
- 上线后检查 XML、规范域名、robots.txt 和搜索平台读取状态。
sitemap 的目标不是炫技,而是让搜索引擎稳定、清楚、低成本地理解你的网站结构。
参考链接
- Next.js sitemap 官方文档:<https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap>
- Next.js Route Segment Config 官方文档:<https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config>
- Sitemaps XML 格式说明:<https://www.sitemaps.org/protocol.html>



