96 lines
5.8 KiB
Markdown
96 lines
5.8 KiB
Markdown
|
|
# 写博客 Blog(个人 Markdown 博客 + `#post/<id>` 路由)
|
|||
|
|
|
|||
|
|
登录后的个人博客:按登录用户隔离,可写 / 存 / 读 / 改 / 删 Markdown 文章。文章正文走
|
|||
|
|
Markdown 渲染,单篇文章有 `#post/<id>` 可分享路由(刷新/直链都能定位到该文章)。
|
|||
|
|
|
|||
|
|
**职责切分**:文章正文**按登录用户存服务器**(最多 200 篇/人、严格私有、JWT 鉴权),
|
|||
|
|
每篇一个 JSON 文件;`readingTime`/`excerpt` 等派生信息不入库,前端实时算。
|
|||
|
|
|
|||
|
|
设计方案见 [specs/2026-06-25-blog-writing-design.md](superpowers/specs/2026-06-25-blog-writing-design.md),
|
|||
|
|
实现计划见 [plans/2026-06-25-blog-writing.md](superpowers/plans/2026-06-25-blog-writing.md)。
|
|||
|
|
|
|||
|
|
## 入口与路由
|
|||
|
|
|
|||
|
|
博客页是开发者入口门(`dev_gate`)登录后的**默认页**(非 `#photo`、非特殊文件夹时)。
|
|||
|
|
列表↔文章的唯一真相源是 `App.tsx` 的地址栏 hash:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
浏览器(已登录,无 #photo / 文件夹 hash)
|
|||
|
|
└─ BlogPage:Hero + 文章列表
|
|||
|
|
点某篇文章 → 地址栏置 #post/<id>
|
|||
|
|
└─ App 解析 #post/<id> → 传 initialPostId 给 BlogPage → 渲染 PostArticle 全文
|
|||
|
|
未登录直接开 #post/<id> → 复用登录页,登录后落到该文章
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 列表↔文章由 hash 驱动(可分享、可刷新、浏览器后退回列表);**编辑器浮层不进 URL**
|
|||
|
|
(不可分享,符合设计)。
|
|||
|
|
- 文章页「编辑」保存后,靠 `articleNonce` 作 `PostArticle` 的 `key` 强制重挂载,
|
|||
|
|
保证显示最新内容(否则 id 不变其 fetch effect 不重跑)。
|
|||
|
|
|
|||
|
|
## 涉及文件
|
|||
|
|
|
|||
|
|
前端:
|
|||
|
|
- `Client/src/App.tsx` — 解析 `#post/<id>`:置 `postId`,传 `token`/`initialPostId` 给
|
|||
|
|
`BlogPage`;`#post/<id>` 未登录时也走登录页。
|
|||
|
|
- `Client/src/components/BlogPage.tsx` — 外壳(Hero / 三栏 / 页脚)+ 中心三态内容
|
|||
|
|
(列表 / 文章 / 编辑器),列表读真实数据,写死的占位数组已删除。
|
|||
|
|
- `Client/src/components/PostArticle.tsx` — 全文阅读页(Markdown 渲染、编辑入口、
|
|||
|
|
删除二次确认)。
|
|||
|
|
- `Client/src/components/PostEditor.tsx` — 写 / 改文章(标题 / 标签 / 正文 + Markdown 预览)。
|
|||
|
|
- `Client/src/components/Markdown.tsx` — `react-markdown` + `remark-gfm`,套站点暖色编辑风;
|
|||
|
|
默认不解析原始 HTML(未引 `rehype-raw`),天然防注入。
|
|||
|
|
- `Client/src/api/posts.ts` — 文章接口封装(见下)+ 派生工具 `readingTime`/`excerpt`。
|
|||
|
|
|
|||
|
|
后端(Rust):
|
|||
|
|
- `Server/src/posts.rs` — `PostStore`:按用户隔离到 `BLOG_DIR/<用户>/<hex>.json`,含
|
|||
|
|
路径安全(`valid_id` 仅小写 hex ≤32)、限额、`create` 加锁防并发越额;单测覆盖
|
|||
|
|
CRUD / 倒序 / 坏输入 / 上限 / 用户隔离 / 坏 id。
|
|||
|
|
- `Server/src/state.rs` — `AppState` 新增 `posts: PostStore` 字段。
|
|||
|
|
- `Server/src/routes/web.rs` — 文章接口(见下);CORS 放行 `PUT`。
|
|||
|
|
- `Server/src/main.rs` — `mod posts;`。
|
|||
|
|
- 复用 `Server/src/routes/extract.rs` 的 `AuthUser` 提取器、`photos::sanitize_user`。
|
|||
|
|
|
|||
|
|
## 接口(均经 `AuthUser` 鉴权,按用户隔离)
|
|||
|
|
|
|||
|
|
| 方法 | 路径 | 行为 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| GET | `/api/web/posts` | 列出当前用户文章(按 `createdAt` 倒序,`{ items:[Post], max:200 }`) |
|
|||
|
|
| POST | `/api/web/posts` | 新建(body `{ title, tag, body }`)→ 201 + `Post`;满 200 篇 409、超 64KB 413、标题空/标签过长 400 |
|
|||
|
|
| GET | `/api/web/posts/{id}` | 取单篇 `Post`(属主校验,非法/不存在 404) |
|
|||
|
|
| PUT | `/api/web/posts/{id}` | 更新 → 204(创建时间不变,更新时间刷新) |
|
|||
|
|
| DELETE | `/api/web/posts/{id}` | 删除 → 204(不存在 404) |
|
|||
|
|
|
|||
|
|
`Post` 字段 camelCase:`id, title, tag, body, createdAt, updatedAt`(秒级时间戳)。
|
|||
|
|
上限常量:`MAX_POSTS=200`、`MAX_BODY=64KiB`、标题 ≤200 字、标签 ≤40 字,标题 trim 后不得为空。
|
|||
|
|
|
|||
|
|
存储:`BLOG_DIR`(默认 `blog`,线上 `/opt/internet-project/blog/`)下每用户一子目录,
|
|||
|
|
每篇随机十六进制文件名 `<hex>.json`。读取已鉴权,文件名干无需密码学随机。
|
|||
|
|
|
|||
|
|
## 功能
|
|||
|
|
|
|||
|
|
- **写 / 改**:标题、标签、Markdown 正文;编辑器内「Write / Preview」切换实时预览。
|
|||
|
|
- **读**:全文 Markdown 渲染(标题/列表/引用/代码块/链接/图片/分隔线/GFM 表格等),
|
|||
|
|
顶部显示标签、日期、估算阅读时长。
|
|||
|
|
- **删**:文章页二次确认(「删除」→「确认删除」)。
|
|||
|
|
- **列表**:最新在前,每篇卡片显示日期、标签、摘要(剥除 Markdown 标记)、阅读时长;
|
|||
|
|
空态引导「写文章」。
|
|||
|
|
- **派生**:`readingTime`(~200 字/分钟,最少 1 分钟)、`excerpt`(剥标记取前 80 字)
|
|||
|
|
均前端实时算,不入库。
|
|||
|
|
|
|||
|
|
## 实现注记
|
|||
|
|
|
|||
|
|
- **路由真相源单一**:列表↔文章统一由 `App.tsx` 经 hash 决定(`initialPostId`),
|
|||
|
|
`BlogPage` 仅管编辑器浮层,避免双方争用 URL。
|
|||
|
|
- **就地编辑刷新**:`PostArticle` 的 fetch effect 依赖 `[token, id]`,id 不变时不重拉;
|
|||
|
|
故编辑保存后 bump `articleNonce` 并用作 `key` 强制重挂载。
|
|||
|
|
- **Markdown code 边界**:以 `language-` 类名区分行内/块级,未指定语言的围栏块按行内样式
|
|||
|
|
渲染(写 ` ```lang ` 即按块级)——个人博客可接受,不引额外判定增复杂度。
|
|||
|
|
- **防注入**:未引 `rehype-raw`,原始 HTML 不解析;正文区 `data-selectable` 放开选择以便复制。
|
|||
|
|
|
|||
|
|
## 线上访问
|
|||
|
|
|
|||
|
|
`http://8.130.143.54:8548/`(开发者入口门登录后即默认进博客页)。博客引入后端改动 →
|
|||
|
|
走**全量重部署**(重编 Rust → 传二进制 → `restart_rust.sh` 重启),非 frontend-only。
|
|||
|
|
存储目录 `BLOG_DIR` 默认 `blog`(即 `/opt/internet-project/blog/`),首次写文章自动建
|
|||
|
|
`blog/<用户>/`。复用现有 `weather.env` 的 `DATABASE_URL`(登录校验)与 `JWT_SECRET`(token 签名)。
|