研究证据阅读器
在 youteacher_analyze 这个应用里,有一层很小的证据浏览层:一组 /data/* 路由,每条路由从磁盘上取一份抓取来的研究材料,交给一个懂得如何显示那种数据形状的 viewer 组件。目的,是让商业文档背后的原始材料——论坛帖子、对比分析、结构化报告——能在浏览器里直接读,而前面不必架一个数据库或 API。
从服务端读文件
一条 data 路由就是一个普通的 Next.js 服务端页面。它用 path.join(process.cwd(), ...) 拼出绝对路径,用 fs.readFileSync 同步读文件,再把结果作为 props 往下传。Reddit 证据那条路由从 public/data/ 读一份 JSON,JSON.parse 成一个帖子数组,交给 ForumPostsViewer 渲染;平台对比那条路由从 public/analysis/ 把一份 Markdown 当字符串读进来,交给 AnalysisReportViewer 渲染。两条路由都把 viewer 包在一层很窄的页面外壳里,外壳上唯一的装饰就是一个 "← Back to Business Plan" 的返回首页链接。所以这里的文件系统本身就是数据源:要发布一份新证据,就把文件丢进 public/,再让一条路由指向它。
两条读取路径,是刻意的
这一层其实有两种读取策略,而且是有意共存的。服务端路由在服务端渲染时就把文件读掉(fs.readFileSync)——访客的浏览器从不亲自去取那份文件。另一条路径是 DataViewer:一个可折叠的客户端控件,点击之前什么都不做,点了之后才按需 fetch 一个 URL,然后要么把它(作为 JSON)美化打印,要么把它(作为 Markdown)原样塞进一个可滚动的 <pre> 里。服务端路由用于承载一整页的证据;客户端 DataViewer 则用于那些你只想就地瞄一眼的证据,惰性加载,没人打开就不花代价。
专用 viewer 与通用兜底
viewer 组件也沿着同一条线分开。ForumPostsViewer 是为一种数据量身定做的:它接收一个论坛帖子数组,提供一个客户端搜索框(在标题、content、body、作者之间过滤),每二十条分一页,并把每条帖子渲染成一张卡片——序号徽章、作者、日期、标题、截断后的正文(在 500 字符处截断)、赞数与回复数、以及一个指向原帖的链接。它的 ForumPost 类型把每个字段都标成可选,还带一个索引签名,于是从不同来源抓来、形状各异的帖子都能渲染而不崩。AnalysisReportViewer 则是为分析报告定做的:它按 ## 标题把 Markdown 切成若干小节,再按关键词给每节打标签——"pain point""evidence"/"quote""frequency"/"severity"——给痛点小节配上红色边框、给每种类型配上各自的图标,然后用 react-markdown 加 remark-gfm 渲染正文。
在这两者之下,是通用兜底:JsonReader 渲染任意 JSON(数组变成一串编号条目,对象变成带标签的键/值行,嵌套对象则被字符串化塞进 <pre>),MarkdownReader 渲染任意 Markdown。专用 viewer 让某一份数据既清楚又好看;通用的这两个则让任何数据至少看得见。
两个 Markdown 渲染器
之所以值得一提,是因为这一层里确实存在一处不一致:Markdown 有两种渲染方式。AnalysisReportViewer 用的是 react-markdown 库,带 GitHub 风格 Markdown 支持以及逐元素的样式覆写;MarkdownReader 则跑一串手写的正则,把标题、粗体、斜体、链接、列表、代码、引用和分隔线转成一段 HTML 字符串,再用 dangerouslySetInnerHTML 注入。正则解析器更轻,但能力也弱得多,而且信任它的输入;它只会被喂仓库内部的证据文件,绝不会碰访客提交的东西。
本组件内容
- document-publishing-architecture ——这些证据路由挂靠其上的 Next.js 外壳
- esl-pain-point-scraper-suite ——产出这些 viewer 所读 JSON 与 Markdown 的上游