在做完 reader 页 SSR 和 ISR 之后,文章正文已经可以稳定地出现在初始 HTML 里了。
但对一个内容站来说,仅仅让页面能被服务端渲染还不够。搜索引擎还需要知道:哪些页面值得抓取,哪些页面不应该抓取。
这次做的 SEO 优化就是补齐两个基础能力:
sitemap.xmlrobots.txt
在 Next.js App Router 里,对应的实现就是:
src/app/sitemap.tssrc/app/robots.ts
这两个文件代码量不多,但它们决定了搜索引擎如何发现站点内容,以及如何避开不应该暴露的页面。
sitemap 是什么
sitemap.xml 可以理解成网站提供给搜索引擎的“公开页面清单”。
搜索引擎当然可以通过页面里的链接慢慢发现内容,但对文章站来说,主动给出 sitemap 会更直接。
比如 Visionary 里真正希望被搜索引擎发现的页面主要是:
- 首页。
- 用户协议这类公开静态页面。
- 已发布且公开的文章详情页。
其中最重要的是文章详情页:
https://visionaryblog.cn/reader/412
https://visionaryblog.cn/reader/413
...
如果这些 URL 出现在 sitemap 里,搜索引擎就能更稳定地发现新文章,也能根据 lastModified 判断哪些文章可能需要重新抓取。
简单说:
sitemap.xml 负责告诉搜索引擎:这些页面值得抓。

robots 是什么
robots.txt 则是另一类信号。
它不是告诉搜索引擎“抓什么”,而是告诉搜索引擎“不要抓什么”。
一个真实项目里,并不是所有页面都应该进入搜索结果。
比如:
- API 路由。
- 编辑器页面。
- 创作中心。
- 草稿预览页。
- 审核页。
- 测试页。
- 用户私有页面。
这些页面即使能访问,也不应该被搜索引擎收录。
所以 robots.txt 的职责就是:
robots.txt 负责告诉搜索引擎:这些路径不要抓。
它和 sitemap 是互补关系。
一个负责提供公开内容入口,一个负责划清禁止抓取的边界。

为什么这次要一起做
前面已经把 reader 页改成了公开文章的 ISR 页面。
公开文章页现在满足几个条件:
- 不读取 cookie。
- 不依赖当前用户。
- 只展示
is_published = 1且view_permission = 'all'的文章。 - 初始 HTML 里有完整正文。
- 内容更新后通过
revalidatePath刷新缓存。
这意味着公开文章页已经具备被搜索引擎抓取的基础条件。
但还有两个问题:
第一,搜索引擎怎么发现所有文章 URL?
第二,搜索引擎如何知道预览页、编辑页、API 不应该抓?
所以 sitemap 和 robots 要一起补。
只做 sitemap,不做 robots,可能会让搜索引擎误抓一些不该出现的页面。
只做 robots,不做 sitemap,搜索引擎仍然需要靠链接慢慢发现文章。
两者一起做,边界才完整。
sitemap 只放公开文章
sitemap 里最重要的原则是:只放可以公开访问、希望被收录的页面。
所以文章查询不能直接查所有文章,更不能把草稿、审核中、私密文章放进去。
这次新增了一个轻量查询方法:
export type ArticleSitemapItem = {
id: number;
updated_time: Date | string | null;
};
export const getPublishedPublicArticleSitemapItems = async () => {
const connection = await pool.getConnection();
try {
const sql = `
SELECT id, updated_time
FROM articles
WHERE is_published = 1
AND view_permission = 'all'
ORDER BY updated_time DESC
`;
const [rows] = await connection.execute(sql);
return Array.isArray(rows) ? rows as ArticleSitemapItem[] : [];
} finally {
connection.release();
}
};
这里有两个设计点。
第一,只查公开文章:
WHERE is_published = 1 AND view_permission = 'all'
这和公开 reader 页的访问条件保持一致。
如果一篇文章不能通过 /reader/[articleId] 被公开访问,它就不应该出现在 sitemap。
第二,只查 sitemap 需要的字段:
SELECT id, updated_time
sitemap 不需要文章正文、标题、摘要、标签,所以没必要查大字段。
这能让 sitemap 查询更轻量,尤其文章数量变多之后更重要。
Next.js 里的 sitemap.ts
在 App Router 里,只要新增 src/app/sitemap.ts,Next.js 就会自动生成:
/sitemap.xml
sitemap 不需要每次访问都重新查数据库。搜索引擎本身也不是实时抓取,公开文章进入 sitemap 延迟一天是可以接受的。
所以这里把 sitemap 设置成每天刷新一次:
export const revalidate = 86400;
这样 /sitemap.xml 会被缓存 24 小时。24 小时内访问不会反复查数据库;超过 24 小时后,下次访问会触发重新生成。
当前实现是:
import type {MetadataRoute} from 'next';
import {getPublishedPublicArticleSitemapItems} from '@/server/article/article.service';
export const revalidate = 86400;
const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL || 'https://visionaryblog.cn').replace(/\/$/, '');
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const articles = await getPublishedPublicArticleSitemapItems();
return [
{
url: siteUrl,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
{
url: `${siteUrl}/userAgreement`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.2,
},
...articles.map(article => ({
url: `${siteUrl}/reader/${article.id}`,
lastModified: article.updated_time ? new Date(article.updated_time) : new Date(),
changeFrequency: 'weekly' as const,
priority: 0.8,
})),
];
}
这里有几个细节。
第一,siteUrl 支持环境变量:
process.env.NEXT_PUBLIC_SITE_URL || 'https://visionaryblog.cn'
这样以后如果域名变化,或者 staging 环境需要不同站点 URL,可以通过环境变量配置。
第二,replace(/\/$/, '') 去掉结尾斜杠。
这样拼接 URL 时不会出现:
https://visionaryblog.cn//reader/412
第三,文章的 lastModified 使用 updated_time。
lastModified: article.updated_time ? new Date(article.updated_time) : new Date()
这能告诉搜索引擎文章最近更新时间。
第四,不同页面设置不同优先级。
首页优先级最高:
priority: 1
文章页次之:
priority: 0.8
用户协议这类页面不是主要内容入口,所以优先级较低:
priority: 0.2
这些字段不是强制命令,而是给搜索引擎的参考信号。
第五,sitemap 采用按天刷新。
export const revalidate = 86400;
文章详情页是长期缓存加按需刷新,因为用户访问文章时希望尽快看到最新内容。
sitemap 的实时性要求没那么高。它只是帮助搜索引擎发现 URL,新文章晚一点进入 sitemap 通常可以接受。一天刷新一次可以减少数据库查询,也能保持搜索引擎发现能力。
robots 要禁止哪些页面
robots 的设计重点是:不要把非公开、无 SEO 价值或不该暴露的路径交给搜索引擎。
这次禁止了这些路径:
/api/
/creator/
/editor/
/reader/preview/
/reader/review/
/test
/trackTest
/userCenter/myData
/userCenter/readHistory
这些路径可以分成几类。
第一类是接口:
/api/
API 返回的是数据,不是面向搜索结果的页面。
第二类是后台和编辑器:
/creator/
/editor/
这些页面依赖登录态和用户操作,不应该被搜索引擎抓取。
第三类是预览和审核页:
/reader/preview/
/reader/review/
其中 /reader/preview/ 是作者预览非公开文章的入口,必须禁止抓取。
第四类是测试页:
/test
/trackTest
这些页面没有公开 SEO 价值。
第五类是用户私有页:
/userCenter/myData
/userCenter/readHistory
这些页面和当前登录用户相关,也不应该被收录。
Next.js 里的 robots.ts
在 App Router 里,只要新增 src/app/robots.ts,Next.js 就会自动生成:
/robots.txt
当前实现是:
import type {MetadataRoute} from 'next';
const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL || 'https://visionaryblog.cn').replace(/\/$/, '');
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: [
'/api/',
'/creator/',
'/editor/',
'/reader/preview/',
'/reader/review/',
'/test',
'/trackTest',
'/userCenter/myData',
'/userCenter/readHistory',
],
},
sitemap: `${siteUrl}/sitemap.xml`,
};
}
生成出来的语义大概是:
User-agent: *
Allow: /
Disallow: /api/
Disallow: /creator/
Disallow: /editor/
Disallow: /reader/preview/
Disallow: /reader/review/
Disallow: /test
Disallow: /trackTest
Disallow: /userCenter/myData
Disallow: /userCenter/readHistory
Sitemap: https://visionaryblog.cn/sitemap.xml
这里的 Sitemap 字段也很关键。
它把 robots 和 sitemap 串起来,让搜索引擎访问 robots 时就能知道 sitemap 的位置。
robots 不是权限系统
这里要特别注意一点:robots.txt 不是权限系统。
它只是给搜索引擎看的抓取建议。
正常搜索引擎会遵守,但它不能阻止用户直接访问某个 URL,也不能阻止恶意爬虫抓取。
所以像编辑器、草稿、预览这些页面,真正的访问控制仍然要靠服务端鉴权和 middleware。
robots 的作用是 SEO 层面的:告诉搜索引擎不要把这些页面抓进索引。
权限的作用是安全层面的:没有权限的人访问不了。
这两个不能互相替代。
sitemap 也不是强制收录
同样,出现在 sitemap 里的页面也不代表一定会被收录。
sitemap 只是告诉搜索引擎:这些 URL 是站点认为重要的公开页面。
搜索引擎是否收录,还会看很多因素:
- 页面是否可访问。
- 内容质量是否足够。
- 是否有重复内容。
- 页面加载性能。
- 是否被 robots 禁止。
- 是否有 canonical。
- 站点整体质量。
所以 sitemap 是“发现入口”,不是“收录保证”。
但对文章站来说,它仍然是非常基础且必要的能力。
和 reader ISR 的关系
这次 sitemap/robots 优化和前面的 reader ISR 是一套组合拳。
reader ISR 解决的是:
公开文章页能不能高效稳定地输出 HTML。
sitemap 解决的是:
搜索引擎怎么发现这些公开文章页。
robots 解决的是:
搜索引擎应该避开哪些非公开或无价值页面。
三者配合之后,公开内容的 SEO 路径就更完整了。
公开文章页通过 /reader/[articleId] 输出完整 HTML。
sitemap 把公开文章 URL 暴露给搜索引擎。
robots 阻止搜索引擎抓取 /reader/preview/、/editor/、/creator/ 等页面。
这能避免两个常见问题。
第一,公开文章已经 SSR/ISR 了,但搜索引擎发现不充分。
第二,搜索引擎抓到一些不该出现在结果里的页面,比如编辑器、预览页、测试页。
怎么确认是否生效
构建后,Next.js 输出里可以看到:
○ /robots.txt
○ /sitemap.xml ... Revalidate 1d
这说明这两个文件已经由 App Router 生成。
其中 Revalidate 1d 表示 sitemap 最多缓存一天。
部署后可以直接访问:
https://visionaryblog.cn/robots.txt
https://visionaryblog.cn/sitemap.xml
检查点包括:
robots.txt是否包含Sitemap: https://visionaryblog.cn/sitemap.xml。robots.txt是否禁止了/editor/、/creator/、/reader/preview/。sitemap.xml是否包含公开文章/reader/[id]。sitemap.xml是否没有草稿、预览、审核页。- 文章 URL 的
lastModified是否来自updated_time。
如果这些都符合预期,基础 SEO 发现和抓取边界就建立起来了。
后续还可以继续优化什么
这次只做了 sitemap 和 robots,属于 SEO 基建。
后续还可以继续做几件事。
第一,文章页加 canonical。
避免带参数 URL 或不同入口造成重复内容。
第二,文章页加结构化数据。
使用 BlogPosting 或 Article JSON-LD,帮助搜索引擎理解标题、作者、发布时间、封面和正文摘要。
第三,首页文章列表 SSR/ISR。
当前首页如果还是客户端拉文章列表,初始 HTML 对搜索引擎并不友好。可以把首页首屏公开文章列表改成服务端输出。
第四,用户公开主页和专栏页 SSR/ISR。
作者文章列表、专栏文章列表也属于有 SEO 价值的公开内容。
第五,图片 alt 和 OG 图片优化。
文章封面、正文图片、分享卡片都会影响搜索展示效果。
总结
这次 SEO 优化做了两件基础但重要的事。
sitemap.ts 负责生成 /sitemap.xml,把首页、用户协议页和所有公开文章页交给搜索引擎发现。
robots.ts 负责生成 /robots.txt,明确禁止搜索引擎抓取 API、编辑器、创作中心、预览页、审核页、测试页和用户私有页。
核心设计原则是:
- sitemap 只放公开、可访问、希望被收录的页面。
- robots 禁止非公开、登录态、编辑态、测试态页面。
- sitemap 和公开 reader 的权限条件保持一致。
- robots 只负责抓取边界,不替代权限控制。
对内容站来说,这一步相当于把“公开内容在哪里”和“哪些地方不要抓”告诉搜索引擎。
配合前面的 reader SSR/ISR 改造,公开文章页不仅能输出完整 HTML,也能更容易被搜索引擎发现,同时避免预览页和编辑页进入搜索结果。