diw_craft 的技术实现说明:框架选型、依赖关系、内容同步机制与日常发布工作流,面向略懂技术的设计师。
这篇文章是 diw_craft 站点的技术实现说明——不是教程,而是一份让你能读懂这个系统怎么运作的参考文档。
如果你懂一点 React、Next.js 或者 Git,这里的内容不会陌生;如果不熟悉这些词,只看第四节(工作流)就够了。
diw_craft 是一个 Notion 驱动的个人作品集网站。所有内容在 Notion 数据库里管理,通过一个同步脚本拉到本地,再由 Next.js 构建成静态网页部署到 Vercel。
核心理念:内容在 Notion 编辑 → 同步到本地文件 → 构建时直接读取本地文件 → 运行时不依赖 Notion。
这意味着网站上线后,即使断开 Notion 连接,网站照样正常运行——因为内容已经编译进静态文件了。
数据流向如下:
Notion 数据库
↓ npm run sync(拉取 + 图片压缩 + 生成索引)
本地文件系统(提交到 Git)
↓ npm run build / git push
Next.js 静态页面 → Vercel CDN
本地文件系统的结构:
content/metadata.json — 所有条目的元数据(标题、分类、封面路径等)content/items/{slug}/body.md — 每篇文章的 Markdown 正文content/items/{slug}/blur.json — 封面图的模糊预览数据public/content-images/{slug}/ — 已下载并压缩的封面图和内联图片public/search-index.json — 全文搜索索引
这些文件都提交到 Git 仓库,Vercel 拿到代码后直接构建,不需要在服务器上连接 Notion。运行时依赖(最终打包进网站的):
Next.js 16(App Router)— 页面框架。主要用它的 SSG(静态页面生成)能力:构建时把所有内容页预渲染成 HTML,用户访问时直接返回静态文件,不用等服务器实时计算。
React 19 — UI 组件层,Next.js 的底层。
TypeScript — JavaScript 的类型扩展,让代码更安全,编辑器能提示错误。
Tailwind CSS v4 — 样式方案。直接在 HTML 元素上写样式类名,不用单独写 CSS 文件。
Framer Motion — 动画库,处理页面过渡、卡片交互等动效。
next-mdx-remote — 把 Markdown 渲染成网页内容。文章正文存为 .md 文件,由这个库在服务端编译成 HTML。
MiniSearch — 本地全文搜索库。搜索直接在浏览器内完成,不走服务器。
Sharp — 图片处理库。sync 脚本用它压缩封面图(超过 1MB 的自动转成 WebP 格式),运行时 Next.js 也用它做图片优化。
Zod — 数据校验库,用于验证从 Notion 同步过来的数据结构是否正确。 仅在开发/同步时用到(不进生产包):
@notionhq/client — Notion 官方 API 客户端,只有 sync 脚本用它,网站运行时完全不依赖。
tsx — 让 Node.js 能直接运行 TypeScript 文件,用来执行 sync 脚本。
npm run sync 做了什么这是整个系统最核心的一步。运行命令:
npm run sync
脚本(scripts/sync-notion.ts)按顺序做以下事情:
1. 从 Notion 拉取页面
查询数据库,获取所有 Status 为 Published 或 Protected 的页面。
2. 增量比对
对每个页面,比较 Notion 的 last_edited_time(最后编辑时间)和本地记录的时间。如果没变化,跳过这条,直接复用本地文件。这让 sync 速度很快——即使数据库有几十条,通常只有 1-3 条需要重新处理。
3. 下载并处理封面图
对需要更新的条目,下载 Notion 里上传的封面图。如果文件超过 1MB,用 Sharp 压缩成 WebP 格式(质量 80%)。同时分析图片底部区域的亮度,判断文字叠加层应该用深色还是浅色——这个数据存在 blur.json 里,用于卡片图片加载前的占位显示。
4. 生成本地文件
把页面正文转换成 Markdown,和元数据一起写入本地文件。
5. 生成搜索索引
把所有公开条目的标题、摘要、标签、正文前 500 字整合成 search-index.json。密码保护的条目只索引标题和摘要,不索引正文。
6. 清理已删除的条目
检查本地文件中有哪些 slug 已经不在 Notion 的结果里了,删掉对应的本地文件和图片。
全量重新同步(忽略增量缓存):
npm run sync -- --force
网站的页面路由:
/ — 首页,展示所有公开条目,Bento 卡片网格布局/projects /writing /photos /play — 四个分类页,分别对应 project / writing / photo / experiment 类型的内容/items/[slug] — 内容详情页,SSG 预渲染,构建时生成所有 slug 对应的静态 HTML/password — 密码输入页
所有列表页和详情页都是 Server Components(服务端组件)——数据读取发生在服务端,返回给浏览器的是已经渲染好的 HTML,不需要客户端再去发请求拿数据。SearchDialog(搜索对话框)和 CardModal(卡片预览抽屉)是 Client Components,用 dynamic import 延迟加载,不影响首屏性能。
对于 Notion 里标记为 Protected 的条目,网站有一套安全的处理方式:
预渲染的静态 HTML 只包含标题、摘要等元数据,正文内容不出现在 HTML 里。
用户访问时,页面加载一个客户端组件(ProtectedBodyLoader),流程如下:
/api/protected-content/{slug}/api/auth → 服务端比对 SITE_PASSWORD 环境变量按 Cmd+K(Mac)或 Ctrl+K(Windows)触发搜索对话框。
搜索用的是 MiniSearch,一个纯 JavaScript 的全文搜索库。搜索索引(/search-index.json)在 sync 时生成,部署后作为静态文件由浏览器直接加载,搜索完全在本地执行,没有服务器请求。
搜索字段权重:标题(3)> 标签(2)> 摘要(1.5)> 正文(1)。支持模糊匹配(fuzzy: 0.2)和前缀匹配(prefix: true)。
发布新内容或更新内容:
# 1. 在 Notion 数据库编辑或新增条目
# 2. 同步到本地
npm run sync
# 3. 本地预览(可选)
npm run dev
# 4. 提交到 Git
git add content/ public/
git commit -m "sync: update content"
git push
# 5. Vercel 自动检测到新提交,触发部署(约 2-3 分钟)
Notion 数据库的必填字段:
my-new-project)SITE_PASSWORDNOTION_API_KEY 和 NOTION_DATABASE_ID 只在本地 sync 时用,不需要配置到 Vercel。