实现与验收:静态文档站的引用阅读器
比较静态预览页、客户端 MDX 和摘要路线,核对发布过滤、链接、ID、焦点与历史边界,给出实施顺序和可验证条件。
核查于
推荐路线与确认边界
整站外壳按新的视觉定稿设计,并接入统一引用阅读器:桌面使用非模态面板,窄屏使用模态覆盖阅读。全文内容优先验证“共享服务端正文组件+静态轻量预览路由+同源 iframe”。它能延续当前 MDX 编译、相对链接解析和发布过滤,并隔离两篇文章的标题、脚注及布局 ID。
视觉方向先通过完整设计稿确定,技术路线随后做最小验证。这是代码与资料支持的第一候选。本次未构建预览原型或运行浏览器,焦点、资源成本和跨文档交互仍需验证。若这些边界在代表内容中表现不合适,再采用已经存在的客户端 MDX 按需加载能力,并补齐公开清单与正文隔离。
成熟能力与本站需要组装的部分
| 能力 | 已核查的可用基础 | 本站新增职责 |
|---|---|---|
| 亮暗模式 | RootProvider、next-themes | 风格与阅读偏好接线 |
| 布局 | DocsLayout / DocsPage 的 slots 与选项 | 面板打开后的网格、TOC 与窄屏分配 |
| 对话区域 | Base UI Dialog、Popover | 持续阅读、加载状态和目标切换 |
| 编译正文 | page.data.body、MDX 注册表 | 复用正文渲染,去除多余整页导航外壳 |
| 发布范围 | 过滤后的 source | 所有预览入口、路由和清单读取同一集合 |
| 相对链接 | 服务端 createRelativeLink 和 source.resolveHref | 预览目标与正式目标的区分 |
| 浏览器 MDX | Fumadocs MDX Browser Entry | 若采用,限定公开 import 集合并隔离 DOM |
本次检查官方组件与布局文档、本地安装包,未发现包含“获取另一篇正文、管理并排阅读与返回状态”的完整成品。Fumadocs 和 Base UI 提供构建所需的基础能力,引用阅读器由本站组装。Docs Layout、Base UI Dialog
桌面可采用 Dialog modal={false},并使用 disablePointerDismissal,防止点击原文或把焦点移出面板后关闭。窄屏使用模态焦点管理。已有 src/components/ui/dialog.tsx 的 DialogContent 固定组装遮罩和居中 Popup,不能仅传 modal={false} 就形成适合并排阅读的外壳;应使用相同 primitive 组装专用 ReaderPanel。Dialog API
静态导出约束
next.config.ts 采用 output: 'export'。内容页面通过 generateStaticParams 构建,source 在生产输入阶段过滤草稿。新增正文预览仍可在构建期生成 HTML、CSS 与 JS,不要求新增在线后端。
Next.js 官方 Static Exports 明确列出 Intercepting Routes 为不支持功能。常见用拦截路由打开 modal 的示例不能直接用于当前部署方式。本次同时核对本地 Next 文档中的对应限制。Static Exports
全文渲染候选比较
| 路线 | 可以提供什么 | 主要实现代价 | 结论 |
|---|---|---|---|
| 静态预览路由+同源 iframe | 服务端 MDX、当前链接解析、真实交互组件、独立 document | 焦点、主题、内部导航、历史与弹层范围 | 第一候选,先做代表内容验证 |
| 客户端 MDX 按需 import | 真实组件、同一 document 与主题上下文 | 公开 import 集合、服务端链接迁移、ID 与观察器隔离 | 第二候选,有成熟 loader 但接线工作明显 |
| 构建期摘要或章节摘录 | 简介、脚注、局部文本 | 裁剪完整语义、脚注与 MDX 组件的规则 | 用于轻量预览,不能独立满足完整阅读 |
| 抓取正式页 HTML 后插入 | 静态标记 | React 交互、模块初始化、ID 和链接适配 | 不作为本站全文方案 |
| 每页预先带全站正文 | 打开时减少单次获取 | 首屏载荷、重复内容和公开边界 | 不采用 |
不同路线均使用原始正文派生,不增加人工维护的第二份预览文章。自动生成的 HTML、摘要或 loader 清单是产物,后续可随正文重新生成。
第一候选怎样工作
正文、预览路由与公开集合
从文档页抽出共用的服务端 ArticleContent,把 page.data.body、相对链接解析、Card 解析和 MDX 注册表保留在这一边界。正式页组装完整导航与元信息;预览路由只组装阅读必需的正文和 provider。
预览路由通过同一 source.getPages() 生成静态参数。生产只生成公开页面及公开祖先下的内容;开发按现有规则显示草稿。新增预览路径需要纳入保留路由名,避免与作者目录冲突。其 metadata 指向正式页 canonical、设置 noindex,并不参与正文导航、搜索和 sitemap。
面板按点击时创建 iframe,一次显示一个目标。面板外壳负责标题、“打开全文”、关闭和加载反馈,iframe 内负责正文,避免重复标题和整套工具。重用现有样式与必要 provider;主题初始化、代码复制、Tabs、图片和图示依赖不能省略后宣称完整能力保留。
两个文档之间的协议
iframe 有独立的 document 和浏览上下文。标题、脚注和组件 ID 得到隔离,但父页面 CSS 不会自然控制子文档;焦点、事件和状态需要明确协同。iframe 本身也增加内存与运行开销。MDN iframe
建议定义有限的通信职责:子页报告已就绪、加载异常和未被内部弹层消费的关闭请求;父页传递当前偏好与目标;如果支持引用链,子页请求在同一面板继续查看。消息携带当前目标或请求标识,父页拒绝过时结果,并核对精确 origin、发送窗口和允许的站内目标。MDN postMessage
主题使用共享偏好来源。当前 next-themes 有同源持久化基础,新增风格、正文设置与动效开关还需初始化和同步;必须检查首次绘制及面板已打开时的改变。动效同时覆盖 CSS 与图示的 JavaScript,关闭收尾见动效设计。子页键盘事件不自动冒泡到父文档,Esc 传递应尊重内部图片或 Popover 的关闭层级。
iframe 的 load 不能独立证明目标加载成功,浏览器即使加载失败也可能触发该事件。使用子页 ready/error 握手、超时和正式链接回退;快速更换目标时,旧子页的消息不能把新目标标成成功。MDN iframe 的事件说明
正式地址、锚点与历史
同页标题和脚注在子文档中定位。首轮跨页链接指向正式地址,指定 target="_top" 或等效的父级正式导航,避免完整 Docs 页面被打开在预览 iframe 中;Ctrl/Cmd、中键和右键新标签继续沿用真实链接行为。后续若增加面板内预览入口,再向父页面请求替换引用,并同步提供局部返回。首次定位需要考虑字体、图示和图片加载造成的位移,并避免后续内容更新反复抢走读者位置。
外部链接首轮同样按真实 href 在主窗口正式打开,由读者选择浏览器新标签操作;标明新标签的操作再使用 _blank。目标窗口按链接用途逐项处理,不全局使用 <base target="_top">,以免把同页标题和脚注也带出预览。
默认 Heading 的复制锚点读取 window.location.href,在预览路径中会得到预览 URL。需要通过 Heading 适配将复制结果指回正式 canonical URL 加原始 hash。作者的相对页面链接继续以目标文章为解析上下文,不能根据主文或预览路由路径计算。
iframe 导航会影响页面会话历史。实现时需要明确首次创建、切换目标和同页定位的历史规则,尽量采用替换式更新,避免每次预览增加浏览器 Back 步骤。必须验收“切换数个引用—关闭面板—浏览器后退”的行为;仅保持顶层地址栏不变不足以证明历史不变。MDN iframe 浏览上下文说明
弹层与焦点的实际边界
图片放大与 Twoslash 等 Portal 默认位于子文档,只能覆盖引用窗口的范围。首轮应明示并检查该表现;如果引用图片必须占满顶层屏幕,则单独桥接图片查看器。不能把完整组件渲染等同于所有弹层自动覆盖主页面。
iframe 设置具体文章名的 title。父面板只用短标题描述阅读区域,长篇正文不整段放进 aria-describedby。桌面焦点能进出面板和主文,窄屏模态焦点必须包含子文档;关闭时返回有效触发点。自动化检查之外,还需要真实键盘与辅助技术核查。WAI-ARIA Dialog Modal Pattern
客户端全文为什么保留为候选
Fumadocs MDX 提供 Browser Entry,使用异步 import 和 createClientLoader 加载编译后的 MDX,具有预加载、内容读取和组件获取能力。本地 15.4.6 的生成文件与运行时已确认这些接口。Browser Entry
但当前 apps/site/.source/browser.ts 的 import 清单覆盖整个 content,包含草稿;src/lib/source.ts 的过滤发生在服务端 source。把 collections/browser 直接引入公开客户端后,不能仅凭点击前校验目标便证明草稿没有进入 JS chunks。需要生成只包含同一公开集合的 import 清单,或核查适用的官方编译过滤入口,并检查实际产物。
这属于新增路线的构建风险,本次没有发现当前站点已经通过客户端 loader 输出草稿的证据。后续清单必须由正文公开集合派生,不能人工列维护两套页面。
另一个边界是 createRelativeLink 为服务端接口。本地浏览器实现直接抛出只支持 Node 环境的错误。客户端全文需在构建期派生已解析 href 或映射,并覆盖 Markdown 链接、Card 与静态 MDX href;query 和 hash 继续保留。
同一 document 内的双正文还需处理:
- 标题、脚注、作者 HTML id 以及对应 hash、ARIA 引用的一致隔离。
- Fumadocs TOC 当前通过
document.getElementById查标题,其观察与滚动逻辑不能直接当作两份正文的隔离方案。 - 面板内锚点定位到指定滚动容器,避免滚动主页面。
- 避免嵌入第二个完整 DocsPage 后重复固定布局 ID;使用独立 ReaderBody。
- Mermaid、图片放大和类型弹层的上下文、事件和所在区域。
客户端方案能共享主题和减少跨文档通信。若原型证实其公开清单、编译链接和正文隔离能以更小维护成本解决,可以调整最终选择。当前保留两条候选是为了明确未验问题,交互层的目标与操作规则保持一致。
状态与改动位置
阅读器只保存无法直接推导的状态:当前预览目标及锚点、有效触发元素、加载请求结果,必要时保存面板局部阅读位置和引用历史。开关可由目标是否存在派生;宽屏与窄屏呈现由可用空间派生;页面标题和正式 URL 来自 source 派生的只读资料。
| 位置 | 后续实现职责 |
|---|---|
| 文档布局 | 挂载统一阅读器,分配右侧 TOC 与面板空间 |
| 文档页面 | 共用正文组装,传递正式地址与最小预览资料 |
page-relations.tsx | 改为紧凑条目,标题链接和预览按钮相邻 |
| MDX 链接适配 | 在正文站内引用提供预览入口,保留真实 href |
| 新增轻量预览路由 | 静态参数、草稿过滤、canonical、noindex、ReaderBody |
| 新增 ReaderPanel | 非模态/模态呈现、焦点、加载和目标替换 |
tokens.css / theme.css / prose.css | 阅读专属值、面板网格与风格变量 |
content-index.ts | 若需要引用出现位置,从已有 AST 派生;现有页级索引无需人工补字段 |
首轮可只用现有页级关系接入面板。正文锚点和引用位置增强再沿原链接与 AST 补充,保持内容作者继续维护一份正文。清单、预览路由与索引都是生成结果。
实施与验收清单
先建立一篇代表内容,包含相对链接、中文标题锚点、脚注、代码、公式、Mermaid、图片放大和 Tabs,同时设置草稿页面与草稿祖先作为排除样本。先验证静态编译、公开集合、正式链接和路由;随后进行已授权的浏览器验收。技术试验遵循本站共用验证环境与授权约定。
| 类别 | 可验证条件 |
|---|---|
| 内容边界 | 正式页和预览来自相同 source;公开路由、资源或客户端清单不意外包含被排除正文 |
| 链接 | 解析上下文正确,hash/query 保留,正式打开和复制锚点指向原页面 |
| 完整 MDX | 代码复制、Tabs、公式、Mermaid、图片与类型提示在预览中按定义运行 |
| 异步 | 快速切换、404、超时和晚到消息不会覆盖当前目标;正式打开始终可用 |
| 空间与滚动 | 主文位置保留,两侧分别滚动,宽内容局部溢出;缩放与窄屏正确降级 |
| 焦点 | 桌面可操作两边;窄屏模态可关闭并返回;Esc 尊重内部弹层;焦点不被遮挡 |
| 历史 | 预览开关与切换不制造非预期 Back 步骤,正式导航后状态符合约定 |
| 主题 | 风格、明暗、大字和系统变化在两边一致 |
| 动效 | 开关保存并同步两边;中途关闭、连续操作后无残留动画、遮罩或滚动锁,焦点正常 |
| 性能 | 打开前不载入整站正文;记录打开后的额外请求、JS、内存与首次可操作延迟 |
| 阅读效果 | 同一核查任务比较正确性、往返、位置迷失和主观舒适度 |
性能需给出实际环境和数据,首轮不编造延迟或内存目标。若 iframe 的成本或双文档交互明显不适合,回到客户端候选做相同任务比较;不能仅凭抽象优缺点宣布性能更优。
本次只完成调研与草稿,没有进行以上原型、浏览器、屏幕阅读器或真实读者验证。正文格式检查证明文章符合本站技术接口,其结果不证明引用阅读功能已经实现或用户效果已经成立。
最后更新于