Skip to content

feat(seo): SEO 仪表盘 + sitemap/robots 路由代理与 origin 修正#50

Merged
one-ea merged 2 commits into
mainfrom
dev
Apr 25, 2026
Merged

feat(seo): SEO 仪表盘 + sitemap/robots 路由代理与 origin 修正#50
one-ea merged 2 commits into
mainfrom
dev

Conversation

@one-ea

@one-ea one-ea commented Apr 25, 2026

Copy link
Copy Markdown
Owner

变更摘要

新增管理后台 SEO 仪表盘,并修复 sitemap/robots 在 Pages 域下被 SPA fallback 截胡 + 后端 `` 误用 Workers 自身域名两个问题。

主要改动

前端

  • 新增 `client/src/lib/seo-analyzer.ts`(318 行)— SEO 评分算法
  • 新增 `client/src/pages/admin/seo.tsx`(343 行)— SEO 仪表盘页面
  • `client/src/components/admin-layout.tsx` — 加 SEO 入口导航
  • `client/src/app.tsx` — 注册 `/admin/seo` lazy route
  • `client/src/pages/admin/dashboard.tsx` — 文章列表独立滚动容器

Pages Functions

  • 新增 `client/functions/sitemap.xml.ts` — 代理 `/sitemap.xml` 到 Workers,Content-Type 透传 `application/xml`
  • 新增 `client/functions/robots.txt.ts` — 代理 `/robots.txt` 到 Workers,Content-Type 透传 `text/plain`

Workers

  • `server/src/index.ts` — Bindings 加 `SITE_ORIGIN?`,sitemap/robots 路由优先读取该 env,软回退 `new URL(c.req.url).origin`
  • `server/wrangler.toml` — 配置 `SITE_ORIGIN = "https://monolith-client.pages.dev"\`

测试结果

预览部署 https://ebf37c58.monolith-client.pages.dev 已验证:

端点 Content-Type 内容
`/sitemap.xml` `application/xml` ✅ 14 个 `` 全部以 `https://monolith-client.pages.dev\` 开头
`/robots.txt` `text/plain` ✅ `Sitemap:` 指向 `https://monolith-client.pages.dev/sitemap.xml\`
`/admin/seo` HTTP 200 ✅ SEO 仪表盘渲染正常
`/api/health` HTTP 200 ✅ Workers 健康

typecheck(client functions + server)零错误。

风险评估

  • 新增字段 `SITE_ORIGIN` 为 optional,env 缺失时软回退到原行为,向下兼容
  • Pages Functions 新增 2 个端点都是纯透传代理,无业务逻辑
  • 文章列表滚动容器改造仅影响 `/admin` 视觉,不动数据

- 新增 client/src/lib/seo-analyzer.ts: SEO 评分算法(318 行)
- 新增 client/src/pages/admin/seo.tsx: SEO 仪表盘页面(343 行)
- client/src/components/admin-layout.tsx: 加 SEO 入口导航
- client/src/app.tsx: 注册 /admin/seo lazy route
- client/src/pages/admin/dashboard.tsx: 文章列表独立滚动容器
- 新增 client/functions/sitemap.xml.ts: 代理 /sitemap.xml 到 Workers
- 新增 client/functions/robots.txt.ts: 代理 /robots.txt 到 Workers
- server/src/index.ts: Bindings 加 SITE_ORIGIN,sitemap/robots 优先读取
- server/wrangler.toml: 配置 SITE_ORIGIN=https://monolith-client.pages.dev
@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@one-ea has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 47 minutes and 6 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 47 minutes and 6 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b09ec0fe-ff46-456a-86a6-d7a4aae904c5

📥 Commits

Reviewing files that changed from the base of the PR and between fb5076a and a70765a.

📒 Files selected for processing (4)
  • client/functions/robots.txt.ts
  • client/functions/sitemap.xml.ts
  • client/src/lib/seo-analyzer.ts
  • client/src/pages/admin/seo.tsx
📝 Walkthrough

Walkthrough

本PR实现完整的SEO分析功能,包括SEO检查引擎、管理后台页面、Pages Functions代理层,以及后端环境变量支持。提供按维度分类的评分系统、故障检查、关键词提取和内容质量评估。

Changes

Cohort / File(s) Summary
SEO分析核心库
client/src/lib/seo-analyzer.ts
新增318行的SEO分析引擎,包含13项确定性检查(元数据、结构化数据、内容质量、可读性),导出规范化类型和8个工具函数,核心逻辑:analyzePost()计算单篇报告,buildOverview()聚合站点指标、维度评分、关键词频率。
SEO管理页面
client/src/pages/admin/seo.tsx
新增343行的管理页面,集成数据获取、tab过滤(全部/警告/差/草稿)、搜索、评分环形图、维度分布柱状图、全局检查网格、文章健康表、关键词云和Sitemap/Robots预览。
管理界面集成
client/src/app.tsx, client/src/components/admin-layout.tsx
注册/admin/seo路由和导航菜单项,新增Sparkles图标,总计+4行。
Pages Functions代理
client/functions/robots.txt.ts, client/functions/sitemap.xml.ts
各新增23行,实现流式代理逻辑:解析后端URL、返回错误响应、构建目标URL、转发原始方法/请求头、返回流式响应保留状态和请求头。
后端环保
server/src/index.ts, server/wrangler.toml
后端添加可选SITE_ORIGIN绑定和配置变量,生成sitemap/robots的绝对URL时优先使用,默认回退至请求源。
仪表板UI优化
client/src/pages/admin/dashboard.tsx
文章列表改为flex-column容器,header/toolbar和加载状态设shrink-0,列表区域独立滚动(overflow-y-auto)确保固定高度约束下响应式布局。

Sequence Diagram

sequenceDiagram
    participant User as 用户
    participant Page as AdminSeo页面
    participant API as 后端API
    participant PF as Pages Functions
    participant Engine as SEO引擎

    User->>Page: 访问/admin/seo
    activate Page
    Page->>API: fetchAdminPosts()
    activate API
    API-->>Page: 所有文章数据
    deactivate API
    
    Page->>PF: GET /sitemap.xml
    activate PF
    PF->>API: 代理请求
    API-->>PF: XML响应
    PF-->>Page: 流式响应
    deactivate PF
    
    Page->>PF: GET /robots.txt
    activate PF
    PF->>API: 代理请求
    API-->>PF: 文本响应
    PF-->>Page: 流式响应
    deactivate PF
    
    Page->>Engine: buildOverview(文章数据)
    activate Engine
    Engine->>Engine: 遍历计算analyzePost()
    Engine->>Engine: 聚合维度评分、关键词、分布
    Engine-->>Page: SeoOverview报告
    deactivate Engine
    
    Page->>Page: 过滤/搜索/排序
    Page-->>User: 渲染评分表、检查项、关键词云
    deactivate Page
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

关注点:SEO分析引擎的13项检查规则是否准确(尤其是标题/摘要长度、中英文关键词双语提取、图片alt检测);seo.tsx的过滤/排序/搜索逻辑一致性;双主题/响应式支持(暗色评分环配色、表格滚动布局);Pages Functions代理的流式处理和错误恢复。

Possibly related PRs

Suggested labels

frontend, backend, feature

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR标题遵循Conventional Commits格式,包含type(feat)、scope(seo)和明确的description,准确反映了SEO仪表盘新增与sitemap/robots路由修复的核心改动。
Description check ✅ Passed PR描述详尽阐述了变更摘要、主要改动、测试结果和风险评估,完全关联到changeset中的所有文件改动及其目的。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added frontend 前端 (React/Vite) 相关变更 backend 后端 (Hono Workers) 相关变更 feature 新功能 labels Apr 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/index.ts (1)

336-336: ⚠️ Potential issue | 🟡 Minor

RSS feed 未同步使用 SITE_ORIGIN,绝对 URL 仍可能错位

/rss.xmlsiteUrl 仍然是 new URL(c.req.url).origin,没有像 sitemap/robots 一样优先读 c.env.SITE_ORIGIN。如果 RSS 端点在 Pages 域下也通过 Functions 代理(或将来添加),生成的 <link> / <guid> 会指向 Workers 自身域名,与 sitemap 的输出不一致。

建议同步切换:

-  const siteUrl = new URL(c.req.url).origin;
+  const siteUrl = c.env.SITE_ORIGIN || new URL(c.req.url).origin;

另外:本 PR 在 client/functions/ 加了 sitemap/robots 代理,但 rss.xml 没有;若希望 RSS 也走 Pages 域名访问(避免 SPA fallback 截取),还需补一个 client/functions/rss.xml.ts

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` at line 336, 在生成 RSS 的地方不要直接用 new URL(c.req.url).origin
— 改为优先使用环境变量 SITE_ORIGIN(即当 c.env.SITE_ORIGIN 存在时用它),否则回退到 new
URL(c.req.url).origin;在 server/src/index.ts 中更新 siteUrl 的赋值(变量名 siteUrl)以先读
c.env.SITE_ORIGIN。若还希望 RSS 通过 Pages 域名暴露以避免 SPA fallback,另外新增一个代理函数
client/functions/rss.xml.ts(与 sitemap/robots 相同的代理逻辑)。
🧹 Nitpick comments (8)
client/src/lib/seo-analyzer.ts (2)

127-127: slug 规范校验逻辑在多处重复,建议抽取共享函数

相同的判定 /^[a-z0-9-]+$/.test(slug) && !slug.includes("--") && !slug.startsWith("-") && !slug.endsWith("-") 同时出现在 client/src/pages/admin/dashboard.tsx:281,且 server/src/index.ts 中没有对应的服务端校验。

建议在 client/src/lib/seo-analyzer.ts 顶部导出一个 isValidSlug(slug: string): boolean,dashboard 复用之;服务端创建/更新文章接口也建议加上同样的校验,避免脏数据进入数据库。

♻️ 抽取共享函数
+/** 校验 slug 是否为合法的 URL 友好格式(小写字母+数字+连字符,不允许首尾或连续连字符) */
+export function isValidSlug(slug: string): boolean {
+  return /^[a-z0-9-]+$/.test(slug)
+    && !slug.includes("--")
+    && !slug.startsWith("-")
+    && !slug.endsWith("-");
+}
+
 export function analyzePost(p: AnalyzeInput): PostSeoReport {
   const checks: SeoCheckResult[] = [];
   ...
-  const slugOk = /^[a-z0-9-]+$/.test(p.slug) && !p.slug.includes("--") && !p.slug.startsWith("-") && !p.slug.endsWith("-");
+  const slugOk = isValidSlug(p.slug);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/lib/seo-analyzer.ts` at line 127, Duplicate slug validation logic
is repeated and missing server-side checks; extract the predicate into a shared
function isValidSlug(slug: string): boolean at the top of
client/src/lib/seo-analyzer.ts (implement the existing logic:
/^[a-z0-9-]+$/.test(slug) && !slug.includes("--") && !slug.startsWith("-") &&
!slug.endsWith("-")), export it, then replace the inline checks in
client/src/pages/admin/dashboard.tsx (where p.slug is validated) to call
isValidSlug, and add the same validation call in the server article
create/update handlers in server/src/index.ts to reject invalid slugs before
persisting.

264-270: publishedPosts === 0totalScore 会是 0,不够友好

新站/全部草稿场景下,publishedReports 为空,所有 dimensionScores 落到 0totalScore 也变成 0,仪表盘显示"红色 0%"会误导用户认为站点 SEO 极差。

建议在没有已发布文章时直接返回中性状态(如 null / --),由 UI 层判定显示空态。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/lib/seo-analyzer.ts` around lines 264 - 270, When there are no
published posts the current calculation produces 0 which is misleading; update
the logic around dimensionScores/totalScore to short-circuit when there are no
published posts (check the existing publishedPosts or publishedReports counter)
and return a neutral value (e.g. null or undefined) instead of 0 so the UI can
render an empty state. Locate the aggregation using dimAcc and the computed
dimensionScores and adjust the code that computes totalScore to first test
publishedPosts/publishedReports === 0 and handle that case by returning the
neutral sentinel rather than computing/rounding a 0 score.
server/src/index.ts (1)

373-373: SITE_ORIGIN 做最小规范化,避免运维误配

如果运维误填了带尾斜杠的值(例如 "https://monolith-client.pages.dev/"),/sitemap.xml 会输出 <loc>https://monolith-client.pages.dev//posts/foo</loc>/robots.txt 会出现 Sitemap: https://.../sitemap.xml 之前多一个斜杠。建议在使用前做一次 trim + 去尾斜杠:

🛡️ 防御性规范化
-  const siteUrl = c.env.SITE_ORIGIN || new URL(c.req.url).origin;
+  const siteUrl = (c.env.SITE_ORIGIN?.trim().replace(/\/+$/, "")) || new URL(c.req.url).origin;

可考虑抽成 helper 在两处复用。

Also applies to: 428-428

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` at line 373, The SITE_ORIGIN value may include trailing
whitespace or a trailing slash causing double-slashes in outputs; normalize it
before use by trimming whitespace and removing any trailing slash, then fall
back to new URL(c.req.url).origin if empty. Update the code that builds siteUrl
(const siteUrl = c.env.SITE_ORIGIN || new URL(c.req.url).origin) to call a small
helper like normalizeSiteOrigin(value) that returns the trimmed value without a
trailing slash (and returns null/undefined for empty strings) and use that
helper in both places where SITE_ORIGIN is read (the siteUrl assignment and the
robots/sitemap generation code) so all consumers get the canonical origin.
server/wrangler.toml (1)

9-10: 自定义域名切换时记得同步更新 SITE_ORIGIN

当前硬编码为 Pages 预览域名 https://monolith-client.pages.dev。一旦绑定自定义域,需要同步修改此值,否则 sitemap 中的 <loc> 与 robots 中的 Sitemap: 仍会指向 Pages 默认域。考虑在注释中加一句提醒,或后续改为按部署环境注入。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/wrangler.toml` around lines 9 - 10, The SITE_ORIGIN value is hardcoded
to "https://monolith-client.pages.dev" which will break sitemap/robots when
switching to a custom domain; update SITE_ORIGIN whenever you bind a custom
domain or change the implementation to read an environment/deployment variable
(e.g., process.env.SITE_ORIGIN or equivalent runtime injection) instead of the
literal, and add a short inline comment by the SITE_ORIGIN declaration reminding
maintainers to sync this value with any custom domain changes.
client/src/pages/admin/dashboard.tsx (1)

179-179: max-h-[calc(100vh-260px)] 依赖魔法值,需关注上方区域高度变化

260px 是当前“顶栏 + 数据概览 + 搜索框”累计高度的硬编码估算。后续若新增/删除概览卡或调整间距,列表区会出现被截断或底部留白。

可选改进方向(非必须):

  • 将外层 <div className="mx-auto …"> 改为 flex flex-col min-h-screen,把列表容器的 lg:max-h 改为 lg:flex-1 lg:min-h-0,让浏览器自动算高;
  • 或者使用 CSS Grid 的 grid-template-rows: auto auto auto 1fr 让列表区天然占满剩余空间。

当前实现功能正常,仅是维护性建议。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/admin/dashboard.tsx` at line 179, The hardcoded
lg:max-h-[calc(100vh-260px)] on the div with className "flex flex-col min-h-0
lg:max-h-[calc(100vh-260px)]" relies on a magic 260px value and should be made
flexible: update the surrounding layout (the outer wrapper element currently
using "mx-auto …") to use a full-height flex container (e.g., "flex flex-col
min-h-screen") and change this inner list container to use flexible growth
instead of a calc max-height (e.g., "flex flex-col min-h-0 lg:flex-1
lg:min-h-0") so the list naturally fills remaining space; alternatively replace
the layout with a CSS Grid using grid-template-rows: auto auto auto 1fr to let
the list area occupy the leftover space.
client/functions/robots.txt.ts (1)

8-8: 需导出 ApiEnv,然后在所有 PagesFunction 中使用统一的类型声明

_shared.ts 中的 ApiEnv 接口未被导出,建议先在 _shared.ts 第 1 行改为 export interface ApiEnv,然后在 robots.txt.tssitemap.xml.tsrss.xml.tsapi/[[path]].tscdn/[[path]].ts 中统一改为 PagesFunction<ApiEnv>

虽然 getBackendUrl 已正确处理 undefined 的情况(通过可选链 ?. 和 null 检查),但类型声明中将 API_BASE: string(必需)与实际的 API_BASE?: string(可选)不一致会误导类型检查。统一使用导出的 ApiEnv 可提高类型安全性。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/functions/robots.txt.ts` at line 8, 在 _shared.ts 中将 interface ApiEnv
改为导出(export interface ApiEnv),然后把 robots.txt.ts 中的 PagesFunction<{ API_BASE:
string }> 以及 sitemap.xml.ts、rss.xml.ts、api/[[path]].ts、cdn/[[path]].ts
中的类似声明统一替换为 PagesFunction<ApiEnv>;保持 getBackendUrl 对 API_BASE 可选性的处理不变(因为实际是
API_BASE?: string),以确保类型声明与实际行为一致并提升类型安全。
client/src/pages/admin/seo.tsx (2)

67-70: document.title 未在卸载时还原。

切换到其他 admin 子路由时,标题会保留为「SEO 优化 | Monolith」直到下个页面再次覆写;如果某个目标页没设置 title,浏览器标签会一直定格在此。建议在 useEffect 中返回清理函数还原原始标题,或改用统一的 useDocumentTitle 工具。

   useEffect(() => {
+    const prev = document.title;
     document.title = "SEO 优化 | Monolith";
     loadAll();
+    return () => {
+      document.title = prev;
+    };
   }, []);

As per coding guidelines: 页面级组件审查时关注「SEO 相关(页面标题、meta 标签)」。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/admin/seo.tsx` around lines 67 - 70, The effect that sets
document.title inside useEffect currently changes the page title without
restoring it; modify the useEffect in the component to capture the original
title (const prev = document.title) before setting document.title = "SEO 优化 |
Monolith" and return a cleanup function that restores document.title = prev, or
replace the manual logic by using the shared useDocumentTitle hook to set and
auto-restore the title; reference the existing useEffect and loadAll calls when
making the change.

58-58: 正则解析 sitemap 不解码 XML 实体,建议改用 DOMParser

/<loc>([^<]+)<\/loc>/g 取到的字符串若包含 &amp; / &#39; 等实体不会被还原,进入 UI 后展示和点击跳转都会失真(虽然当前后端生成的 14 条 URL 暂未触发,但带 query 参数或多语言路径的链接很容易踩到)。

-      const urls = Array.from(smRes.matchAll(/<loc>([^<]+)<\/loc>/g)).map((m) => m[1]);
+      const doc = new DOMParser().parseFromString(smRes, "application/xml");
+      const urls = Array.from(doc.getElementsByTagName("loc")).map((n) => n.textContent ?? "").filter(Boolean);

顺带还能在 doc.querySelector("parsererror") 上识别后端返回非法 XML 的情况。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/admin/seo.tsx` at line 58, The sitemap parsing using
smRes.matchAll(/<loc>([^<]+)<\/loc>/g) doesn't decode XML entities; replace this
with DOMParser: parse smRes into a document, check for parsing errors via
doc.querySelector("parsererror") and handle/report them, then extract URLs by
selecting all <loc> elements (e.g., doc.querySelectorAll("loc")) and reading
each element's textContent to get decoded URLs before building the urls array;
update the code that currently assigns urls from smRes.matchAll to use this
DOMParser-based approach and add error handling for invalid XML.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/functions/robots.txt.ts`:
- Around line 8-22: The onRequest handler currently forwards any HTTP method and
all request headers to the backend, which is unnecessary and unsafe for static
endpoints; update onRequest (and related logic using getBackendUrl,
buildTargetUrl, createPlainApiBaseErrorResponse) to only allow GET and HEAD
(return 405 for others), and send a minimal, explicit header set (e.g., Accept
and User-Agent) instead of context.request.headers so cookies/Authorization are
not leaked; also wrap the fetch(target, ...) in try/catch and add a timed fetch
(or AbortController) so failures return a controlled 502/plain-text response
rather than bubbling an exception.

In `@client/src/lib/seo-analyzer.ts`:
- Around line 280-285: globalChecks currently hardcodes all items as passing
which is misleading when features like rss_enabled or a missing API_BASE can
make /rss.xml, /sitemap.xml or /robots.txt return 404/500; replace the static
pass values by performing runtime probes in the AdminSeo UI: call
fetch('/rss.xml', { method: 'HEAD' }) and fetch('/sitemap.xml'|'/robots.txt', {
method: 'HEAD' }) (handle network errors and non-2xx statuses) and update the
globalChecks entries (the array named globalChecks) with correct
status/score/detail based on response, falling back to "not configured" or
"error" if probe fails; alternatively if you prefer not to probe, rename the
array/labels from "checks" to "feature list" to avoid implying verification
(update references to globalChecks and labels in AdminSeo and any UI
components).

In `@client/src/pages/admin/seo.tsx`:
- Around line 86-100: The filteredReports useMemo currently builds slugSet by
returning p.published for any tab other than "drafts", which causes the "all"
tab to show only published posts; update the logic in the slugSet construction
(inside filteredReports) to explicitly handle tab values so "all" includes both
published and draft posts (e.g., if tab === "drafts" return !p.published; else
if tab === "published" return p.published; else /* "all" */ return true),
ensuring slugSet contains slugs for the intended set of posts and downstream
filters/search still operate on that full set.
- Around line 49-65: loadAll 目前只有 try/finally,缺少错误处理与 IO 隔离:为 loadAll 补上 catch
分支并维护一个 error 状态(例如 setError),在
fetchAdminPosts、fetch("/sitemap.xml")、fetch("/robots.txt") 三个请求间做隔离(用单独的
try/catch 或 Promise.allSettled)以防单个请求失败导致整个页面无提示;在各自失败时填入合理的回退值(空
sitemapPreview/robotsPreview 或带错误信息的对象)并调用
setSitemapPreview/setRobotsPreview/setPosts 或 setError 对应更新,最后在 UI 的头部或表格区域根据
error 渲染用户可见的错误提示;保留 finally 中对 setRefreshing(false)/setLoading(false)
的调用以保证状态被清理。

---

Outside diff comments:
In `@server/src/index.ts`:
- Line 336: 在生成 RSS 的地方不要直接用 new URL(c.req.url).origin — 改为优先使用环境变量
SITE_ORIGIN(即当 c.env.SITE_ORIGIN 存在时用它),否则回退到 new URL(c.req.url).origin;在
server/src/index.ts 中更新 siteUrl 的赋值(变量名 siteUrl)以先读 c.env.SITE_ORIGIN。若还希望 RSS
通过 Pages 域名暴露以避免 SPA fallback,另外新增一个代理函数 client/functions/rss.xml.ts(与
sitemap/robots 相同的代理逻辑)。

---

Nitpick comments:
In `@client/functions/robots.txt.ts`:
- Line 8: 在 _shared.ts 中将 interface ApiEnv 改为导出(export interface ApiEnv),然后把
robots.txt.ts 中的 PagesFunction<{ API_BASE: string }> 以及
sitemap.xml.ts、rss.xml.ts、api/[[path]].ts、cdn/[[path]].ts 中的类似声明统一替换为
PagesFunction<ApiEnv>;保持 getBackendUrl 对 API_BASE 可选性的处理不变(因为实际是 API_BASE?:
string),以确保类型声明与实际行为一致并提升类型安全。

In `@client/src/lib/seo-analyzer.ts`:
- Line 127: Duplicate slug validation logic is repeated and missing server-side
checks; extract the predicate into a shared function isValidSlug(slug: string):
boolean at the top of client/src/lib/seo-analyzer.ts (implement the existing
logic: /^[a-z0-9-]+$/.test(slug) && !slug.includes("--") &&
!slug.startsWith("-") && !slug.endsWith("-")), export it, then replace the
inline checks in client/src/pages/admin/dashboard.tsx (where p.slug is
validated) to call isValidSlug, and add the same validation call in the server
article create/update handlers in server/src/index.ts to reject invalid slugs
before persisting.
- Around line 264-270: When there are no published posts the current calculation
produces 0 which is misleading; update the logic around
dimensionScores/totalScore to short-circuit when there are no published posts
(check the existing publishedPosts or publishedReports counter) and return a
neutral value (e.g. null or undefined) instead of 0 so the UI can render an
empty state. Locate the aggregation using dimAcc and the computed
dimensionScores and adjust the code that computes totalScore to first test
publishedPosts/publishedReports === 0 and handle that case by returning the
neutral sentinel rather than computing/rounding a 0 score.

In `@client/src/pages/admin/dashboard.tsx`:
- Line 179: The hardcoded lg:max-h-[calc(100vh-260px)] on the div with className
"flex flex-col min-h-0 lg:max-h-[calc(100vh-260px)]" relies on a magic 260px
value and should be made flexible: update the surrounding layout (the outer
wrapper element currently using "mx-auto …") to use a full-height flex container
(e.g., "flex flex-col min-h-screen") and change this inner list container to use
flexible growth instead of a calc max-height (e.g., "flex flex-col min-h-0
lg:flex-1 lg:min-h-0") so the list naturally fills remaining space;
alternatively replace the layout with a CSS Grid using grid-template-rows: auto
auto auto 1fr to let the list area occupy the leftover space.

In `@client/src/pages/admin/seo.tsx`:
- Around line 67-70: The effect that sets document.title inside useEffect
currently changes the page title without restoring it; modify the useEffect in
the component to capture the original title (const prev = document.title) before
setting document.title = "SEO 优化 | Monolith" and return a cleanup function that
restores document.title = prev, or replace the manual logic by using the shared
useDocumentTitle hook to set and auto-restore the title; reference the existing
useEffect and loadAll calls when making the change.
- Line 58: The sitemap parsing using smRes.matchAll(/<loc>([^<]+)<\/loc>/g)
doesn't decode XML entities; replace this with DOMParser: parse smRes into a
document, check for parsing errors via doc.querySelector("parsererror") and
handle/report them, then extract URLs by selecting all <loc> elements (e.g.,
doc.querySelectorAll("loc")) and reading each element's textContent to get
decoded URLs before building the urls array; update the code that currently
assigns urls from smRes.matchAll to use this DOMParser-based approach and add
error handling for invalid XML.

In `@server/src/index.ts`:
- Line 373: The SITE_ORIGIN value may include trailing whitespace or a trailing
slash causing double-slashes in outputs; normalize it before use by trimming
whitespace and removing any trailing slash, then fall back to new
URL(c.req.url).origin if empty. Update the code that builds siteUrl (const
siteUrl = c.env.SITE_ORIGIN || new URL(c.req.url).origin) to call a small helper
like normalizeSiteOrigin(value) that returns the trimmed value without a
trailing slash (and returns null/undefined for empty strings) and use that
helper in both places where SITE_ORIGIN is read (the siteUrl assignment and the
robots/sitemap generation code) so all consumers get the canonical origin.

In `@server/wrangler.toml`:
- Around line 9-10: The SITE_ORIGIN value is hardcoded to
"https://monolith-client.pages.dev" which will break sitemap/robots when
switching to a custom domain; update SITE_ORIGIN whenever you bind a custom
domain or change the implementation to read an environment/deployment variable
(e.g., process.env.SITE_ORIGIN or equivalent runtime injection) instead of the
literal, and add a short inline comment by the SITE_ORIGIN declaration reminding
maintainers to sync this value with any custom domain changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b7ce672f-1a80-441f-a800-821fed697590

📥 Commits

Reviewing files that changed from the base of the PR and between 14466fb and fb5076a.

📒 Files selected for processing (9)
  • client/functions/robots.txt.ts
  • client/functions/sitemap.xml.ts
  • client/src/app.tsx
  • client/src/components/admin-layout.tsx
  • client/src/lib/seo-analyzer.ts
  • client/src/pages/admin/dashboard.tsx
  • client/src/pages/admin/seo.tsx
  • server/src/index.ts
  • server/wrangler.toml
📜 Review details
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (4)
client/functions/**

⚙️ CodeRabbit configuration file

client/functions/**: Cloudflare Pages Functions(API 反向代理层)。审查时请关注: 1. 代理目标 URL 是否正确构建 2. 请求头的传递和清理 3. 错误响应处理

Files:

  • client/functions/sitemap.xml.ts
  • client/functions/robots.txt.ts
client/src/components/**

⚙️ CodeRabbit configuration file

client/src/components/**: 这是 React 前端组件目录。审查时请关注: 1. 是否同时兼容暗色和亮色主题(检查 CSS 变量和 data-theme) 2. 响应式布局是否完整(移动端/平板/桌面端) 3. 无障碍访问(aria 标签、键盘导航) 4. 组件是否保持单一职责

Files:

  • client/src/components/admin-layout.tsx
server/src/index.ts

⚙️ CodeRabbit configuration file

server/src/index.ts: Hono Workers API 路由总入口。审查时请关注: 1. JWT 认证中间件是否正确保护管理接口 2. CORS 配置是否安全 3. 请求参数验证

Files:

  • server/src/index.ts
client/src/pages/**

⚙️ CodeRabbit configuration file

client/src/pages/**: 页面级组件。审查时请关注: 1. 数据加载和错误处理是否完善 2. SEO 相关(页面标题、meta 标签) 3. 导航和路由是否正确

Files:

  • client/src/pages/admin/seo.tsx
  • client/src/pages/admin/dashboard.tsx
🔇 Additional comments (3)
client/functions/sitemap.xml.ts (1)

1-23: robots.txt.ts 完全重复,建议抽取共享代理工厂

两个文件代码几乎逐字相同,仅注释中文件名差异。已在 _shared.ts 中沉淀辅助函数,建议进一步把整个 onRequest 抽成工厂:

♻️ DRY 重构示例

client/functions/_shared.ts 添加:

export function createProxyHandler(): PagesFunction<ApiEnv> {
  return async (context) => {
    if (context.request.method !== "GET" && context.request.method !== "HEAD") {
      return new Response("Method Not Allowed", { status: 405, headers: { Allow: "GET, HEAD" } });
    }
    const backend = getBackendUrl(context.env);
    if (!backend) return createPlainApiBaseErrorResponse();
    const target = buildTargetUrl(backend, context.request);
    try {
      const res = await fetch(target, { method: context.request.method });
      return new Response(res.body, { status: res.status, headers: res.headers });
    } catch {
      return new Response("Upstream unavailable", { status: 502, headers: { "Content-Type": "text/plain; charset=utf-8" } });
    }
  };
}

然后两个文件简化为:

import { createProxyHandler } from "./_shared";
export const onRequest = createProxyHandler();

至于 method/headers 透传与类型一致性问题,已在 robots.txt.ts 评论中说明,此处同样适用。

client/src/app.tsx (1)

25-25: 路由与懒加载注册得当

AdminSeolazy 导入与现有 admin 页面写法一致;<Route path="/admin/seo"> 放在 <Route path="/admin"> 之前避免了被通配匹配吞掉。LGTM。

Also applies to: 169-169

client/src/components/admin-layout.tsx (1)

11-11: SEO 入口接入正确

新菜单项复用了现有 navGroups 渲染管线,自动继承双主题样式(text-foreground / bg-muted 等语义变量)和移动端抽屉行为;Sparkles 图标在 lucide-react 标准库内,无需额外配置。LGTM。

Also applies to: 55-55

Comment thread client/functions/robots.txt.ts
Comment thread client/src/lib/seo-analyzer.ts
Comment thread client/src/pages/admin/seo.tsx
Comment thread client/src/pages/admin/seo.tsx
* loadAll 用 Promise.allSettled 隔离 4 个请求(含新增 RSS HEAD 探活),
  每路单独错误回退,新增 error state 与红色错误 banner 提示用户
* globalChecks 改为基于真实 fetch 信号的状态判定:
  - sitemap:urlCount 决定 pass/warn/fail,detail 显示真实 URL 数
  - robots:检测是否包含 Sitemap 指令
  - rss:HEAD 探活
  - 缺信号时显示"未检测"warn,杜绝硬编码 pass 名实不符
* filteredReports:'all' tab 真正显示全部文章(含草稿),
  仅 warn/poor 限定已发布范围,与按钮文案保持一致
* Pages Functions(sitemap.xml.ts/robots.txt.ts)收紧:
  - 仅允许 GET/HEAD,其他 method 直接 405
  - 不透传客户端 headers,避免 Cookie/Authorization 被透传到后端
@one-ea one-ea merged commit abe82be into main Apr 25, 2026
16 checks passed
@github-actions github-actions Bot mentioned this pull request May 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend 后端 (Hono Workers) 相关变更 feature 新功能 frontend 前端 (React/Vite) 相关变更

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant