创见博客
给 Next.js 内容站补齐 sitemap 和 robots:一次 SEO 基建实践
七崽爱吃小饼干2026/07/15阅读 4专栏 Next.js/SEO

在做完 reader 页 SSR 和 ISR 之后,文章正文已经可以稳定地出现在初始 HTML 里了。

但对一个内容站来说,仅仅让页面能被服务端渲染还不够。搜索引擎还需要知道:哪些页面值得抓取,哪些页面不应该抓取。

这次做的 SEO 优化就是补齐两个基础能力:

  • sitemap.xml
  • robots.txt

在 Next.js App Router 里,对应的实现就是:

  • src/app/sitemap.ts
  • src/app/robots.ts

这两个文件代码量不多,但它们决定了搜索引擎如何发现站点内容,以及如何避开不应该暴露的页面。

sitemap 是什么

sitemap.xml 可以理解成网站提供给搜索引擎的“公开页面清单”。

搜索引擎当然可以通过页面里的链接慢慢发现内容,但对文章站来说,主动给出 sitemap 会更直接。

比如 Visionary 里真正希望被搜索引擎发现的页面主要是:

  • 首页。
  • 用户协议这类公开静态页面。
  • 已发布且公开的文章详情页。

其中最重要的是文章详情页:

txt
https://visionaryblog.cn/reader/412
https://visionaryblog.cn/reader/413
...

如果这些 URL 出现在 sitemap 里,搜索引擎就能更稳定地发现新文章,也能根据 lastModified 判断哪些文章可能需要重新抓取。

简单说:

txt
sitemap.xml 负责告诉搜索引擎:这些页面值得抓。

robots 是什么

robots.txt 则是另一类信号。

它不是告诉搜索引擎“抓什么”,而是告诉搜索引擎“不要抓什么”。

一个真实项目里,并不是所有页面都应该进入搜索结果。

比如:

  • API 路由。
  • 编辑器页面。
  • 创作中心。
  • 草稿预览页。
  • 审核页。
  • 测试页。
  • 用户私有页面。

这些页面即使能访问,也不应该被搜索引擎收录。

所以 robots.txt 的职责就是:

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 里最重要的原则是:只放可以公开访问、希望被收录的页面。

所以文章查询不能直接查所有文章,更不能把草稿、审核中、私密文章放进去。

这次新增了一个轻量查询方法:

ts
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();
  }
};

这里有两个设计点。

第一,只查公开文章:

sql
WHERE is_published = 1 AND view_permission = 'all'

这和公开 reader 页的访问条件保持一致。

如果一篇文章不能通过 /reader/[articleId] 被公开访问,它就不应该出现在 sitemap。

第二,只查 sitemap 需要的字段:

sql
SELECT id, updated_time

sitemap 不需要文章正文、标题、摘要、标签,所以没必要查大字段。

这能让 sitemap 查询更轻量,尤其文章数量变多之后更重要。

Next.js 里的 sitemap.ts

在 App Router 里,只要新增 src/app/sitemap.ts,Next.js 就会自动生成:

txt
/sitemap.xml

sitemap 不需要每次访问都重新查数据库。搜索引擎本身也不是实时抓取,公开文章进入 sitemap 延迟一天是可以接受的。

所以这里把 sitemap 设置成每天刷新一次:

ts
export const revalidate = 86400;

这样 /sitemap.xml 会被缓存 24 小时。24 小时内访问不会反复查数据库;超过 24 小时后,下次访问会触发重新生成。

当前实现是:

ts
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 支持环境变量:

ts
process.env.NEXT_PUBLIC_SITE_URL || 'https://visionaryblog.cn'

这样以后如果域名变化,或者 staging 环境需要不同站点 URL,可以通过环境变量配置。

第二,replace(/\/$/, '') 去掉结尾斜杠。

这样拼接 URL 时不会出现:

txt
https://visionaryblog.cn//reader/412

第三,文章的 lastModified 使用 updated_time。

ts
lastModified: article.updated_time ? new Date(article.updated_time) : new Date()

这能告诉搜索引擎文章最近更新时间。

第四,不同页面设置不同优先级。

首页优先级最高:

ts
priority: 1

文章页次之:

ts
priority: 0.8

用户协议这类页面不是主要内容入口,所以优先级较低:

ts
priority: 0.2

这些字段不是强制命令,而是给搜索引擎的参考信号。

第五,sitemap 采用按天刷新。

ts
export const revalidate = 86400;

文章详情页是长期缓存加按需刷新,因为用户访问文章时希望尽快看到最新内容。

sitemap 的实时性要求没那么高。它只是帮助搜索引擎发现 URL,新文章晚一点进入 sitemap 通常可以接受。一天刷新一次可以减少数据库查询,也能保持搜索引擎发现能力。

robots 要禁止哪些页面

robots 的设计重点是:不要把非公开、无 SEO 价值或不该暴露的路径交给搜索引擎。

这次禁止了这些路径:

txt
/api/
/creator/
/editor/
/reader/preview/
/reader/review/
/test
/trackTest
/userCenter/myData
/userCenter/readHistory

这些路径可以分成几类。

第一类是接口:

txt
/api/

API 返回的是数据,不是面向搜索结果的页面。

第二类是后台和编辑器:

txt
/creator/
/editor/

这些页面依赖登录态和用户操作,不应该被搜索引擎抓取。

第三类是预览和审核页:

txt
/reader/preview/
/reader/review/

其中 /reader/preview/ 是作者预览非公开文章的入口,必须禁止抓取。

第四类是测试页:

txt
/test
/trackTest

这些页面没有公开 SEO 价值。

第五类是用户私有页:

txt
/userCenter/myData
/userCenter/readHistory

这些页面和当前登录用户相关,也不应该被收录。

Next.js 里的 robots.ts

在 App Router 里,只要新增 src/app/robots.ts,Next.js 就会自动生成:

txt
/robots.txt

当前实现是:

ts
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`,
  };
}

生成出来的语义大概是:

txt
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 解决的是:

txt
公开文章页能不能高效稳定地输出 HTML。

sitemap 解决的是:

txt
搜索引擎怎么发现这些公开文章页。

robots 解决的是:

txt
搜索引擎应该避开哪些非公开或无价值页面。

三者配合之后,公开内容的 SEO 路径就更完整了。

公开文章页通过 /reader/[articleId] 输出完整 HTML。

sitemap 把公开文章 URL 暴露给搜索引擎。

robots 阻止搜索引擎抓取 /reader/preview/、/editor/、/creator/ 等页面。

这能避免两个常见问题。

第一,公开文章已经 SSR/ISR 了,但搜索引擎发现不充分。

第二,搜索引擎抓到一些不该出现在结果里的页面,比如编辑器、预览页、测试页。

怎么确认是否生效

构建后,Next.js 输出里可以看到:

txt
○ /robots.txt
○ /sitemap.xml ... Revalidate 1d

这说明这两个文件已经由 App Router 生成。

其中 Revalidate 1d 表示 sitemap 最多缓存一天。

部署后可以直接访问:

txt
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,也能更容易被搜索引擎发现,同时避免预览页和编辑页进入搜索结果。

评论
0/100