BDI 博客系统迁移:技术选型、实现方案与取舍总结
详细总结 BDI 官网博客系统从历史项目迁移到 Next.js 16 + Nextra v4 架构的完整过程,涵盖 Frontmatter 标准化、Status Badge 改进、布局优化、CSP 安全加固、zod 输入验证、深度代码审查等三轮持续迭代内容。
前言
BDI 官网是一个面向企业(ToB)的商业数据智能营销网站。在本轮迭代中,我们将历史项目中的博客列表和详情页迁移至全新的技术架构,同时实现中英文双语支持。本文从技术选型开始,逐一剖析每个关键决策背后的对比与权衡,为同类项目的技术选型提供参考。
核心技术栈总览
| 领域 | 最终选型 | 版本 | 备选方案 |
|---|---|---|---|
| 框架 | Next.js | 16.1.6 | Remix, Astro, Nuxt |
| 内容系统 | Nextra | 4.6.1 | ContentLayer, mdx-bundler, next-mdx-remote |
| UI 组件库 | shadcn/ui + Radix UI | 统一包 | Ant Design, Chakra UI, MUI |
| 样式方案 | Tailwind CSS | 4.1.18 | CSS Modules, Styled Components |
| 国际化 | 自建字典方案 | — | next-intl, react-i18next |
| 数据库 | node:sqlite | 内置 | PostgreSQL, MongoDB, Prisma |
| 代码规范 | Biome | 2.3.14 | ESLint + Prettier |
| 运行时 | React | 19.2.4 | — |
| 构建工具 | Turbopack | 内置 | Webpack |
框架选型:为什么是 Next.js 16
备选方案对比
Remix:以 Web 标准为核心,嵌套路由和数据加载模式优秀,但生态规模和企业采用率不如 Next.js,且 SSG(静态生成) 支持相对薄弱。对于 BDI 这类需要兼顾 SEO、SSG 和 SSR 的营销网站,Next.js 的灵活性更强。
Astro:在纯内容站点方面表现卓越,Island Architecture 理念先进。但 BDI 需要的不仅是静态内容,还包括博客评论互动、点赞统计等动态功能,加之后续可能扩展管理后台,Astro 的交互能力受限于其"内容优先"定位。
Nuxt:Vue 生态方案,技术上完全可行,但团队技术栈以 React 为主,切换成本过高。
选择 Next.js 16 的关键理由
- App Router 范式成熟:Server Components + Server Actions 完美契合 ToB 营销站的内容呈现模式
- Turbopack 默认启用:开发服务器启动和 HMR 速度显著提升
- React Compiler 支持:
experimental.reactCompiler: true自动优化组件渲染 - Partial Prerendering (PPR):静态壳 + 动态内容的混合渲染,兼顾首屏速度和 SEO
- Nextra v4 深度集成:作为官方推荐的内容系统,与 App Router 无缝协作
内容系统选型:为什么是 Nextra v4
这是本次迁移最关键的技术决策之一。博客和文档系统需要一个成熟的 MDX 内容管理方案。
备选方案对比
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Nextra v4 | 官方生态,内置主题、搜索、目录、页面导航 | 耦合度较高,自定义需了解其约定 | 文档 + 博客混合站点 |
| ContentLayer | 类型安全,编译时验证 | 已停止维护,与最新 Next.js 兼容性存疑 | 仅博客 |
| mdx-bundler | 灵活,支持运行时编译 | 需要自建全部基础设施(目录、导航、搜索等) | 高度自定义需求 |
| next-mdx-remote | 支持远程 MDX 内容 | 功能单一,需要大量额外工作 | 远程 CMS 内容 |
选择 Nextra v4 的决定性因素
- 双栖能力:同一套 Nextra 配置同时驱动博客和文档两套系统,共享 MDX 组件和渲染管道
getPageMap()API:提供结构化的页面地图,直接获取所有文章的frontMatter,无需手动扫描文件系统importPage()API:按需导入单篇文章,返回{default, metadata, toc}三元组,天然支持目录提取_meta.ts配置:声明式页面排序和标题管理,比文件系统排序更灵活- 主题系统:
nextra-theme-docs开箱即用地提供侧边栏、搜索、暗色模式,显著降低文档系统建设成本
关键取舍
我们没有使用 Nextra 的博客主题(nextra-theme-blog),而是在博客列表和详情页采用完全自定义 UI。原因是:
- 历史项目的博客布局、配色和交互已经经过打磨,用户体验良好
nextra-theme-blog的默认样式与 BDI 品牌调性不匹配- 自定义 UI 配合
getPageMap()和importPage()API 依然高效
但文档系统直接使用了 nextra-theme-docs 主题,因为其侧边栏、搜索、面包屑等功能无需额外开发。
数据库选型:为什么是 node:sqlite
博客系统需要存储两类交互数据:点赞/反对(approvals/deviations)和评论(comments)。
备选方案对比
| 方案 | 优势 | 劣势 |
|---|---|---|
| node:sqlite | 零依赖,Node.js 原生内置,无需额外服务 | 不适合高并发写入,单进程锁 |
| PostgreSQL | 功能全面,适合生产级应用 | 需要额外部署和维护数据库服务 |
| MongoDB | Schema 灵活 | 需要外部服务,对于简单结构过于重量级 |
| Prisma + SQLite | ORM 类型安全 | 多一层抽象,对简单场景过度工程化 |
| 文件系统 JSON | 最简单 | 并发不安全,不可扩展 |
选择 node:sqlite 的理由
- 零基础设施成本:Node.js 22+ 内置
node:sqlite,无需安装任何额外数据库软件 DatabaseSyncAPI:同步 API 在 Server Actions 中使用直觉自然,代码简洁- 适配当前规模:ToB 营销网站的博客互动量有限,SQLite 的性能完全足够
- 可迁移性:SQL 语义标准,后续如需升级至 PostgreSQL 迁移成本极低
- 服务端限定:博客交互全部通过 Server Actions 处理,数据库仅在服务端运行
数据表设计
-- 博客互动记录
CREATE TABLE IF NOT EXISTS blog_interactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('approval', 'deviation')),
created_at TEXT DEFAULT (datetime('now'))
);
-- 博客评论
CREATE TABLE IF NOT EXISTS blog_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL,
author TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);配合 zod 进行输入验证,确保 Server Actions 接收的数据类型安全。
国际化选型:为什么是自建字典方案
备选方案对比
| 方案 | 优势 | 劣势 |
|---|---|---|
| 自建字典方案 | 零依赖,完全可控,与 Nextra + RSC 无冲突 | 缺少复数、日期格式化等高级功能 |
| next-intl | 功能全面,社区活跃 | 与 Nextra v4 的路由系统存在潜在冲突 |
| react-i18next | 成熟生态,插件丰富 | 偏向客户端渲染,与 RSC 模式理念冲突 |
选择自建方案的核心考量
- Nextra 兼容性:Nextra v4 自身管理
[locale]路由段,引入next-intl的中间件和路由逻辑可能产生冲突 - RSC 优先:自建字典方案天然是服务端的 ——
getDictionary(locale)在 Server Component 中直接调用,无需客户端包体 - ToB 内容特点:BDI 官网的文案以专业术语和固定短语为主,不涉及复数形式或复杂的 ICU 消息格式
- 可维护性:两个 JSON 文件(
zh.json/en.json)结构清晰,翻译人员可直接编辑
具体实现
// lib/dictionaries.ts
const dictionaries = {
zh: () => import("@/dictionaries/zh.json").then((m) => m.default),
en: () => import("@/dictionaries/en.json").then((m) => m.default),
};
export async function getDictionary(locale: string) {
const loader = dictionaries[locale as keyof typeof dictionaries];
if (!loader) return dictionaries.zh();
return loader();
}客户端组件通过 DictionaryProvider + React 19 use() API 访问字典:
// 服务端:Layout 中注入 Promise
<DictionaryProvider dictionary={getDictionary(locale)}>
{children}
</DictionaryProvider>
// 客户端:use() 自动 suspend 等待
const dict = useDictionary();内容分语言模式
历史项目使用"同文件双语"模式(在同一 MDX 文件中根据 locale 切换内容),本次迁移改为分目录语言模式:
content/
├── zh/
│ └── blog/
│ ├── _meta.ts
│ ├── asian-cuisine.mdx
│ ├── mdx-security-defense.mdx
│ └── ...
└── en/
└── blog/
├── _meta.ts
├── asian-cuisine.mdx
└── ...
这一模式的优点:
- 编辑独立:中英文内容可由不同翻译人员独立维护,互不阻塞
- 部分翻译:允许某些文章暂时只有中文版本,不影响英文站整体可用性(中文站 5 篇,英文站 2 篇)
- SEO 友好:每个语言版本有独立的 URL 路径(
/zh/blog/xxxvs/en/blog/xxx),搜索引擎可独立索引 - Nextra 原生支持:
getPageMap('/${locale}/blog')按语言获取页面地图,零额外配置
UI 组件库:shadcn/ui + Radix UI 统一包
选择理由
- 非运行时依赖:shadcn/ui 的组件代码直接复制到项目中,不存在版本锁定风险
- Radix UI 原语:底层使用 Radix UI 无障碍(a11y)原语,确保 WAI-ARIA 合规
- 统一包导入:使用
radix-ui统一包替代@radix-ui/react-xxx原子包,减少依赖碎片 - Tailwind CSS 深度集成:
class-variance-authority+tailwind-merge+cn()函数形成一致的样式模式 - 主题通过 CSS 变量:亮色/暗色模式通过 CSS 变量切换,与项目蓝色主调、绿色辅助色无缝配合
实际使用的组件
本次迁移使用了以下 shadcn/ui 组件:
Button:主按钮、互动按钮(点赞/反馈)Card:博客列表卡片Badge:文章分类标签、"最新" 标记Input+Textarea:评论表单NavigationMenu:顶部导航DropdownMenu:主题切换Sheet:移动端导航抽屉Breadcrumb:面包屑导航
博客系统核心实现
博客列表页
通过 Nextra 的 getPageMap() API 获取当前语言的全部博客文章:
const pageMap = await getPageMap(`/${locale}/blog`);
const posts = pageMap
.filter((item): item is MdxFile => "name" in item && "frontMatter" in item)
.sort((a, b) => {
const dateA = a.frontMatter?.date || "";
const dateB = b.frontMatter?.date || "";
return dateB.localeCompare(dateA);
});列表 UI 保留了历史项目的双列卡片布局,每张卡片包含:封面图、分类标签、标题、摘要、作者信息、发布时间和互动数据。
博客详情页
通过 importPage() 动态导入单篇文章内容:
const { default: MDXContent, metadata, toc } = await importPage(`${locale}/blog/${slug}`);详情页包含:
- 文章头部:标题、分类标签、发布时间、作者卡片
- 封面图:
next/image自动优化,配合remotePatterns白名单 - MDX 内容:通过自定义组件系统渲染(支持 Callout、MDXTabs、Icon 等)
- 互动区域:点赞/反对按钮(Server Actions 处理)
- 评论系统:表单 + 评论列表(Server Actions + node:sqlite)
- 目录导航:从
toc提取,固定在侧边栏
JSON-LD 结构化数据
每篇博客文章自动生成 BlogPosting 类型的 JSON-LD:
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: metadata?.title,
datePublished: frontMatter.date,
dateModified: frontMatter.date,
inLanguage: locale === "zh" ? "zh-CN" : "en-US",
author: {
"@type": "Person",
name: frontMatter.authorName || dict.blog.defaults.authorName,
},
publisher: {
"@type": "Organization",
name: dict.Metadata.siteName,
logo: { "@type": "ImageObject", url: `${baseUrl}/logo.png` },
},
};代码质量保障
Biome 替代 ESLint + Prettier
| 维度 | ESLint + Prettier | Biome |
|---|---|---|
| 配置复杂度 | 多个配置文件,插件依赖关系复杂 | 单一 biome.json |
| 执行速度 | 较慢(Node.js 运行时) | 极快(Rust 编写) |
| Lint + Format | 需要两个工具各自配置 | 统一内置 |
| 本次耗时 | — | 63 个文件 746ms |
深度代码审查修复清单
本次迁移后进行了全面代码审查,修复项包括:
- 消除硬编码文本(19 处)→ 全部迁移至 i18n 字典
- 配置
images.remotePatterns→ 安全域名白名单替代unoptimized - 内联样式转 Tailwind → 16 处
style={{ maxWidth: "80rem" }}统一为max-w-[80rem] - 增强 JSON-LD → 补充
dateModified、inLanguage、修正 publisher logo - 添加
revalidatePath→ 评论提交后自动刷新页面缓存 - 补齐 SEO 基础设施 →
robots.ts+sitemap.ts - 全局错误边界 →
global-error.tsx优雅处理未捕获异常 - 防御性参数检查 → 所有
generateMetadata函数兼容 Nextra 内部路由解析
迁移过程中的关键挑战
挑战一:Nextra 内部路由与 generateMetadata 冲突
Nextra v4 内部路由解析会调用页面的 generateMetadata 函数,但传入的 params 可能不包含预期的 locale 字段。这导致构建时 TypeError: Cannot destructure property 'locale' of ...。
解决方案:在所有 generateMetadata 中添加防御性检查:
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const resolvedParams = await params;
if (!resolvedParams?.locale) return {};
// ... 正常逻辑
}挑战二:Turbopack 序列化限制与 rehype 插件
Turbopack 要求 MDX 插件配置为可序列化值。JavaScript 函数无法被序列化,因此自定义 rehype 插件必须以 CJS 格式编写,并通过绝对路径字符串引用。
挑战三:MDX 组件简写语法
MDX 作者可能使用简化形式编写 Tab 内容,如 <div value="tab1">内容</div>。我们增强了 MDXTabs 组件,使其能够自动检测子元素的 value 和 label prop,在没有显式 MDXTabsList 的情况下自动生成 Tab 切换 UI。
挑战四:sitemap.ts 类型谓词
Nextra 的 getPageMap() 返回 PageMapItem[] 联合类型。在 TypeScript strict 模式下,类型谓词 (item): item is { name: string } 不可赋值给 PageMapItem。最终使用 Nextra 导出的 MdxFile 类型替代:
import type { MdxFile } from "nextra";
const posts = pageMap.filter((item): item is MdxFile => "name" in item && "frontMatter" in item);构建与验证结果
最终构建产出 23 个页面,涵盖:
- 中英文首页(2 页)
- 中英文博客列表(2 页)
- 中英文博客文章(7 篇)
- 中英文服务页(2 页)
- 中英文文档(含子页面,6 页)
robots.txt+sitemap.xml(2 页)- 根路由重定向(1 页)
- 404 页面(1 页)
浏览器测试覆盖 9 条核心路由,全部 0 console 错误。
后续迭代:Frontmatter 标准化与全面优化
在初始迁移完成后,我们对博客系统进行了第二轮深度迭代,重点解决字段规范化、UI 体验和代码质量问题。
一、Frontmatter 字段标准化
将所有 MDX 文件的前言字段经历了两轮标准化:
第一轮(合并字段):
| 旧字段 | 中间字段 | 说明 |
|---|---|---|
badgeLabel: "标签" | tags: [标签] | 改为数组格式 |
publishDate + publishTime | dateTime: "YYYY-MM-DD HH:mm:ss +0800" | 合并为单个字段,含时区信息 |
imageSrc | cover | 语义更清晰 |
第二轮(终版标准):
| 中间字段 | 最终字段 | 说明 |
|---|---|---|
tags: [标签] | category: "标签" | 改为字符串,更简洁 |
dateTime: "..." | date: "YYYY-MM-DD HH:mm:ss +0800" | 字段名更通用 |
工具函数 formatBlogDate 和 parseDateTimeString 同步重构,接受单个 date 参数,解析 +0800 时区格式。
二、Status Badge("NEW"徽章)改进
问题:原实现使用 bg-primary 颜色,在亮色主题下显示为黑色,暗色主题下显示为白色,均不适合"新内容"的语义。判定阈值为 48 小时过长。
修复:
- 颜色改为
bg-emerald-500 dark:bg-emerald-600,绿色在双主题下均清晰可辨 - 指示点从
h-2 w-2增大至h-2.5 w-2.5(列表页)和h-3 w-3(详情页),动画更明显 - 阈值从 48 小时缩短至 24 小时,基于
dateTime字段计算
三、博客详情页"返回列表"链接
在文章顶部添加 ← 返回博客列表 链接(使用 lucide-react 的 ArrowLeft 图标),参考 Nextra 官方的简洁导航风格。中英文文案通过 i18n 字典管理。
四、布局间距调整
| 页面 | 问题 | 修复 |
|---|---|---|
| 博客列表页 | max-w-7xl4 拼写错误导致无最大宽度约束 | 修正为 max-w-7xl,增加 px-6 sm:px-8 lg:px-12 内边距 |
| 博客详情页 | px-4 在小屏幕过于贴边 | 改为 px-6 sm:px-8 lg:px-12 |
| 首页/服务页 | max-w-7xl(1280px)在大屏幕两侧留白过多 | 全部改为 max-w-screen-2xl(1536px) |
五、深度代码审查修复
对项目全部活跃代码进行逐行审查,共发现并修复 10+ 项问题:
- 评论组件:错误消息固定显示绿色 → 根据状态显示红/绿色
- localStorage 操作:未做异常处理 → 添加
try/catch - toggleReaction:缺少
revalidatePath调用 → 补齐 - window.pageYOffset:已弃用 → 替换为
window.scrollY - Loading 骨架屏:暗色模式不可见 → 添加
dark:bg-slate-800 - OG images alt:可能为 undefined → 添加
?? slug回退 - 全局错误页:纯英文 → 改为中英双语
- 404 页面:未使用字典标题 → 补充
dict.NotFound.title - Demo 按钮:无行为 → 链接到服务页
- hreflang 标签:使用相对路径 → 改为绝对 URL
第三轮迭代:视觉统一、安全加固与深度审查
在前两轮迭代的基础上,我们进行了第三轮全面强化,聚焦三个维度:跨屏视觉一致性、Web 安全防护基线和代码质量深度审查。
一、全站容器间距统一
问题:导航栏、页脚、首页各 Section、Services 页面 5 个区块、博客列表/详情页的内边距各不相同(px-4 md:px-6、px-6 sm:px-8 lg:px-12、px-4等),在 14 寸笔记本与 24 寸外接显示器之间切换时,视觉节奏不连贯。
统一方案:全站采用 container mx-auto max-w-screen-2xl px-4 sm:px-6 lg:px-8 2xl:px-12 模式,涵盖:
| 组件/页面 | 修改数 |
|---|---|
| site-header / site-footer | 2 |
| hero / service-overview / platform / value / characteristics / philosophy | 6 |
| services 页面 5 大区块 | 5 |
| blog 列表页 / blog 详情页 | 2 |
额外修复:
- ServiceOverviewSection 移除
max-w-350过紧约束 - blog 卡片边框从
border-slate-100增强为border-slate-200(暗色dark:border-slate-700),提升可辨识度 - 导航栏底部边框改用
border-border/40,减少视觉压迫感 - 代码块增加
prose-pre:overflow-x-auto和全局 CSS 层处理pre溢出
二、安全加固(CSP + 安全头 + 输入验证)
依据 Next.js 官方 Content Security Policy 和 Data Security 指南,实施三层安全防护。
CSP 策略(Without Nonces 方案)
选择"Without Nonces"方案的原因:本站采用 SSG(全页面 generateStaticParams),如使用 Nonce 方案会强制所有页面切换为动态渲染,导致 CDN 缓存失效、首屏性能下降、并与 Turbopack 不兼容(SRI 仅限 Webpack)。
// next.config.ts - CSP 主要指令
("default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // dev 需要 unsafe-eval
"style-src 'self' 'unsafe-inline'",
"img-src 'self' blob: data: https://picsum.photos https://fastly.picsum.photos https://i.pravatar.cc",
"object-src 'none'",
"form-action 'self'",
"frame-ancestors 'none'",
"upgrade-insecure-requests"); // 仅生产环境安全响应头
| Header | Value | 作用 |
|---|---|---|
| X-Content-Type-Options | nosniff | 防止 MIME 嗅探 |
| X-Frame-Options | DENY | 防止点击劫持 |
| X-XSS-Protection | 1; mode=block | IE 旧版 XSS 过滤 |
| Referrer-Policy | strict-origin-when-cross-origin | 控制来源信息泄露 |
| Permissions-Policy | camera=(), microphone=(), geolocation=(), payment=() | 禁用不必要权限 |
| Strict-Transport-Security | max-age=63072000; includeSubDomains; preload | 强制 HTTPS |
同时设置 poweredByHeader: false 隐藏 X-Powered-By: Next.js 指纹。
输入验证(zod v4)
所有 Server Actions 增加 zod schema 验证:
const slugSchema = z
.string()
.min(1)
.max(200)
.regex(/^[\w-]+$/);
const localeSchema = z.enum(["zh", "en"]);
const commentSchema = z.object({
author: z
.string()
.min(1)
.max(50)
.transform((v) => v.replace(/<[^>]*>/g, "")),
content: z
.string()
.min(1)
.max(2000)
.transform((v) => v.replace(/<[^>]*>/g, "")),
});所有函数使用 safeParse 校验输入,失败时返回结构化错误而非抛出异常。
三、第三轮代码审查修复
通过子代理(subagent)对全部 64 个活跃文件进行逐行审查,共发现 23 项问题,修复关键项包括:
- global-error.tsx:补充
<head>元素(charset/viewport/title),移除无 locale 上下文的双语硬编码 - icon.tsx:将
import * as LucideIcons替换为显式白名单 Map(约 20 个常用图标),避免破坏 tree-shaking - callout.tsx:移除不必要的
"use client"指令(纯展示组件无需客户端声明) - language-switcher.tsx:Cookie 增加
SameSite=Lax属性,防止 CSRF - table-of-contents.tsx:
<nav>添加aria-label提升无障碍可访问性 - mdx-tabs.tsx:补充
role="tablist"、role="tab"、aria-selected、role="tabpanel"等 WAI-ARIA 属性 - docs page:将硬编码
["zh", "en"]替换为i18n.locales配置引用 - interaction-group.tsx:localStorage 值增加显式字符串比较验证
四、构建与验证
最终构建产出 25 个静态页面(较上一轮增加 2 页),全站 pnpm build 通过。
浏览器自动化测试覆盖 7 条核心路由:
/(中文首页)、/services、/blog、/blog/[slug](详情页)/docs、/en(英文首页)、/en/blog
所有页面 0 控制台错误,CSP 及安全头全部生效。
总结
本次迁移的核心理念是 "在约束中寻找最优解":
- 框架约束:Next.js 16 + Turbopack 决定了内容处理必须兼容其序列化要求
- 内容约束:Nextra v4 提供了强大的基础设施,但需要理解其约定来实现自定义
- 规模约束:ToB 营销网站的交互量决定了 node:sqlite 足以应对,无需过度工程化
- 团队约束:自建 i18n 字典方案虽功能有限,但与 Nextra + RSC 零冲突且易于维护
- 安全约束:SSG 架构决定了 CSP 采用 Without Nonces 方案,配合 zod 输入验证实现数据安全
每一项技术选型都不存在绝对的"最优"。关键在于清晰地识别项目的核心需求和边界条件,在备选方案中找到最匹配的组合。BDI 官网的技术栈在当前阶段实现了功能完整、性能优秀、安全可靠、维护成本低的平衡。随着业务发展,部分选型(如数据库、国际化方案)可以平滑升级,而不影响整体架构。
第四轮迭代:Frontmatter 终版标准化与极限测试修复
在前三轮迭代的基础上,进行了第四轮质量强化,聚焦 Frontmatter 字段终版标准化和极限边界测试博客的渲染修复。
一、Frontmatter 终版字段重命名
将所有 10 篇 MDX 博客文件的字段统一为最终规范:
| 上一轮字段 | 最终字段 | 说明 |
|---|---|---|
dateTime: "..." | date: "YYYY-MM-DD HH:mm:ss +0800" | 字段名更通用,保留时区格式 |
tags: [标签] | category: "标签" | 单分类字符串,非数组 |
同步更新了 app/[locale]/blog/page.tsx 和 app/[locale]/blog/[slug]/page.tsx 中的所有字段引用。
二、极限测试博客修复
对 do-not-delete-or-modify.mdx 极限测试文章中的 6 个渲染问题进行了修复:
| 问题 | 原因 | 修复方案 |
|---|---|---|
| 脚注跳转偏移 | sticky header 遮挡脚注目标 | globals.css 添加 scroll-margin-top: 6rem 规则 |
| 零宽字符未解析 | MDX 不处理 JS 转义序列 \u200B | 替换为 HTML 实体 ​ 等 |
| 引用块单行 | 相邻 > 行为同一段落 | 插入空白 > 行分隔两段落 |
| Icon 不显示 | CheckCircle、X 未在白名单 | icon.tsx 添加 CircleCheck、X 映射 |
| RTL 文本渲染 | 缺少 dir 属性 | 添加 <bdi dir="rtl"> 包裹希伯来文 |
| XSS 弹框 | <script> 在 SSR HTML 中执行 | 以行内代码格式转义危险标签 |
三、MDX 安全强化
在 mdx-components.tsx 中添加了 iframe 和 script 的安全覆盖,作为运行时的纵深防御层。同时创建了 lib/rehype-sanitize-mdx.ts rehype 插件,用于非 Turbopack 构建时的 AST 级别安全清洗(Turbopack 不支持函数类 rehype 插件)。
四、构建与验证
pnpm check:65 个文件通过 Biome 检查pnpm build:TypeScript 编译通过,25/25 静态页面成功生成- 浏览器测试:中英文博客列表及详情页 0 控制台错误,XSS 弹框已消除
- 版本: 4.0.0
- 时间: 2026-02-09 20:12:27
- 作者: Claude Opus 4.6
- 简介: 新增第四轮迭代内容:Frontmatter 终版标准化(dateTime→date、tags→category)、极限测试博客 6 项渲染修复、MDX 安全强化。