document-publishing-architecture

文档发布架构

youteacher_analyze 是一个基于 Next.js App Router 的站点,它的全部职责就是通过一套共享外壳发布三份商业文档——商业计划书(Business Plan)、营销计划(Marketing Plan)、技术手册(Tech Playbook)。这里没有 CMS,也没有为每份文档单写一套布局代码。每份文档不过是一串有序的 React 章节组件,交给同一个 DocumentLayout

一套外壳,三份文档

根路由只负责转发访客。src/app/page.tsx 只有两行:redirect('/businessplan'),所以 / 永远落到商业计划书这个默认入口。三条真实路由就是 App Router 里的三个目录 businessplan/marketingplan/techplaybook/,每个都是一个 page.tsx,返回形状完全一致的东西:一个 <DocumentLayout>,带 currentDoctitlesubtitle 三个 prop,把该文档的章节作为 children 包起来。

三个页面之间唯一不同的,只是 prop 的取值和里面的章节组件清单。商业计划书传 currentDoc="business-plan" 和九个章节(从 Executive Summary 到 Appendix)。营销计划传 currentDoc="marketing-plan" 和十四个章节。技术手册传 currentDoc="tech-playbook" 和五个(PRD、Architecture、Agile Sprints、Quality Assurance、Deployment)。currentDoc 这个 prop 是一个固定的联合类型,取值恰好是这三个字符串字面量。

DocumentLayout 组装了什么

DocumentLayout 是一个 client 组件,它围绕拿到的章节把页面框架搭起来。它按顺序渲染:

  • 页面顶部附近一个固定定位的 DocumentSelector,并告诉它当前是哪份文档;
  • 一个 header 区块,显示文档的 titlesubtitle,后面接一个静态的 meta-info 面板(项目概述、团队、日期、状态)——这块信息写死在布局里,不是每份文档单独传进来的;
  • 一个页面主体,把 TableOfContents 侧栏放在 content 区域旁边,并把章节 children 落进这个 content 区域。

因为框架完全活在布局里,给某份文档加一章,只需要在那份 page.tsx 里加一行 import 和一行 JSX,别的地方都不用知道。

内容原语:Chapter 与 Section

文档由两个极小的结构组件搭成。Chapter 渲染一个带该章 id、类名 chapterdata-type="chapter" 标记的 <div>,并在 children 上方放一个 <h2> 标题。Section 渲染一个 <section>,带可选的 id 和可选的 <h3> 标题,标题在 children 上方。两者都刻意做得很薄——它们承载的是结构与身份,而不是内容样式。正是这一点让目录无需任何手工维护的索引就能工作(见下)。

目录由 DOM 生成,而非清单

TableOfContents 是最巧的一块。它不维护任何手写的章节列表。挂载后它稍等 DOM 稳定,然后遍历每一个 .chapter 元素,把它的 <h2> 读成章标题、把其中嵌套的 <section> 里的 <h3> 读成子节,再据此渲染出的实际内容构建出目录树。这正是 ChapterSection 要费事发出稳定 id 和标题标签的原因——目录是已渲染文档的一个投影,所以在 page.tsx 里加的一章会自动出现在侧栏里,不存在第二处要同步更新。

它还跑一个 scroll-spy:测量每个目标的绝对偏移,随读者滚动追踪当前所在的小节,高亮对应链接,并在某章(或其某个子节)处于激活态时展开该章。点击链接会带一个固定顶部偏移平滑滚动到目标。滚动处理通过 requestAnimationFrame 做了节流,让这个 spy 保持轻量。

在文档之间切换

DocumentSelector 负责跨文档导航。它持有一份很小的静态清单,把每份文档映射到它的标签、图标和路由路径,标出当前项,选中时调用 Next.js router 把选中的路径 push 进去。于是这三份文档用起来像是同一份出版物的几个标签页,尽管每一份都是独立的 App Router 路由。

为什么是这个形状

这个设计用一个非常小、非常好读的系统,换掉了一个通用内容系统:一份文档就是一串有序的组件,框架是一套共享布局,而导航(无论是文档内的大纲还是跨文档的切换器)都自己推导出来,而不是靠手工维护。代价是内容活在代码里(每一章都是一个 React 组件),而非活在数据里——对三份精心编排的规划文档来说这没问题,也正因如此,这里没有编辑器、没有数据库、没有为每份文档重复的样板代码。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →