PersonalWebApplication/docs/blog.md
cc edde910cfe docs(blog): 写博客功能文档 + 计划标记完成 + 部署说明
- 新增 docs/blog.md:功能文档(入口/路由、涉及文件、接口、实现注记、上线)
- plans/2026-06-25-blog-writing.md:29 个任务步骤全部勾选完成,加状态/偏差说明
- plans/2026-06-18-deploy.md:补写博客全量重部署说明(BLOG_DIR 等)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:17:43 +08:00

96 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 写博客 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
└─ BlogPageHero + 文章列表
点某篇文章 → 地址栏置 #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 签名)。