项目3 · Next.js 个人知识站

用 App Router 和本地 SQLite 做一个能长期维护的个人知识站:检索、编辑、版本、归档、导出都要真实可用。

约 12 小时
学习重点
  • 用 App Router 的目录结构组织列表、详情、新建、编辑、版本历史和归档恢复等页面,并处理动态参数与 not-found
  • 把数据访问收在服务端数据层:Server Component 直接读 SQLite,浏览器构建里不出现 node:sqlite
  • 用 node:sqlite 建 articles / categories / article_versions 三张表,写可重复执行的迁移函数和 seed 脚本
  • 在 Server Action 写入成功后调用 revalidatePath,理解数据库、文件系统与路由缓存各自的职责
  • 每次编辑保存一条版本记录,支持查看历史与回滚,并用 Route Handler 导出 JSON / Markdown
页面 / 接口清单
  • 01文章列表页:分类筛选、关键词搜索、结果计数与空状态
  • 02文章详情页:标题、分类、标签、正文、更新时间与操作入口
  • 03新建文章页:Server Action 提交、字段校验、slug 唯一性检查
  • 04编辑文章页:回填原值、保存后生成新版本并刷新详情
  • 05版本历史页:版本号、变更说明、时间与正文预览,支持回滚
  • 06归档页:归档列表、恢复操作,归档文章不出现在默认列表
  • 07导出页:下载全部数据 JSON 和文章 Markdown
  • 08Route Handlers:/api/export 与 /api/export/markdown 两个下载接口
验收清单
0 / 8(0%)

列表 /、详情 /articles/[slug]、新建 /articles/new、编辑 /articles/[slug]/edit、版本 /articles/[slug]/versions、归档 /archive、导出 /export 均可访问;不存在的文章进入 not-found,导航链接可用。

lib/db.ts 使用 node:sqlite 打开 data/knowledge.db,通过 PRAGMA user_version 顺序执行迁移,建出 categories、articles、article_versions 三张表;npm run seed 写入 3 篇示例文章且可重复执行不重复插入。

首页默认只显示 active 文章并按更新时间倒序;分类下拉与关键词可组合生效,URL 上保留 q 和 category 参数;无结果时显示空状态和清除筛选入口。

详情页完整展示标题、分类、标签、正文和更新时间;新建表单校验标题、slug 格式、摘要、正文和分类,重复 slug 有明确错误提示,成功后跳转详情。

编辑页回填原值;每次保存更新 articles 并插入一条 article_versions,version_no 递增;版本历史页按版本号倒序列出变更说明、时间和正文预览。

任意历史版本可回滚;回滚后详情页内容恢复为该版本,同时新增一条「回滚到 vN」的版本记录,旧版本不被删除。

详情页可归档,归档后首页和搜索结果不再出现;归档页列出全部 archived 文章并可恢复,恢复后重新出现在首页,状态在详情、列表、导出三处一致。

所有写操作在返回前调用 revalidatePath 覆盖列表、详情、归档等受影响路径;/api/export 下载包含三表的 JSON,/api/export/markdown 下载 Markdown;solution 与 starter 均 npm run build 通过,node tests/check.mjs 判定 solution 通过、starter 存在缺口。

README(项目目录)
# Next.js 个人知识站(p3-knowledge-site)

用 Next.js 16 App Router + React 19 + `node:sqlite` 做一个真正能用的个人知识站:文章列表、分类筛选、关键词搜索、详情、新建、编辑、版本历史、回滚、归档恢复、JSON / Markdown 导出。

这个项目的重点不是「页面能打开」,而是把服务端数据访问、写入校验、版本快照和缓存失效连成一条完整的链路。

## 学习目标

- 用 App Router 的目录组织页面:静态段、动态段 `[slug]`、`searchParams`、`notFound()`。
- 在 Server Component 里直接读本地 SQLite,理解为什么数据库代码不能进入浏览器构建。
- 用 `node:sqlite` 建 `categories`、`articles`、`article_versions` 三张表,写基于 `PRAGMA user_version` 的迁移和可重复执行的 seed。
- 用 Server Actions + `useActionState` 完成表单校验、错误回显、防重复提交。
- 每次编辑保存一条完整版本快照,支持查看历史与回滚;归档只改状态,不伪造版本。
- 写入后调用 `revalidatePath`,让列表、详情、归档页立刻反映数据库变化。
- 用 Route Handler 输出 JSON 与 Markdown 下载,完成数据导出闭环。

## 环境要求

- Node.js **24+**(`node:sqlite` 在此版本稳定;不需要安装数据库服务)
- npm 10+
- 无网络字体依赖,构建不需要访问 Google Fonts

## 快速开始

### 运行起点(starter)

```bash
cd starter
npm install --no-audit --no-fund
npm run seed
npm run dev
```

打开 `http://localhost:3000`。起点只有布局、静态列表外观、最小 `articles` 表和 seed,缺口写在 `starter/TODO.md` 与代码里的 `TODO m1`–`TODO m8`。

### 运行参考实现(solution)

```bash
cd solution
npm install --no-audit --no-fund
npm run seed
npm run dev
```

一条命令版本:

```bash
cd solution && npm install --no-audit --no-fund && npm run seed && npm run dev
```

常用命令:

| 命令 | 作用 |
| --- | --- |
| `npm run dev` | 启动开发服务器 |
| `npm run seed` | 建库、迁移并导入 `../fixtures/articles.json`,已存在的文章跳过 |
| `npm run seed:reset` | 清空三张表后重新导入 |
| `npm run build` | 生产构建(starter 与 solution 都必须通过) |
| `npm run start` | 启动生产服务器 |
| `npm run typecheck` | TypeScript 检查 |

### 自动检查

```bash
# 在 p3-knowledge-site 根目录
node tests/check.mjs
```

脚本检查 solution 的页面、数据层、Server Actions、导出接口等 11 项特征,并用临时 SQLite 文件真实执行迁移和 seed;同时确认 starter 仍有缺口。详细走查步骤见 `tests/README.md`。

## 目录结构

```text
p3-knowledge-site/
├── README.md
├── starter/                         # 可启动起点
│   ├── app/
│   │   ├── layout.tsx
│   │   ├── page.tsx                 # 静态列表骨架 + TODO
│   │   └── globals.css
│   ├── lib/db.ts                    # 最小建表骨架
│   ├── scripts/seed.ts
│   ├── next.config.ts               # cacheComponents: false
│   ├── package.json
│   ├── TODO.md
│   └── tsconfig.json
├── solution/                        # 完整参考实现
│   ├── app/
│   │   ├── layout.tsx
│   │   ├── page.tsx                 # 列表 + 分类筛选 + 关键词搜索
│   │   ├── not-found.tsx
│   │   ├── articles/
│   │   │   ├── new/page.tsx         # 新建
│   │   │   └── [slug]/
│   │   │       ├── page.tsx         # 详情
│   │   │       ├── edit/page.tsx    # 编辑
│   │   │       └── versions/page.tsx# 版本历史与回滚
│   │   ├── archive/page.tsx         # 归档列表与恢复
│   │   ├── export/page.tsx          # 导出入口
│   │   └── api/export/
│   │       ├── route.ts             # JSON 下载
│   │       └── markdown/route.ts    # Markdown 下载(可单篇)
│   ├── components/
│   │   └── article-form.tsx         # useActionState 表单
│   ├── lib/
│   │   ├── db.ts                    # 连接、迁移、seed
│   │   ├── articles.ts              # 查询、写入、版本、导出
│   │   ├── validation.ts            # 表单校验
│   │   ├── actions.ts               # Server Actions
│   │   ├── form-state.ts            # 表单状态类型
│   │   └── format.ts                # 北京时间展示
│   ├── scripts/seed.ts
│   ├── next.config.ts
│   └── package.json
├── tests/
│   ├── check.mjs                    # 结构 / API 特征 + 临时数据库检查
│   └── README.md                    # 本机走查步骤
├── docs/
│   ├── data-model.md                # 三表结构、迁移、版本规则、职责
│   └── routes.md                    # 路由、Server Actions、缓存失效
└── fixtures/
    └── articles.json                # 3 个分类 + 3 篇示例文章
```

`node_modules/`、`.next/`、`data/*.db` 是运行产物,不提交。

## 实现要点

### 数据层

- `lib/db.ts`:`openDatabase()` 建目录、开 WAL、执行迁移;`migrate()` 用 `PRAGMA user_version` 记账;`seedDatabase()` 在事务里 upsert 分类、按 slug 幂等插入文章与 v1。
- `lib/articles.ts`:所有 SQL 集中在这里。列表查询用 `LIKE` 同时匹配标题、摘要、正文、标签;版本号用 `MAX(version_no) + 1`;回滚生成新版本而不是删除历史。
- 数据页统一 `export const dynamic = "force-dynamic"`,避免 `next build` 时把某一次数据库结果静态化。

### 写入与缓存

- `lib/actions.ts` 顶部 `"use server"`,表单通过 `useActionState` 接收 `{ status, message, errors, values }`。
- 每个写操作在 `redirect` 前调用 `revalidatePath`,覆盖 `/`、`/archive`、`/articles` layout、详情与版本页;slug 变化时补旧路径。
- 数据库负责持久化,路由缓存只负责复用渲染结果。不要把查询结果写回 JSON 文件当缓存。

### 版本与归档

- 新建:`articles` 一行 + `article_versions` v1。
- 编辑:更新当前行 + 追加 v2/v3/…,`change_note` 来自表单。
- 回滚:用旧快照覆盖当前行 + 追加「回滚到 vN」,历史只增不减。
- 归档:只改 `status`,首页与搜索隐藏,归档页可恢复;不产生版本。

## 验收清单(m1–m8)

| 里程碑 | 验收 |
| --- | --- |
| m1 路由骨架 | 列表、详情、新建、编辑、版本、归档、导出均可访问;不存在的文章进入 404 |
| m2 数据层 | 三张表 + `user_version` 迁移;`npm run seed` 写入 3 篇,重复执行跳过 |
| m3 列表检索 | 分类与关键词可组合,URL 保留 `q` / `category`,空状态可用 |
| m4 详情与新建 | 详情字段完整;表单校验与 slug 唯一性错误可见,成功后跳详情 |
| m5 编辑与版本 | 每次保存版本号 +1,版本页按倒序展示变更说明与正文预览 |
| m6 回滚 | 回滚后正文恢复,历史新增一条「回滚到 vN」,旧版本仍在 |
| m7 归档恢复 | 归档后首页/搜索消失、归档页出现;恢复后回到首页 |
| m8 缓存与导出 | 写后 `revalidatePath` 生效;JSON / Markdown 可下载;两个工程 build 通过 |

逐项手动走查命令与预期结果见 `tests/README.md`。

## 建议实现顺序

1. 先读 `docs/data-model.md`,把三张表和版本规则画出来。
2. 补 `lib/db.ts` 的迁移与 seed,用 `node tests/check.mjs` 看数据层是否达标。
3. 做列表与详情,让只读链路先跑通。
4. 接 Server Action 完成新建、编辑,再补版本历史与回滚。
5. 最后做归档、导出和 `revalidatePath`,跑 build 与完整走查。
数据模型
categories(id, slug, name, description) 保存分类;articles(id, slug, title, summary, content, category_id, tags, status, created_at, updated_at) 保存当前文章,status 取 active / archived;article_versions(id, article_id, version_no, title, summary, content, category_id, tags, change_note, created_at) 保存每次编辑的完整快照。articles 与 categories 是多对一,article_versions 与 articles 是多对一,同一文章内 version_no 唯一。新建文章写入 v1;每次编辑先更新 articles,再插入下一条版本;回滚不删除历史,而是用旧版本内容生成一条新版本。

Command Palette

Search for a command to run...