news 2026/9/18 1:36:42

Epic Stack SEO 实战:基于 Resource Route 的 robots.txt 与 sitemap.xml 定制指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Epic Stack SEO 实战:基于 Resource Route 的 robots.txt 与 sitemap.xml 定制指南

Epic Stack SEO 实战:基于 Resource Route 的 robots.txt 与 sitemap.xml 定制指南

【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack

搜索引擎抓取是许多全栈应用上线后的必修课:站点地图(sitemap.xml)帮助爬虫发现内容,robots.txt 声明抓取策略。Epic Stack 作为开箱即用的全栈 Starter,把这两项能力直接内建在框架路由层,开发者无需编写任何服务器中间件,就能在项目里以"声明式"的方式完成 SEO 基础建设。本文将以 docs/seo.md 为主线,结合仓库源码,讲解 Epic Stack 中 meta 标签、robots.txt、sitemap.xml 的开箱用法与按路由定制方案,读完你就能在自己的 Epic Stack 项目中配置一套完整的搜索引擎可见性策略。

一、Epic Stack 的 SEO 能力全景

Epic Stack 的 SEO 能力由两层构成:

  1. 按路由设置 meta 标签:React Router(Remix)框架原生支持在每条路由上通过meta导出声明页面的<title><meta name="description">等标签,无需手动拼装<head>
  2. 资源路由(Resource Route)生成 robots.txt 与 sitemap.xml:借助@nasa-gcn/remix-seo库,以/robots.txt/sitemap.xml两个端点动态输出标准文件,所有逻辑都是框架内的 TypeScript 代码。

其中第 2 层是本仓库的默认配置重点:sitemap 默认包含所有路由,但你可以通过路由的handle导出按需定制每条路由是否进入、以及如何进入站点地图。下面先从默认实现讲起。

二、开箱即用的 robots.txt 与 sitemap.xml

在 Epic Stack 中,这两个端点分别由_seo布局下的两个资源路由提供:

  • app/routes/_seo/robots[.]txt.ts
  • app/routes/_seo/sitemap[.]xml.ts

2.1 robots.txt:声明 sitemap 位置

robots 路由 的实现非常简洁,核心只有一行:

import { generateRobotsTxt } from '@nasa-gcn/remix-seo' import { getDomainUrl } from '#app/utils/misc.tsx' import { type Route } from './+types/robots[.]txt.ts' export function loader({ request }: Route.LoaderArgs) { return generateRobotsTxt([ { type: 'sitemap', value: `${getDomainUrl(request)}/sitemap.xml` }, ]) }

generateRobotsTxt接收一个规则数组,这里仅声明了一条Sitemap:指令,指向当前域名的/sitemap.xml。注意:

  • 没有添加User-agent/Disallow规则,即对所有爬虫默认放行,仅告知站点地图位置;
  • 域名由getDomainUrl(request)动态推导(见 2.3),因此无需硬编码生产域名。

2.2 sitemap.xml:动态生成并做边缘缓存

sitemap 路由 调用generateSitemap完成站点地图的生成:

import { generateSitemap } from '@nasa-gcn/remix-seo' import { getDomainUrl } from '#app/utils/misc.tsx' import { type Route } from './+types/sitemap[.]xml.ts' export async function loader({ request, context }: Route.LoaderArgs) { // TODO: This is typeerror is coming up since of the remix-run/server-runtime package. We might need to remove/update that one. // @ts-expect-error return generateSitemap(request, context.serverBuild.routes, { siteUrl: getDomainUrl(request), headers: { 'Cache-Control': `public, max-age=${60 * 5}`, }, }) }

几个值得注意的实现细节:

  • 自动枚举路由context.serverBuild.routes是构建时生成的路由表,generateSitemap会遍历其中所有路由,默认把每条路由都作为独立 URL 输出(URL 依据框架对路由文件名的约定自动推导)。
  • CDN 友好的缓存:响应头显式设置了Cache-Control: public, max-age=300(5 分钟),方便接入 CDN 或代理层缓存,降低动态生成的频率。仓库中源码注释也说明此处存在一个来自remix-run/server-runtime的类型兼容问题,当前以@ts-expect-error规避。
  • 动态 URL:通过siteUrl参数(来自getDomainUrl)拼出完整的绝对 URL,而非相对路径。

2.3 getDomainUrl:域名推导的底层原理

sitemap.xmlrobots.txt都依赖 app/utils/misc.tsx 中的getDomainUrl

export function getDomainUrl(request: Request) { const host = request.headers.get('X-Forwarded-Host') ?? request.headers.get('host') ?? new URL(request.url).host const protocol = request.headers.get('X-Forwarded-Proto') ?? 'http' return `${protocol}://${host}` }

推导优先级依次为:X-Forwarded-Hosthost→ 请求 URL 中的 host;协议同样优先取X-Forwarded-Proto,默认回退为http。这套逻辑对部署在反代/负载均衡(如 Fly.io、Nginx)之后的应用尤为重要——只有在生产环境正确透传X-Forwarded-*头,站点地图中的 URL 才会指向正确的公开域名,否则可能出现 "http://内部地址/sitemap.xml" 之类的问题。

三、默认策略:所有路由进入 sitemap 的安全边界

默认情况下,所有路由都会被generateSitemap收录。这对"页面即内容"的营销站点很友好,但意味着像登录、注册、设置中心这类私有页面也会被列进站点地图。因此在 Epic Stack 中有一条明确的工程约定(见 docs/seo.md):

只有面向公众的页面才应被包含进 sitemap.xml。

从源码结构看,仓库已经为大量非公开路由显式关闭了收录,例如:

  • 认证类:app/routes/_auth/login.tsx、app/routes/_auth/signup.tsx、app/routes/_auth/verify.tsx、app/routes/_auth/forgot-password.tsx、app/routes/_auth/reset-password.tsx
  • 设置中心:app/routes/settings/profile/下的index.tsxchange-email.tsxpassword.tsxphoto.tsxconnections.tsxtwo-factor/*等全部路由
  • 管理后台:app/routes/admin/cache/index.tsx

它们的写法完全一致——通过路由handle导出返回nullgetSitemapEntries(细节见下一节)。这为自定义项目提供了一个可直接照搬的"排除清单"模式。

四、按路由定制 sitemap:SEOHandle 与 handle 导出

@nasa-gcn/remix-seo通过SEOHandle类型约定路由的 sitemap 行为。你需要在目标路由文件中:

  1. @nasa-gcn/remix-seo导入type SEOHandle
  2. 导出handle对象,实现getSitemapEntries

4.1 动态生成多个条目:从数据库拉取内容

对于内容型站点(博客、文档、商品列表),最常见的需求是让每篇内容都生成独立的站点地图条目。官方推荐写法(与 docs/seo.md 一致)如下:

// routes/blog/_layout.tsx import { type SEOHandle } from '@nasa-gcn/remix-seo' import { serverOnly$ } from 'vite-env-only/macros' export const handle: SEOHandle = { getSitemapEntries: serverOnly$(async (request) => { const blogs = await db.blog.findMany() return blogs.map((blog) => { return { route: `/blog/${blog.slug}`, priority: 0.7 } }) }), }

要点拆解:

  • 返回条目结构:数组中的每个元素包含route(相对路径)和可选的priority(优先级,0.0~1.0,用于提示搜索引擎该 URL 的相对重要程度)。
  • 异步 + 注入请求getSitemapEntries接收request,可结合登录态、区域(参考 docs/decisions/009-region-selection.md)等上下文做差异化输出,也可以像示例一样直接查询数据库。
  • 不要返回动态路由本身的 URL:注意示例返回的是/blog/${blog.slug}这类具体内容页;/blog/:slug这样的动态占位路径不能直接进入 sitemap,必须在函数里展开成真实地址列表。

4.2 serverOnly$:把服务端逻辑从客户端构建中剥离

handle是路由的导出对象,同时存在于客户端与服务端构建中。而getSitemapEntries内部如果引用了数据库、密钥等仅服务端可用的代码,把它带进客户端 bundle 就会导致构建失败或泄露风险。

解决方案是vite-env-only提供的serverOnly$宏。Epic Stack 的 vite.config.ts 中已经预置了envOnlyMacros()插件:

import { envOnlyMacros } from 'vite-env-only' // ... plugins: [ // ... envOnlyMacros(), // ... ],

package.json中对应依赖为vite-env-only(当前仓库版本为^3.0.3)。编译时宏会把包裹的函数替换为空实现,保证只在服务端构建中保留真实逻辑。这也是handle这种"两端共享、逻辑不同"的导出的标准处理手法——凡是依赖 Node.js 运行时的 sitemap 逻辑,都应包上serverOnly$

4.3 排除路由:让 URL 不出现在 sitemap 中

对于登录页、设置页等私有页面,只需让getSitemapEntries返回null(docs/seo.md 中的第二个示例,也是 3 节中仓库各路由的实际写法):

// in your routes/url-that-doesnt-need-sitemap import { type SEOHandle } from '@nasa-gcn/remix-seo' import { type Route } from './+types/sitemap[.]xml.ts' export async function loader({ request }: Route.LoaderArgs) { /**/ } export const handle: SEOHandle = { getSitemapEntries: () => null, }

返回null即表示"该路由不生成任何站点地图条目"。这种写法无需serverOnly$——因为函数体内没有任何服务端专属逻辑,返回一个常量即可。

4.4 两者结合的仓库实况

在 Epic Stack 中,"排除"写法被大量使用(见 3 节清单),而"动态展开"写法对应getSitemapEntries的完整能力。一个完整的自定义策略可以是:营销落地页、关于页、隐私条款等公开页面保留默认收录;登录/注册/设置等私有页面返回null排除;内容列表通过异步查询动态生成条目并设置priority。你可以在 app/routes/_marketing/ 与 app/routes/_auth/ 目录下对比观察这两类路由的实现差异。

五、meta 标签与全局抓取开关

除 sitemap 外,Epic Stack 的 SEO 还包含两个值得配套使用的点。

5.1 按路由设置 meta

React Router 原生支持路由级meta导出。根路由 app/root.tsx 提供了一个全局兜底示例:

export const meta: Route.MetaFunction = ({ data }) => { return [ { title: data ? 'Epic Notes' : 'Error | Epic Notes' }, { name: 'description', content: `Your own captain's log` }, ] }

MetaFunction接收路由loader的返回值data,据此动态切换页面标题与描述;任何子路由都可以导出自己的meta覆盖父级设置,实现每页独立的标题与描述。

5.2 ALLOW_INDEXING:一键开关全站收录

在 app/root.tsx 中,还有一个全局级别的抓取开关:

const allowIndexing = ENV.ALLOW_INDEXING !== 'false' // ... {allowIndexing ? null : ( <meta name="robots" content="noindex, nofollow" /> )}

当环境变量ALLOW_INDEXING'false'时,所有页面都会注入<meta name="robots" content="noindex, nofollow" />,阻止搜索引擎收录与跟踪。该变量在 app/utils/env.server.ts 中通过 Zod 校验,取值限定为'true' | 'false',可选。它非常适合预发布/沙箱环境:在不删除任何 sitemap 代码的前提下,一键让整站对搜索引擎"隐身",而生产环境只需设置ALLOW_INDEXING=true即可正常放行。

六、实践清单与注意事项

综合 docs/seo.md 与仓库源码,落地一套 Epic Stack SEO 配置时建议遵循以下清单:

  1. 确认反代透传:部署在反向代理/边缘网络后时,确认X-Forwarded-HostX-Forwarded-Proto正确传递,否则getDomainUrl推导出的站点地图域名会出错;
  2. 审阅默认收录generateSitemap默认收录所有路由,新加路由后检查它是否是公开页面,私有页面务必补上getSitemapEntries: () => null
  3. 服务端逻辑必包宏:凡在getSitemapEntries中使用数据库或 Node 专属 API,一律用serverOnly$包裹,客户端构建会自动剥离;
  4. 动态路由展开:参数化路由(如/notes/:noteId)不会自动生成真实 URL,需要在getSitemapEntries中查询数据后展开为具体路径,并可用priority表达内容优先级;
  5. 配套使用缓存与开关:sitemap 响应自带public, max-age=300缓存头可配合 CDN;非生产环境记得设置ALLOW_INDEXING=false阻止被索引。

这套方案的突出价值在于:sitemap 与 robots 的维护完全融入路由代码——路由即内容,路由即收录策略,既没有额外的中间件配置,也不存在运行时再同步的静态文件,搜索引擎可见性始终与你的代码变更保持一致。

【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 1:36:38

Unity适配鸿蒙:重构级NDK桥接实战指南

1. 项目概述&#xff1a;Unity构建鸿蒙环境不是“移植”&#xff0c;而是重构级适配 Unity构建鸿蒙环境和直接发布鸿蒙应用——这句话乍看像一句技术宣传语&#xff0c;实则藏着一个被大量开发者误读的底层事实&#xff1a; Unity官方至今&#xff08;2024年中&#xff09;并…

作者头像 李华
网站建设 2026/9/18 1:35:00

大一高数无穷级数思维脚手架:审敛法失效场景与幂级数端点处理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 1:34:34

Win11修改用户名:显示名、SAM账户名与C:\Users文件夹全解析

上周帮同事收拾一台笔记本&#xff0c;毛病特别典型&#xff1a;系统是家里人帮忙装的&#xff0c;装的时候随手把用户名填成了中文名&#xff0c;于是C:\Users底下就躺着一个三汉字的文件夹。平时刷网页看视频毫无问题&#xff0c;直到他装某个开发工具&#xff0c;安装脚本直…

作者头像 李华
网站建设 2026/9/18 1:34:27

CentOS 7无网环境离线安装MySQL 8.0.36完整实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 1:34:16

Flutter在OpenHarmony中优化动作表性能与跨设备适配

1. 项目背景与价值解析在OpenHarmony生态中实现流畅的交互组件一直是个技术难点。传统Native开发方式需要针对不同设备类型重复编写UI代码&#xff0c;而Flutter的跨平台特性恰好能弥补这一短板。这次我们选择"动作表"&#xff08;ActionSheet&#xff09;作为切入点…

作者头像 李华
网站建设 2026/9/18 1:33:08

企业档案管理数字化转型:痛点解析与解决方案

1. 档案管理数字化转型的痛点与机遇在各类企事业单位的日常运营中&#xff0c;跨部门档案调阅是再常见不过的基础需求。我曾在一家大型制造企业的档案室工作过三年&#xff0c;最常听到的抱怨就是&#xff1a;"这批生产档案怎么还没调过来&#xff1f;""上周申请…

作者头像 李华