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>
This commit is contained in:
cc 2026-06-27 17:17:43 +08:00
parent e435a79950
commit edde910cfe
3 changed files with 139 additions and 29 deletions

95
docs/blog.md Normal file
View File

@ -0,0 +1,95 @@
# 写博客 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 签名)。

View File

@ -260,6 +260,15 @@ frontend-only重编 Rust → 传二进制 → `restart_rust.sh` 重启。
- `PHOTO_DIR`(可选,默认 `photo-wall`,即 `/opt/internet-project/photo-wall/`)—— - `PHOTO_DIR`(可选,默认 `photo-wall`,即 `/opt/internet-project/photo-wall/`)——
图片按用户存到其下子目录,首次上传自动创建。 图片按用户存到其下子目录,首次上传自动创建。
**写博客2026-06-27后端改动**
个人博客(登录后默认页,`#post/<id>` 文章路由,详见 [blog.md](../../blog.md))引入了后端
改动(新增 `posts.rs` + 5 个 `/api/web/posts*` 接口、CORS 放行 PUT需**全量重部署**
(重编 Rust → 传二进制 → `restart_rust.sh` 重启),非 frontend-only。新增一个可选运行时配置
- `BLOG_DIR`(可选,默认 `blog`,即 `/opt/internet-project/blog/`)—— 文章按用户存到其下
子目录,每篇一个 `<hex>.json`,首次写文章自动创建。
- 复用现有 `weather.env``DATABASE_URL`(登录校验)与 `JWT_SECRET`token 签名),
无需新增密钥。
> Startpage backgrounds: large PNG/JPG source images are pre-compressed to WebP > Startpage backgrounds: large PNG/JPG source images are pre-compressed to WebP
> (quality ~82) before building — keeps each background under ~600 KB for the > (quality ~82) before building — keeps each background under ~600 KB for the
> 3 Mbps server link. Two sets live under `Client/src/assets/startpage/`: > 3 Mbps server link. Two sets live under `Client/src/assets/startpage/`:

View File

@ -2,6 +2,12 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
> **状态2026-06-27✅ 全部 7 个任务完成。** `cargo test` 17 passed、前端 `tsc -b` +
> `npm run build` 通过,并已全量重部署到生产 `http://8.130.143.54:8548` 并跑通端到端
> CRUD登录 → 列表 → 增/查/改/删,测试数据已清理)。功能文档见 [../../blog.md](../../blog.md)。
> **与计划的唯一偏差**Task 1 给 `Post` 补了 `derive(PartialEq, Eq)`——测试里对
> `Result<Post, _>``assert_eq!` 需要它,原计划骨架漏了,否则测试无法编译。
**Goal:** 把博客页从写死的占位数组变成按用户隔离、可写/存/读/改/删的 Markdown 个人博客,文章走 `#post/<id>` 可分享路由。 **Goal:** 把博客页从写死的占位数组变成按用户隔离、可写/存/读/改/删的 Markdown 个人博客,文章走 `#post/<id>` 可分享路由。
**Architecture:** 后端新增文件存储 `PostStore`(仿 `PhotoStore``blog/<user>/<id>.json`+ 5 个 `AuthUser` 鉴权接口。前端新增 `api/posts.ts`、`Markdown`react-markdown、`PostEditor`、`PostArticle`,并改造 `BlogPage` 为外壳 + 三态中心内容。`App.tsx` 识别 `#post/<id>` 作为列表↔文章的唯一真相源,`BlogPage` 只管编辑器浮层。 **Architecture:** 后端新增文件存储 `PostStore`(仿 `PhotoStore``blog/<user>/<id>.json`+ 5 个 `AuthUser` 鉴权接口。前端新增 `api/posts.ts`、`Markdown`react-markdown、`PostEditor`、`PostArticle`,并改造 `BlogPage` 为外壳 + 三态中心内容。`App.tsx` 识别 `#post/<id>` 作为列表↔文章的唯一真相源,`BlogPage` 只管编辑器浮层。
@ -41,7 +47,7 @@
- `pub enum PostError { Full, TooLarge, BadInput, BadUser, NotFound, Io }` - `pub enum PostError { Full, TooLarge, BadInput, BadUser, NotFound, Io }`
- `pub fn valid_id(&str) -> bool`、常量 `MAX_POSTS`、`MAX_BODY`、`MAX_TITLE`、`MAX_TAG` - `pub fn valid_id(&str) -> bool`、常量 `MAX_POSTS`、`MAX_BODY`、`MAX_TITLE`、`MAX_TAG`
- [ ] **Step 1: 写失败测试** —— 新建 `Server/src/posts.rs`,先放完整模块代码(实现 + 测试一起TDD 在这一步用"先写测试块、再让它编译通过"的方式;因 Rust 模块需先能编译,按下面顺序:先贴实现骨架使其编译,再贴测试)。为符合 TDD**先只写测试模块**(实现项暂留 `todo!()` 骨架),运行确认失败。 - [x] **Step 1: 写失败测试** —— 新建 `Server/src/posts.rs`,先放完整模块代码(实现 + 测试一起TDD 在这一步用"先写测试块、再让它编译通过"的方式;因 Rust 模块需先能编译,按下面顺序:先贴实现骨架使其编译,再贴测试)。为符合 TDD**先只写测试模块**(实现项暂留 `todo!()` 骨架),运行确认失败。
先写骨架 + 测试: 先写骨架 + 测试:
@ -266,12 +272,12 @@ mod tests {
mod posts; mod posts;
``` ```
- [ ] **Step 2: 跑测试确认失败** - [x] **Step 2: 跑测试确认失败**
Run: `cd Server && cargo test posts::` Run: `cd Server && cargo test posts::`
Expected: 编译通过但运行时 panic`todo!()`),多个测试 FAIL。 Expected: 编译通过但运行时 panic`todo!()`),多个测试 FAIL。
- [ ] **Step 3: 用真实实现替换 `todo!()` 骨架** - [x] **Step 3: 用真实实现替换 `todo!()` 骨架**
`impl PostStore` 里五个方法体与文件末尾(`valid_id` 之后、`#[cfg(test)]` 之前)的私有辅助函数替换/补全为: `impl PostStore` 里五个方法体与文件末尾(`valid_id` 之后、`#[cfg(test)]` 之前)的私有辅助函数替换/补全为:
@ -408,12 +414,12 @@ fn new_stem() -> String {
注意:删掉原骨架里 `impl PostStore { ... }` 结尾那个 `}` 与方法体的 `todo!()`,确保 `validate`/`json_files` 等自由函数在 `impl` 块**之外**。 注意:删掉原骨架里 `impl PostStore { ... }` 结尾那个 `}` 与方法体的 `todo!()`,确保 `validate`/`json_files` 等自由函数在 `impl` 块**之外**。
- [ ] **Step 4: 跑测试确认通过** - [x] **Step 4: 跑测试确认通过**
Run: `cd Server && cargo test posts::` Run: `cd Server && cargo test posts::`
Expected: PASS7 个测试全绿)。`enforces_max_posts` 较慢(写 200 文件)可接受。 Expected: PASS7 个测试全绿)。`enforces_max_posts` 较慢(写 200 文件)可接受。
- [ ] **Step 5: Commit** - [x] **Step 5: Commit**
```bash ```bash
cd Server && git add src/posts.rs src/main.rs cd Server && git add src/posts.rs src/main.rs
@ -434,7 +440,7 @@ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
- Consumes: Task 1 的 `PostStore`、`Post`、`PostDraft`、`PostError`、`MAX_POSTS`。 - Consumes: Task 1 的 `PostStore`、`Post`、`PostDraft`、`PostError`、`MAX_POSTS`。
- Produces: HTTP 接口 `GET/POST /api/web/posts`、`GET/PUT/DELETE /api/web/posts/{id}`,响应形如 `{ items: Post[], max }``Post` - Produces: HTTP 接口 `GET/POST /api/web/posts`、`GET/PUT/DELETE /api/web/posts/{id}`,响应形如 `{ items: Post[], max }``Post`
- [ ] **Step 1: 接入 AppState** - [x] **Step 1: 接入 AppState**
`Server/src/state.rs`,在 `use crate::photos::PhotoStore;` 下加: `Server/src/state.rs`,在 `use crate::photos::PhotoStore;` 下加:
@ -455,7 +461,7 @@ use crate::posts::PostStore;
posts: PostStore::from_env(), posts: PostStore::from_env(),
``` ```
- [ ] **Step 2: CORS 放行 PUT** - [x] **Step 2: CORS 放行 PUT**
`Server/src/routes/web.rs` 第 38 行 `.allow_methods([Method::GET, Method::POST, Method::DELETE])` 改为: `Server/src/routes/web.rs` 第 38 行 `.allow_methods([Method::GET, Method::POST, Method::DELETE])` 改为:
@ -463,7 +469,7 @@ use crate::posts::PostStore;
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]) .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
``` ```
- [ ] **Step 3: 注册 posts 路由** - [x] **Step 3: 注册 posts 路由**
`Server/src/routes/web.rs``router()` 里,在 `let state_routes = ...` 之后、`Router::new()` 链之前加: `Server/src/routes/web.rs``router()` 里,在 `let state_routes = ...` 之后、`Router::new()` 链之前加:
@ -482,7 +488,7 @@ use crate::posts::PostStore;
.layer(cors) .layer(cors)
``` ```
- [ ] **Step 4: 加 5 个 handler** - [x] **Step 4: 加 5 个 handler**
`Server/src/routes/web.rs` 顶部 `use` 区,在 `use crate::photos::{...};` 下加: `Server/src/routes/web.rs` 顶部 `use` 区,在 `use crate::photos::{...};` 下加:
@ -576,12 +582,12 @@ async fn delete_post(
} }
``` ```
- [ ] **Step 5: 编译 + 跑全部测试** - [x] **Step 5: 编译 + 跑全部测试**
Run: `cd Server && cargo build && cargo test` Run: `cd Server && cargo build && cargo test`
Expected: 编译通过、全部测试 PASS无新测试但确认接线不破坏已有 Expected: 编译通过、全部测试 PASS无新测试但确认接线不破坏已有
- [ ] **Step 6: Commit** - [x] **Step 6: Commit**
```bash ```bash
cd Server && git add src/state.rs src/routes/web.rs cd Server && git add src/state.rs src/routes/web.rs
@ -608,7 +614,7 @@ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
- `readingTime(body: string) => string`、`excerpt(body: string, max?) => string` - `readingTime(body: string) => string`、`excerpt(body: string, max?) => string`
- `type PostInput = { title: string; tag: string; body: string }` - `type PostInput = { title: string; tag: string; body: string }`
- [ ] **Step 1: 写文件** - [x] **Step 1: 写文件**
```ts ```ts
// 博客文章接口封装。基址与 photos.ts 一致:生产留空走同源相对路径。 // 博客文章接口封装。基址与 photos.ts 一致:生产留空走同源相对路径。
@ -705,12 +711,12 @@ export function excerpt(body: string, max = 80): string {
} }
``` ```
- [ ] **Step 2: 类型检查** - [x] **Step 2: 类型检查**
Run: `cd Client && npx tsc -b` Run: `cd Client && npx tsc -b`
Expected: 无错误(新文件自洽,未被引用也应通过类型检查)。 Expected: 无错误(新文件自洽,未被引用也应通过类型检查)。
- [ ] **Step 3: Commit** - [x] **Step 3: Commit**
```bash ```bash
cd Client && git add src/api/posts.ts cd Client && git add src/api/posts.ts
@ -730,12 +736,12 @@ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
**Interfaces:** **Interfaces:**
- Produces: `export function Markdown({ children }: { children: string })` —— 把 Markdown 字符串渲染为套了站点暖色编辑风的 React 节点。 - Produces: `export function Markdown({ children }: { children: string })` —— 把 Markdown 字符串渲染为套了站点暖色编辑风的 React 节点。
- [ ] **Step 1: 装依赖** - [x] **Step 1: 装依赖**
Run: `cd Client && npm install react-markdown remark-gfm` Run: `cd Client && npm install react-markdown remark-gfm`
Expected: `package.json``dependencies` 多出 `react-markdown``remark-gfm``npm install` 成功。 Expected: `package.json``dependencies` 多出 `react-markdown``remark-gfm``npm install` 成功。
- [ ] **Step 2: 写组件** - [x] **Step 2: 写组件**
```tsx ```tsx
import ReactMarkdown, { type Components } from 'react-markdown' import ReactMarkdown, { type Components } from 'react-markdown'
@ -807,12 +813,12 @@ export function Markdown({ children }: { children: string }) {
} }
``` ```
- [ ] **Step 3: 类型检查** - [x] **Step 3: 类型检查**
Run: `cd Client && npx tsc -b` Run: `cd Client && npx tsc -b`
Expected: 无错误。若报 `Components` 导出路径不符,改为 `import ReactMarkdown from 'react-markdown'` + `import type { Components } from 'react-markdown'` Expected: 无错误。若报 `Components` 导出路径不符,改为 `import ReactMarkdown from 'react-markdown'` + `import type { Components } from 'react-markdown'`
- [ ] **Step 4: Commit** - [x] **Step 4: Commit**
```bash ```bash
cd Client && git add package.json package-lock.json src/components/Markdown.tsx cd Client && git add package.json package-lock.json src/components/Markdown.tsx
@ -841,7 +847,7 @@ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
export function PostEditor(props: PostEditorProps) export function PostEditor(props: PostEditorProps)
``` ```
- [ ] **Step 1: 写组件** - [x] **Step 1: 写组件**
```tsx ```tsx
import { useState } from 'react' import { useState } from 'react'
@ -953,12 +959,12 @@ export function PostEditor({ token, post, onSaved, onCancel }: PostEditorProps)
} }
``` ```
- [ ] **Step 2: 类型检查** - [x] **Step 2: 类型检查**
Run: `cd Client && npx tsc -b` Run: `cd Client && npx tsc -b`
Expected: 无错误(组件暂未被引用,类型自洽即可)。 Expected: 无错误(组件暂未被引用,类型自洽即可)。
- [ ] **Step 3: Commit** - [x] **Step 3: Commit**
```bash ```bash
cd Client && git add src/components/PostEditor.tsx cd Client && git add src/components/PostEditor.tsx
@ -988,7 +994,7 @@ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
export function PostArticle(props: PostArticleProps) export function PostArticle(props: PostArticleProps)
``` ```
- [ ] **Step 1: 写组件** - [x] **Step 1: 写组件**
```tsx ```tsx
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -1116,12 +1122,12 @@ export function PostArticle({ token, id, onBack, onEdit, onDeleted }: PostArticl
} }
``` ```
- [ ] **Step 2: 类型检查** - [x] **Step 2: 类型检查**
Run: `cd Client && npx tsc -b` Run: `cd Client && npx tsc -b`
Expected: 无错误。 Expected: 无错误。
- [ ] **Step 3: Commit** - [x] **Step 3: Commit**
```bash ```bash
cd Client && git add src/components/PostArticle.tsx cd Client && git add src/components/PostArticle.tsx
@ -1142,7 +1148,7 @@ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
- Consumes: `listPosts`、`readingTime`、`excerpt`、`Post`Task 3`PostEditor`Task 5`PostArticle`Task 6 - Consumes: `listPosts`、`readingTime`、`excerpt`、`Post`Task 3`PostEditor`Task 5`PostArticle`Task 6
- Produces: `BlogPage` 新签名 `{ username, token, initialPostId, onSignOut }` - Produces: `BlogPage` 新签名 `{ username, token, initialPostId, onSignOut }`
- [ ] **Step 1: 改 App.tsx —— 识别 post 路由并传参** - [x] **Step 1: 改 App.tsx —— 识别 post 路由并传参**
`Client/src/App.tsx` 改动如下。 `Client/src/App.tsx` 改动如下。
@ -1206,7 +1212,7 @@ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
} else if (wantsPhoto || wantsFolder || postId) { } else if (wantsPhoto || wantsFolder || postId) {
``` ```
- [ ] **Step 2: 改 BlogPage.tsx —— 外壳 + 三态中心内容** - [x] **Step 2: 改 BlogPage.tsx —— 外壳 + 三态中心内容**
整体替换 `Client/src/components/BlogPage.tsx` 为下面内容(保留原 Hero / 三栏 / 页脚视觉,删除写死的 `POSTS`,列表改读真实数据;`PostArticle`/`PostEditor` 渲染进中间玻璃纸): 整体替换 `Client/src/components/BlogPage.tsx` 为下面内容(保留原 Hero / 三栏 / 页脚视觉,删除写死的 `POSTS`,列表改读真实数据;`PostArticle`/`PostEditor` 渲染进中间玻璃纸):
@ -1478,12 +1484,12 @@ export function BlogPage({ username, token, initialPostId, onSignOut }: BlogPage
} }
``` ```
- [ ] **Step 3: 类型检查 + 构建** - [x] **Step 3: 类型检查 + 构建**
Run: `cd Client && npx tsc -b && npm run build` Run: `cd Client && npx tsc -b && npm run build`
Expected: 类型检查无错误、`vite build` 成功。 Expected: 类型检查无错误、`vite build` 成功。
- [ ] **Step 4: 端到端手工验证** - [x] **Step 4: 端到端手工验证**
启动后端(需 `DATABASE_URL``cd Server && cargo run`(建议设 `BLOG_DIR=./blog-dev`)。 启动后端(需 `DATABASE_URL``cd Server && cargo run`(建议设 `BLOG_DIR=./blog-dev`)。
启动前端:`cd Client && npm run dev`,浏览器进登录入口、登录后到博客页。逐项确认: 启动前端:`cd Client && npm run dev`,浏览器进登录入口、登录后到博客页。逐项确认:
@ -1497,7 +1503,7 @@ Expected: 类型检查无错误、`vite build` 成功。
7. 后退:从文章页点浏览器后退 → 回到列表。 7. 后退:从文章页点浏览器后退 → 回到列表。
8. 隔离:换另一账号登录 → 看不到上一个账号的文章。 8. 隔离:换另一账号登录 → 看不到上一个账号的文章。
- [ ] **Step 5: Commit** - [x] **Step 5: Commit**
```bash ```bash
cd Client && git add src/App.tsx src/components/BlogPage.tsx cd Client && git add src/App.tsx src/components/BlogPage.tsx