我是怎么搭建 YeagerZhao Blog 的:从本地写作到双语网站的完整架构
做这个网站时,我想有一个自己的地方,放做过的项目、写过的文章,还有旅途中拍下的照片。最初关心的是页面:头像、导航和照片怎么排。开始发布中英文长文后,我才需要把另一条链路理顺——本地稿件怎样进入 Notion,图片怎样保持可用,修改又怎样出现在网站上。
现在,我在本地保存中文稿、英文稿和素材,发布工具把正文写入 Notion、把配图上传 OSS,Next.js 再根据语言和 Slug 生成页面。下面先跟着一篇文章走完这条链路,再解释各层为什么这样分工,以及哪些地方仍需要人工核对。
网站在 www.yeagerzhao.com。本文首次发布于 2026 年 9 月 12 日,最后核对日期为 2026 年 10 月 13 日。本轮正文和配图通过 Notion 更新;文中单独标明的代码修复仍在本地审阅,确认后才进入 Git 和线上部署。配图表示逻辑职责,不是部署监控截图。
先明确网站与写作的需求
网站要放下哪些内容
我把网站分成 Home、Blog、Product、Journey 和 About 五个入口。Blog 用来展开讲一件事,可以写问题的来由、实现过程和遇到的麻烦。Product 更接近项目介绍,让读者先知道我做了什么、目前是什么状态,再决定是否继续读细节。Journey 主要展示照片和视频,文字只补充时间、地点和感受。About 则放个人介绍与经历。
这些内容的阅读方式并不一样。项目卡片需要让人快速看懂,长文章需要目录和舒服的段落间距,照片最好有足够的展示空间。如果所有内容都套进同一张“标题加摘要”的卡片里,开发倒是方便,但网站很难按我想要的方式呈现。
编辑方式也影响了技术选择。我习惯先在本地文件夹里整理文章,与 AI 讨论中文稿,再生成英文稿。这里可以放截图、未完成的段落和临时想法。等内容稳定后,我才希望把它放进 Notion 管理发布状态,交给网站展示。
如果只需要少量纯文字博客,Markdown 或 MDX 加静态站点就已经够用。我选择 Notion,是因为还想保留一个方便修改内容的后台;引入 OSS,则主要是为了让文章配图和摄影作品有独立、稳定的文件地址。这些额外要求,也带来了 API、图片处理和缓存同步的工作。
选择这套组合的关键,是把三种变化分开:稿件更新交给内容发布,图片变化交给资源地址,页面功能变化交给代码部署。它们会在读者打开网页时汇合,但各自有不同的完成条件。
一篇文章怎样从本地进入网站
从一次发布看完整闭环
用 my-next-article 举例,我先准备 draft.zh.md、draft.en.md、manifest.json 和配图,保持 published=false。dry-run 检查文件与转换结果,正式运行后先得到两条未公开的 Notion 草稿。审核需要覆盖两种语言的开头、结尾、表格行和图片;审核完成后再公开,并刷新列表和两个详情地址。
npm run publish:content -- --dir "<文章目录>" --dry-run
npm run publish:content -- --dir "<文章目录>"
npm run revalidate:site -- --paths /zh/blog,/en/blog,/zh/blog/my-next-article,/en/blog/my-next-article最后访问 /zh/blog/my-next-article 和 /en/blog/my-next-article,检查正文、图片与语言切换。上传没有报错、Notion 中能看到页面、正式网站已经更新,是三个需要分别确认的结果。Notion 不会仅因为保存了笔记就自动推送这个网站,OSS 也不会通知网页替换图片。

图中的实线表示请求或写入,虚线表示响应或主动失效。作者与 AI 在本地准备内容,图中发布工具负责重复写入;AI 不是网站运行时必须在线的服务。
中文稿先写稳,再处理英文
我把创作目录放在代码仓库之外。每篇文章有自己的文件夹,Markdown、截图和架构图放在一起;另一个 archive 目录保存发布前后的备份。代码版本和内容版本因此可以各自管理,私人素材也不容易被误提交到 Git。
YeagerZhao/
content/
blog/
yeagerzhao-blog-architecture/
draft.zh.md
draft.en.md
manifest.json
images/
product/
archive/写作时先解决中文表达:这篇文章想回答什么,读者需要哪些背景,哪个地方只靠文字不容易看懂。截图和图示跟着解释走,放在读者需要它的位置。中文稿稳定后,再整理英文,保留相同事实和限制,但句子不必逐字对应。
这一部分后来也成为我调整最多的地方。让 AI 直接“介绍整个项目”,很容易得到一篇技术名词齐全、却读不出重点的文章。它能列出用了哪些框架,但仅凭代码,并不知道我为什么想改掉某个页面,或者哪一次发布最让我在意。
现在我把自己的要求整理成了 yeagerzhao-writing:先给笔记、截图和真实遇到的问题,必要时补一段口述,让 AI 整理出文章主线。方向明确后再写完整中文稿,分别检查技术事实和表达。比如“我增加了缓存”还不够,需要讲清楚当时慢在什么地方、改了哪一层、还有什么没解决。没有测量记录,就不补一个看起来漂亮的提速比例。
我仍需要确认文中的个人判断。AI 可以建议一种解释,但“我为什么选择它”要符合自己的真实想法。中文定稿后,英文沿用同样的事实和限制,再针对语言习惯重新表达。风格规则会继续用我认可的改稿来修订,配图、上传和缓存刷新则交给独立工具。这能减少重复操作,但还不是放进一个题目就能自动得到可靠文章的系统。
manifest.json 负责正文以外的元数据,包括类型、Slug、日期、标签,以及两种语言的标题和摘要。新稿建议先保持未公开,下面是一个简化示例:
{
"type": "blog",
"slug": "my-next-article",
"date": "2026-09-12",
"tags": ["Next.js", "Notion"],
"published": false,
"cover": "images/cover.png",
"zh": { "title": "下一篇文章", "summary": "文章的中文摘要。" },
"en": { "title": "My Next Article", "summary": "A short description." }
}发布工具负责重复操作
准备发布时,我先运行 dry-run。它会读取 manifest 和两份 Markdown,尝试转换正文,检查本地图片引用。这个检查能发现缺文件和部分格式问题,但还不能替代线上 schema 验证,更不会判断文章里的结论是否正确。
npm run publish:content -- --dir "<文章目录>" --dry-run
npm run publish:content -- --dir "<文章目录>"正文通过 @tryfabric/martian 转成 Notion blocks,再处理图片地址和代码语言名称等细节。比如代码块中的 text,需要转换成 Notion 接受的语言枚举;表格需要保留行与单元格,不能只上传一段看起来像表格的字符串。
本地文章配图会上传 OSS,然后作为 external image 写入 Notion;已有公开 HTTP(S) 地址则保留原链接。这里与“所有文章图片都直接上传 Notion”的早期方案不同。Notion 原生图片仍然支持,但本地发布工具目前使用的是 OSS 路径。
中文和英文按约定查找已有页面。本轮本地工具已经把两种更新模式拆清楚:--overwrite 只改属性,保留正文和封面;--replace-content 更新正文并保留恢复快照。新建、改属性和换正文使用不同入口,避免重复追加。Published 默认是 false,审核后再公开;在 manifest 里明确设成 true,则允许直接进入网站的公开查询。
上传成功以后,还要确认内容完整
替换正文不是数据库事务。页面 blocks 分批写入,中间请求失败,就可能出现中文已完成、英文只更新了一部分的情况。这篇长文之前就遇到过更新中断,最后依靠快照和重新读取结果恢复。
本轮本地发布工具会先保存页面属性和完整递归正文,追加新 blocks 并回读核对后,才归档旧 blocks;失败会尝试清理新内容并恢复原 blocks。它仍不是跨页面事务,两种语言可能先后完成,仍需检查快照状态。因此我会保留旧稿,再核对 Notion 中的实际内容:开头和末尾是否完整,图片是否仍然是 image block,表格有没有子行,两种语言是否都写入成功。随后刷新网站缓存,访问正式 URL 检查。命令退出没有报错,只能算这次检查的一部分。
本地归档仍有边界。本轮 sync:notion --pull 已补齐页面属性与完整递归 blocks,并按时间保存快照;图片仍以 URL 引用保留,尚不是包含离线图片的完整资源包。正文 Markdown 导出和双向编辑冲突合并还未完成,不能把这些 JSON 快照称为一键双向同步。
一个 Next.js 项目怎样管理四个内容库
保留一个应用,把内部职责拆清楚
这个网站没有单独部署后端服务。它的大部分服务端工作,是查询已发布的内容、读取正文、生成页面,以及处理缓存刷新和图片代理,Next.js App Router 已经可以承担。
后端代码仍然存在,只是和前端放在同一个项目里。Notion 密钥留在服务端,页面组件不直接接触它。这样少了一个需要维护的服务,也避免了网站每次获取内容时,先通过 HTTP 请求自己的另一个接口。
当前项目使用 Next.js 15.5.15、React 19.1 和 TypeScript 5,样式由 Tailwind CSS v4 与项目自己的 CSS 变量共同管理。双语路由交给 next-intl,Notion 使用官方 SDK,Markdown 转换和 OSS 上传则放在本地发布工具中。这些是项目当前使用的版本,并不是给新项目的通用版本推荐。
代码经过几轮整理后,主要结构如下。完整文件当然更多,但日常修改通常先从这几层找起。
src/
app/ 页面路由、metadata、loading 和 API 入口
[locale]/ 中文和英文路由
api/ JSON、缓存刷新、Notion 图片代理
server/
notion/ 内容查询、字段映射、正文读取
cache/ 缓存时间、标签和失效逻辑
images/ 图片代理
ui/
tokens/ 颜色、字体、间距和动效变量
components/ 按钮、导航、卡片等共享组件
views/ 首页、文章、项目、图册等视图
styles/ 页面布局和正文排版
effects/ Hero 与共享动效
i18n/ 语言路由配置
lib/ 图片地址、视频链接等辅助函数
types/ 共享数据类型
SKILLS/ 本地写作、发布与运维工具路由层负责知道“现在打开的是哪一篇”,服务端负责把数据准备好,UI 再把它呈现出来。比如调整 Notion 字段,我主要检查查询和 mapper;想修改文章底色,就去样式和 tokens 中找。目录名字本身没有隔离能力,正确的导入关系和服务端密钥边界仍然需要代码保证。
跟着一篇文章的地址往下看
以 /zh/blog/yeagerzhao-blog-architecture 为例,zh 表示语言,最后一段是文章的 Slug。app/[locale] 中的方括号就是动态路由参数,访问英文地址时,这个参数变成 en。
页面先调用 getBlogPostBySlug 查询文章元数据,得到标题、日期、标签和 Notion page ID,再用 page ID 获取正文 blocks。之后由 ArticleDetailView 组织布局,NotionBlocks 渲染正文。Product 的过程相似,只是内容类型不同。
文章 URL
-> locale + slug
-> 查询 Content 元数据
-> 获得 Notion page ID
-> 获取正文 block 树
-> 文章布局与正文渲染
-> 返回页面Notion 原始响应不太适合直接交给组件。一个标题可能拆成多个 rich text 片段,标签是 multi-select 对象,图片又有不同的来源类型。mapper 把这些结构转换成普通字符串、数组和明确的类型,页面就不需要反复解释同一套 Notion JSON。
这里还有一个配置细节容易踩坑:环境变量仍然叫 NOTION_CONTENT_DATABASE_ID,当前 SDK 5 的查询却走 dataSources.query,实际需要的是 data source ID。数据库页面链接里的 ID 不能不加区分地填进去,还要确认 integration 已获准访问对应内容。Notion 对两者的关系有官方说明。
文章和项目共用 Content
早期方案按栏目分别建库:文章一个、项目一个,再给个人介绍、经历、兴趣和社交链接各建一个。分类很清楚,但配置随之变多,相近的标题、日期和语言字段也要重复维护。后来我把它们重新合并,最后留下 Content、Journey、JourneyPhoto 和 About 四个库。
文章和项目都有标题、摘要、日期、标签、封面和正文,因此最终共用 Content,用 ContentType 区分 blog 和 product。网站查询中文文章时,条件就是类型为 blog、语言为 zh,并且 Published 已勾选;详情查询再加上 Slug。
正文写在 Notion 页面的内容区,属性只保存检索和展示需要的信息。这样打开数据库能筛选文章,点进一条记录又能像普通笔记一样修改段落和图片。
| 属性 | 在网站中的用途 |
|---|---|
| Title、Summary | 标题和列表摘要 |
| ContentType、Language、Slug | 区分栏目、语言和文章地址 |
| Date、Tags | 日期排序和内容标签 |
| Published、Featured | 是否公开、是否首页推荐 |
| CoverImage | 封面图片引用 |
| ProductUrl、Status | 项目外链与项目状态 |

截图保留了早期数据,列的显示方式不代表今天所有字段的类型。这里想展示的是同一个库怎样容纳不同类型与语言,而不是让读者照着截图逐列复制。
项目介绍和博客也不必写成同一篇。Product 可以集中说明项目能做什么、当前状态和访问入口,Blog 再展开讲设计过程。共用数据结构,主要是为了减少后台维护。
中文和英文怎样对应
界面语言和文章语言是两回事。“返回”变成“Back”属于界面文案;英文正文需要确实有一篇英文稿,不能靠导航切换自动产生。
现在同一篇文章在 Content 中有两条记录,使用相同 Slug,Language 分别为 zh 和 en。发布工具按 ContentType + Slug + Language 查重,不再额外维护 TranslationKey。标题可以调整,已经分享出去的 Slug 尽量保持稳定,否则旧链接就需要另外处理。
这个约定需要发布工具配合。Notion 不会自动为这三个字段建立唯一约束,现有查重也不等于数据库层面的唯一性保证。公开发布前仍要确认同一类型、Slug 和语言只对应一条记录。英文还没有准备好时,创建一条空记录并不算完成双语发布。
图集和个人介绍保留自己的结构
Journey 的一条记录代表一次完整展示,可以是图集、Vlog、动漫混剪或游记,保存标题、日期、地点、封面、感想和视频地址。图集里的每张照片另放在 JourneyPhoto,通过 relation 关联所属条目。列表页先读取封面与基本信息,进入图册后再读取照片,就不必为了展示几个封面先加载整组作品。
当前 Journey 按 Date 倒序,照片查询按 Order、再按 Name 升序。这和早期只按名称排序的设想有区别,实际使用要以当前查询和发布脚本为准。中英文图集可以引用同一份 OSS 图片文件,不需要因为翻译再上传一份照片。
About 则用一个数据库承载个人介绍、经历和亮点,由 SectionType 区分 profile、timeline 和 highlight。查询按语言和 Published 过滤,按 Period 排序,再转换成不同的小节。B站、小红书、CSDN 和 GitHub 链接变化不频繁,留在网站配置里,不再单独建 SocialLink 库。
四个库对目前的内容规模已经够用。需要多一列时,我会先考虑网站是否真的用得到它,以及发布工具能否稳定填写,而不只是觉得以后可能有用。
本轮本地查询包装已跟随数据源的分页游标,列表、照片与 Slug 枚举不再只处理首批结果;正文 blocks 原本就有独立的分页与递归。这个修复需要随网站代码部署,本文更新不会自动改变线上查询实现。
文章配图与摄影作品的独立链路
文章图片要有稳定的引用
文章里的截图和示意图,主要是帮助理解正文。我已经有本地文件时,会在发布阶段上传 OSS,让 Notion 保存地址,浏览器直接读取图片。页面服务器无需替每一次阅读转发全部图片字节,图片也不依附于某次网站构建。
另一种情况是在 Notion 编辑器里直接上传图片。API 返回的文件 URL 有有效期,过期后需要重新读取 block 获取新地址。应当以 expiry_time 为准,不能把某个固定时长当成所有文件的规则。Notion 的文件读取文档解释了这种地址的生命周期。
网站用 /api/notion-image 处理这类来源,检查 host、响应类型和大小,并可结合 block ID 尝试刷新地址。代理上的缓存头减少重复传输,第一次读取仍然要等待上游响应。所以“用户打开时再取回来缓存”能工作,但不代表冷访问就一定很快。
图片更新还有一个与正文不同的缓存问题。把同一个 OSS 地址覆盖成新文件,浏览器可能继续使用旧图;刷新 Next.js 缓存不会自动清掉浏览器里的图片。本轮发布工具将图片内容哈希写入对象文件名:相同内容复用同一地址,图片内容变化就产生新地址,同名但内容不同的图片也不再相互覆盖。Notion 引用随之更新,原来的链接仍可用于恢复旧稿。
摄影图片先处理隐私和尺寸
Journey 的图片处理要更谨慎。原片和修图工程留在本地,公开 OSS 只接收处理后的展示母版。上传之前,工具先根据方向信息校正像素,再缩放、去除原始元数据并处理水印。

方向校正要在移除元数据之前完成,否则竖拍照片可能失去旋转信息。EXIF 和 GPS 去掉之后,也还需要检查画面本身:门牌、车牌或屏幕上的私人信息直接存在于像素里,工具清理元数据不会替我遮住它们。
当前摄影处理使用约 800 万像素的面积预算。相比只限制长边,这种方式对全景照片更友好。以 12000 × 2000 的图片为例,长边压到 3500 后,短边只剩约 583 像素;按 800 万像素缩放,则大约是 6928 × 1155。两种方式都保持比例,后一种给宽幅画面留下更多纵向细节。
缩放系数 = min(1, sqrt(目标像素数 / 原始像素数))
新宽度 = 原宽度 × 缩放系数
新高度 = 原高度 × 缩放系数这只是尺寸计算,最终文件体积仍取决于图像纹理、编码质量和水印处理。800 万像素是当前取舍,不保证每张图都会压到同样大小。摄影发布还依赖本地的 sharp 与 Python 环境,新电脑需要单独检查这些依赖,网站能启动并不代表图片工具已经可用。
水印工具会嵌入 YeagerZhao 文本,并提供解码检查。它能增加识别线索,但不代表经过裁切和压缩后一定还能恢复。这里更值得关注的是实际处理后的测试结果,而不是图片处理脚本有没有成功退出。
我也没有把禁止右键当成版权保护方案。浏览器能显示图片,用户就能获得它的像素,截图和直接请求都无法靠一个按钮限制阻止。能做的是不公开原片、控制展示尺寸、保留创作记录,并验证水印在实际处理后的表现。
列表和画册不需要下载同样大的图
作品上传以后,网站还会按阅读场景选择图片。列表封面只占一小块,画册主图需要更多细节,放大查看时才获取展示母版。src/lib/oss-image.ts 把这些选择集中成三个预设:
| 场景 | 当前处理方式 |
|---|---|
| thumb:列表封面 | 源尺寸的 25%,质量参数 70,默认 WebP |
| preview:画册预览 | 源尺寸的 40%,质量参数 78,默认 WebP |
| full:放大查看 | 直接读取处理后的展示母版 |
full 不是相机 RAW,也不是完整修图工程。它只是公开的最大展示版本。thumb 和 preview 则通过 OSS 的图片处理参数生成,不必手动维护三份独立文件,例如预览地址可以带上 x-oss-process=image/resize,p_40/format,webp/quality,q_78。辅助函数只对识别出的 OSS 来源增加参数,其他来源保持原样。OSS 官方图片处理文档列出了这些能力;对象存储、图片处理和独立 CDN 加速需要分开理解。
Journey 页面因此可以更接近画廊。列表按时间组织作品,以封面为主,展示标题、年月与类型;搜索用于找到某个地点或关键词。进入图集后,窄文字栏放说明,大部分空间留给照片,翻页、键盘操作、顺序指示和放大查看由图册组件负责。Vlog 和混剪保留 B站或 YouTube 地址,交给视频链接逻辑处理,不需要把视频文件一起放进摄影图床。
本轮本地图册也改为只挂载当前及相邻照片,并为大图弹层补上焦点进入、Tab 循环和退出后返回触发点。图片策略与交互都需要在本地检查后再部署,线上目前仍使用上一版图册实现。
让长文章完整而安静地显示
正文读取和渲染需要对应起来
Notion 页面是一棵 block 树。段落、标题、代码、图片和表格是不同节点,有些节点下面还有内容。如果只取页面第一批 blocks,或者只处理顶层节点,短文章可能看不出问题,长文和表格就容易缺内容。
当前正文读取会跟随游标拉取顶层 blocks,再递归获取子节点,递归上限为四层,并限制子 block 请求的并发。渲染层把它们变成对应的 HTML:段落用 p,列表合并成 ul 或 ol,图片放在 figure,代码使用 pre 和 code。
之前表格没有正常显示,就是这种结构问题。Notion 的 table block 主要描述表结构,单元格内容在它下面的 table_row 中。只渲染父节点,页面当然拿不到完整表格。文章是否显示正常,需要把“读取了哪些节点”和“怎样呈现这些节点”放在一起检查。
自有渲染器现在可以处理常见正文、行内格式、列表、引用、折叠、图片、表格和多种媒体,但仍有边界。文件、书签和子页面可能以链接或引用信息呈现,公式、同步块和数据库视图也不能等同于完整复刻 Notion。代码块目前保留文本和语言标识,单有 language class 并不会自动产生语法高亮。
自有渲染层让 HTML、样式和交互更容易按网站需求调整,也意味着遇到新的 block 类型时,需要自己检查它能否完整显示。对这个项目来说,先把常用正文类型做好,比承诺复刻整个 Notion 编辑器更实际。
排版先照顾正文,再安排目录
文章变长以后,原来不明显的布局问题会放大。我遇到过内容离顶部太近、固定导航压住标题、目录过长,以及正文和目录组合后视觉中心偏移的情况。这些需要统一的页面间距和布局约束,不能靠每篇文章单独加一个空行解决。
线上现版文章页把正文和目录一起组织,侧栏里的日期与标签分行,摘要留在外部卡片和 metadata 中。目录从标题生成,滚动时跟踪当前小节,自身高度按约十行的设计限制,避免长目录一直撑下去。返回按钮保持左箭头加文字,与图册一致。
这篇长文还暴露了一个 CSS 细节:段落间距本来设置过,却被后面的 margin: 0 覆盖了,连续几段看起来像挤在一起。修复后,正文设为 16px、1.85 倍行高,段落间距 18px,图片、引用和代码块前后使用 24px。这些参数已放入共享 typography tokens,并在公网检查了实际计算样式。
网页样式之外,内容结构也影响阅读。如果把每个实现细节都列成一个平级标题,读者会不断跳进新话题,却难以记住整篇文章正在解决什么。现在我把相关问题放回同一个大章节里,小标题用于提示进度,具体原因和结果用段落交代。代码块和表格仍保留在需要查阅的地方,不要求读者先读完一份配置手册才能理解项目。
用共享视觉规则减少反复调整
网站最初参考 YeagerZhao Design System 的完整 HTML,后续逐渐拆成 tokens、components、views 和 styles。颜色、字体、间距、圆角和动效时间放进变量,同类组件复用规则。文章面板与首页“最近项目”等面板采用共享的底色约定,调整时就不必到处找颜色值。
英文主要使用 Newsreader,代码和等宽标签使用 Spline Sans Mono,一般中文使用系统字体栈。首页中文问候语保留思源宋体风格,标语使用对应的无衬线字体资源。字体库资源由网站自托管,避免临时依赖第三方字体服务;系统中文字体仍会随设备变化,不能同时承诺所有设备字形完全一致。
Hero 动态背景、卡片滚动显现和 hover 反馈也尽量共用逻辑。首页可以有更明显的动态效果,阅读文章时则保持安静。统一设计的目的,是以后改动容易保持一致,不需要每个页面都表现得一样热闹。
本轮本地阅读页把完整标题、日期和标签统一放在正文顶部,正文中的重复首标题由渲染入口识别并去除,侧栏专注目录。目录保留完整章名,支持展开小节和可分享的 URL 锚点;手机提供折叠目录。非 Hero 页面加入低对比的水彩色层与细颗粒,正文保持不透明面板。列表动效收敛,减少动态效果时保持内容直接可读;Hero 本身的视觉背景保留,只补上离屏、后台和静帧的绘制控制。这些界面修复已在本地代码完成,本轮只发布文章内容,尚未部署界面代码。
内容更新与代码部署怎样保持一致
先分清保存的是什么
“点开文章慢”和“Notion 改了却没更新”需要检查的路径不同。前者可能在等待内容源、页面生成或图片下载;后者可能还在使用旧页面、旧查询结果或浏览器路由状态。默认 3600 秒是重新验证策略,不是每到整点主动同步所有文章。
| 层 | 保存什么 | 更新边界 |
|---|---|---|
| 页面 ISR | 生成的页面结果 | 对应路径重新验证 |
| Next 数据缓存 | Notion 查询与正文树 | cache tag 失效 |
| 浏览器 Router Cache | 已访问或预取的路由数据 | 导航或刷新后重新取得 |
| HTTP 图片缓存 | 某个 URL 的图片响应 | URL 版本或该缓存自己的失效规则 |
线上上一版查询还叠加进程内 Map,主动刷新会处理全部已定义 tags。本轮本地修复移除了查询 Map,避免一次刷新仅清掉某个实例;指定 Blog、Product、Journey 或 About 路径时,按内容域失效标签。首页或默认全站刷新仍失效所有域。这里是按域刷新,还不是每篇文章独立的精准失效;已打开的浏览器也可能需要重新导航或刷新。Next.js 15 缓存接口说明解释了标签与重新验证,具体效果仍取决于项目组合。
预取与反馈分别解决什么
我曾希望打开首页就预热其他页面的全部正文,但这些后台读取会竞争网络和 Notion 请求额度。现在一级导航交给 Next Link;Blog/Product 列表已有意图预取,本轮本地也把首页详情入口接到同一规则:鼠标进入、键盘聚焦或触摸后短暂等待,再恢复预取。离开卡片会取消尚未触发的定时器。
loading 边界提供点击反馈,不能代替正文完成加载。评估时需要分开测点击反馈、正文可读和关键图片完成,并区分冷热缓存。自动预取主要在生产模式下判断,开发环境首次点击包含编译时间,见 Next.js 15 预取文档。JSON 列表接口仍保留,但服务端组件直接复用查询函数,不请求自己的 HTTP 接口。
代码与内容各走一条更新路径
网站代码通过 GitHub 推送触发 Vercel 构建;布局、查询和交互的修改都需要这一步。正文则由本地工具写入 Notion,图片上传 OSS,之后刷新网站缓存并检查两种语言的正式 URL。修改文章不需要顺带部署尚未审阅的页面功能。

这张图只展示当前采用的职责,不再把早期 ECS 自托管方案放进同一张主图。主域名、www 别名和站点 URL 配置要保持一致;Cloudflare 的具体代理与缓存规则需按实际部署另行核对,不能从一个域名可打开反推完整拓扑。
本地生产验证使用 npm run build 与 npm start。Notion 请求失败应该继续抛错,交给错误边界处理;“没有匹配文章”和“没有成功请求到文章”应分开,否则空列表可能把故障缓存下来。本轮还降低了构建时的静态生成并发,减少集中向 CMS 发起请求。
部署环境需要 Notion 凭据、data source ID、站点地址与 revalidate secret。OSS 上传密钥、原始素材和创作目录留在本地发布机。metadata、Open Graph、sitemap 和 robots 描述并发现页面,不保证搜索排名。本轮本地代码还打通 CMS 的 SEO/封面描述字段,只为实际存在的译文生成语言链接;sitemap 不再把每次生成时间当成内容修改时间。
已经解决什么,下一步先补什么
现在,这个网站已经能放下最初想记录的几类内容:文章、项目、摄影和个人介绍。写一篇新文章时,我可以在本地准备中文、英文和图片,再交给 Notion 与网站展示;调整界面时,也不用重新搬运所有正文。内容和页面终于有了相对独立的维护方式。
代价同样明确。Notion 的便利需要用查询、字段映射、分页和缓存处理来换;自有渲染器需要持续验证不同 block;图片独立存储以后,还要考虑版本地址、隐私和分发尺寸。选择这套组合,并没有让维护消失,只是把维护工作放到了更明确的位置。
本轮已经在本地补上发布快照与回读、查询分页、内容哈希图片地址、正文 JSON 归档和阅读交互修复;网站代码仍等待审阅与部署。接下来优先验证发布中断后的恢复,补齐离线资源与 Markdown 导出,并处理 Notion 在线编辑和本地修改的冲突。自动 webhook 仍需要事件验证、去重与失败恢复,不能算作现成功能。
对这个项目,我现在的完成标准也更具体了:发布后,中英文正文完整,图片能打开,本地留有可恢复的版本,下一次修改知道从哪里开始。接下来的改动会围绕这些事情展开。页面还可以慢慢调整,先让内容可靠地留下来,网站才值得一直用下去。