Files
Sakurasan fbda293072 余白 UI:按设计稿模块化还原 + 站主自定义 CSS + 详情 neighbors + 静态缓存头
前端(新增 ui/yohaku/,各视图按 ui_id=vivid 分支渲染;classic 与后台不受影响)
- 令牌层:设计稿数值原样落地,28 个 --v-* 为站主接口(自定义 CSS 可覆盖,
  放 @layer 让出优先级);另加防线,中和 styles.css 裸选择器
  (h1 衬线 / body 行高 / 后台 .stat/.toolbar/.chip 卡片)漏进余白元素
- 顶栏(滚动收缩态)/ 阅读进度(顶部 2px + READ n%)/ 页脚
- 首页:hero(统计条)/ 筛选(类型分段 + 标签 chips + 搜索)/
  长文卡片与短文纸条 / 分页 / 作品展示区(3 件 + 查看所有,参照 diygod.cc);
  面板栅格改 1.6fr 1fr 1fr 且高度自适应内容(标签云明显大一圈)
- 文章页:三栏「纸」—— 左时间线(前后各两篇)/ 中纸张 / 右目录与阅读进度;
  短文详情不显示标题(h1 视觉隐藏保留无障碍)
- 全部规则收敛在 html[data-ui='vivid'] 前缀下;无 !important

后端
- GET /api/posts/:slug 增 neighbors(前后各两篇,详情页侧栏时间线用)
- SPA 缓存头:index.html no-cache,assets/ 带 hash 长期 immutable
  (此前无缓存头,浏览器会一直拿旧构建)

其他
- 站主自定义 CSS 管线(customCss.js + ui/sections.js,按分区注入,仅 vivid)
- 删除旧 vivid 覆盖层实现(VividNav/Hero/Footer/Timeline/Toc、ui/vivid.css)
- 设计稿原型附于仓库根 index.html;作品表清掉外链 diygod.cc 的示例封面,
  改用本地占位图(public/covers/)
2026-09-23 15:37:42 +08:00

223 lines
11 KiB
Markdown
Raw Permalink 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.
# ONE · 一个博客
一个能跑通全流程的博客 MVP:后台登录 → 写文章(长文 / 短文)→ 发布 → 前台按「07 融合 + Twitter 信息流」风格展示。
- 后端:Go(`net/http` + `database/sql`,SQLite / PostgreSQL 双支持),在 `backend/`
- 前台 + 后台:Vue 3 + Vue Router + Vite,在 `frontend/`(pnpm 管理依赖)
- Markdown:服务端用 `goldmark` 渲染入库,编辑器实时预览用 `marked` + `DOMPurify`
## 一条命令跑起来
```bash
make dev # 后端 8080 + 前端 3000(前端带热更新,已配好 /api 代理)
```
打开 http://localhost:3000 是前台,http://localhost:3000/admin 是后台。
停掉:`make stop`
想要单端口(前端构建产物由 Go 直接托管,接近生产形态):
```bash
make start # 先 pnpm run build,再起 Go,只开 8080
```
打开 http://localhost:8080 。
其它:`make web` 只构建前端、`make server` 只起后端、`make test` 跑后端测试、`make clean` 清构建产物、`make db-reset` 清空本地 SQLite 数据。
## 后台登录
默认账号密码来自环境变量,未设置时是 `admin` / `admin`(启动日志会打印出来):
```bash
ONE_ADMIN_USER=admin ONE_ADMIN_PASSWORD=换一个 make start
```
登录态是服务端签发的 httpOnly cookie(HMAC-SHA256,7 天有效),同时支持 `Authorization: Bearer <token>`,方便用 curl / 脚本写文章。
## 环境变量
| 变量 | 默认 | 说明 |
| --- | --- | --- |
| `ONE_ADDR` | `:8080` | 监听地址 |
| `ONE_DB_DRIVER` | `sqlite` | `sqlite` 或 `postgres` |
| `ONE_DB_DSN` | `./data/one.db` | 数据库连接串(Postgres 示例:`postgres://user:pass@localhost/one?sslmode=disable`) |
| `ONE_ADMIN_USER` | `admin` | 后台用户名 |
| `ONE_ADMIN_PASSWORD` | `admin` | 后台密码 |
| `ONE_SECRET` | 随机生成 | 会话签名密钥;不设的话重启后登录态失效 |
| `ONE_SITE_URL` | `http://localhost:8080` | RSS 里的站点地址,部署时务必改成真实域名 |
| `ONE_WEB_DIST` | `./frontend/dist` | 前端构建产物目录 |
| `ONE_DATA_DIR` | `./data` | SQLite 数据目录 |
## 界面:classic / vivid
前台有两套完整界面,都保留,在后台 **设置 → 界面与自定义** 里整站切换(站点级设置,所有访客同款)。
后台自身的外观不受影响。
| `ui_id` | 说明 |
| --- | --- |
| `classic`(默认) | 原来的极简版:三栏骨架(左导航 / 正文 / 右栏)、纸感分区线、衬线标题、首字下沉 |
| `vivid` | 顶部粘性导航 + 单栏卡片流、渐变配色与背景网格、悬浮卡片、首页 hero;支持站主自定义 CSS |
实现要点,改这套东西前先读:
- `ui_id` 写在 `<html data-ui>` 上,由 `frontend/src/site.js` 的 `applyTheme()` 设置。
`index.html` 里有一小段 inline script 先读 `localStorage['one.ui']` 设好,避免首帧闪一下 classic。
- vivid 的皮肤**不复用** paper/sage/rose,而是让 `data-theme` 取 `vivid` / `vivid-dark`
(明暗仍由访客的 themeMode 决定)。这样两套调色板不会互相打架。
- `frontend/src/ui/vivid.css` 是**纯覆盖层**,在 `main.ts` 里排在 `styles.css` 之后。
**每个选择器都必须以 `html[data-ui="vivid"]` 开头**:scoped 样式编译后是 `.foo[data-v-HASH]`
(特指度 0,2,0),而 `html[data-ui="vivid"] .foo` 是 0,2,1 才能稳定取胜;scoped 样式是懒加载 chunk,
在入口 CSS 之后才到位,漏了前缀会变成「有时生效有时不生效」。复合选择器要**整条链**镜像
(如 `.card:hover .layer-front` 要写全,不能只前缀第一段)。该文件里不允许出现 `!important`。
- vivid 把 classic 那 12 个 token 重新赋值(`--paper/--card/--ink/--accent/...`),
所以读 token 的共享组件零改动就换了皮。vivid 自己的变量以 `--v-` 开头。
- 顶部导航与首页 hero 是新增组件(`VividNav.vue` / `VividHero.vue`),由 `App.vue` 在
`ui_id === 'vivid' && !isAdmin` 时渲染;`LeftNav.vue` / `RightRail.vue` 在 vivid 下自身不渲染,
7 个公开视图没有任何改动。
### 站主自定义 CSS
按分区分段写,存在 settings 的 `custom_css`(一行 JSON,`{分区: CSS}`)。
```
global 全局 —— 每个前台页面都会加载,放变量和通用微调
home 首页(含标签筛选)
post 文章详情(含短文)
archive 归档
tags 标签云 + 单个标签下的列表
projects 作品
about 关于
```
分区 id 必须与 `backend/internal/store/store.go` 的 `ValidCSSSections`
和 `frontend/src/ui/sections.js` 的 `SECTIONS` 三处保持一致,否则会出现「保存了但不生效」。
- **只在 `ui_id = vivid` 时注入**,并且 `/admin` 下一律清空 —— 写坏样式影响不到后台,classic 也完全不受影响。
- 注入方式是两个 `<style>`(`#one-css-global` 常驻、`#one-css-section` 跟路由换),只写 `textContent`,
不是 `innerHTML`,所以 CSS 里写 `</style>` 也逃不出去,不需要 CSS 清洗库。
- 上限:单个分区 16 KiB,全部合计 64 KiB。超限的分区会被**静默丢弃**(写的时候就不落库)。
- 后台保存是**整对象 PUT**:请求里没带的字段会变成零值。用 curl 写的时候记得把其他字段一起带上。
可覆盖的变量(写进「全局」段即可整站生效):`--v-accent`、`--v-accent-2`、`--v-accent-3`、
`--v-bg`、`--v-surface`、`--v-surface-2`、`--v-text`、`--v-text-soft`、`--v-muted`、`--v-line`、
`--v-radius`、`--v-radius-pill`、`--v-content-width`、`--v-measure`、`--v-motion`(0 = 关掉全部动效)。
后台「界面与自定义」里内置了几个一键插入的片段(换主色 / 方正圆角 / 关渐变背景 / 首页单列),
它们只是往文本框里塞文本,不落库,插进去还能接着改。
> 注意:目前若将来给站点加 CSP,运行时的 `<style>` 注入需要 `style-src 'unsafe-inline'` 或一个 nonce。
## 目录结构
```
backend/ Go 后端(模块名 oneblog)
main.go 路由装配、静态托管 SPA、优雅退出
internal/config 环境变量
internal/db SQLite / PostgreSQL 连接与 `?` → `$n` 占位符重写
internal/model 数据结构
internal/store schema 迁移 + 全部 SQL
internal/render goldmark 渲染、阅读时长估算、摘要截取
internal/api 公开接口 + RSS(/api/*、/rss.xml)
internal/admin 后台接口与登录鉴权(/api/admin/*)
internal/httpx JSON 读写助手
frontend/ Vue 3 前端(pnpm)
src/styles.css classic 的设计 token 与正文排版(07 风格)
src/ui/vivid.css 第二套 UI(vivid)的覆盖层,全部以 html[data-ui="vivid"] 前缀收敛
src/ui/sections.js 自定义 CSS 的分区清单(与后端 ValidCSSSections 对应)
src/customCss.js 自定义 CSS 的注入管线(按路由切分区,/admin 下清空)
src/site.js 站点设置 + 明暗 / 皮肤 / UI 三个轴的运行时
src/views 前台:时间线 / 详情 / 归档 / 标签 / 作品 / 关于
src/admin 后台:登录 / 列表 / 编辑器 / 标签 / 作品 / 设置
src/components 左栏导航(classic)、右栏卡片(classic)、时间线行、vivid 导航与 hero
```
原有那一版 Vue 前端(07 风格重写之前)保留在 git 历史的第一个 commit `edee708` 里,
需要对照或回滚:`git checkout edee708 -- frontend/src`。
## 接口
公开(无需登录):
```
GET /api/site 站点设置
GET /api/posts?kind=&tag=&q=&page=&size= 已发布文章(kind: long|short)
GET /api/posts/:slug 文章详情
GET /api/archive 按年 → 月分组
GET /api/tags 标签与计数
GET /rss.xml (/feed 同) RSS 2.0
```
后台(需登录):
```
POST /api/admin/login {username,password} → {token}
POST /api/admin/logout
GET /api/admin/me
GET /api/admin/posts?status=&kind=&q=&page=&size=
POST /api/admin/posts 新建
GET /api/admin/posts/:id
PUT /api/admin/posts/:id 更新(编辑器自动保存走这里)
DELETE /api/admin/posts/:id
GET /api/admin/tags
POST /api/admin/tags {name}
PUT /api/admin/tags/:id {name}
DELETE /api/admin/tags/:id
GET /api/admin/settings
PUT /api/admin/settings
```
`Post` 的关键字段:`kind`(`long` / `short`)、`title`、`slug`、`summary`、`content_md`、`content_html`、`status`、`published_at`、`reading_minutes`、`tags[]`。
短文没有可见标题,但 `title` 仍会由正文首句生成,供归档与标签列表索引。
## 部署
生产形态是「一个二进制 + 一个静态目录」:
```bash
make build # 产出 ./one-server,前端已打进 web/dist
ONE_ADDR=:8080 \
ONE_SITE_URL=https://your.domain \
ONE_DB_DRIVER=postgres \
ONE_DB_DSN="postgres://user:pass@127.0.0.1/one?sslmode=disable" \
ONE_SECRET=一串随机长字符串 \
ONE_ADMIN_PASSWORD=强密码 \
./one-server
```
要点:
- **静态资源**:Go 直接托管 `web/dist`,未知路径回落到 `index.html`,所以 `/post/xxx`、`/admin` 刷新都不会 404。
- **反向代理**:前面挂 Nginx / Caddy 时把 `/` 反代到 `ONE_ADDR` 即可;如果只在内网监听,可以不挂。
- **SQLite 部署**:把 `data/` 挂到持久卷;并发写很低的博客足够用(已开 WAL + busy_timeout)。
- **PostgreSQL 部署**:改 `ONE_DB_DRIVER` 与 `ONE_DB_DSN`,schema 会在启动时自动创建,无需手动迁移。
- **systemd 示例**:
```ini
[Unit]
Description=ONE blog
After=network.target
[Service]
WorkingDirectory=/opt/one
Environment=ONE_ADDR=:8080 ONE_SITE_URL=https://your.domain ONE_SECRET=xxx ONE_ADMIN_PASSWORD=xxx
ExecStart=/opt/one/one-server
Restart=always
[Install]
WantedBy=multi-user.target
```
- **备份**:SQLite 直接拷 `data/one.db`(停写更稳);Postgres 用 `pg_dump`。
## 已验证
`make start` 后:后台登录 → 写长文与短文 → 发布 → 前台时间线(长文标题 + 摘要 / 短文正文铺开)、详情、归档、标签、RSS 全部可见;
编辑器改正文 1.5 秒后自动保存到服务端;桌面与移动宽度无横向滚动;SQLite 路径全流程跑通,PostgreSQL 的占位符重写有单元测试覆盖(本机没有 Postgres 实例,未做端到端验证)。
## 待确认
- 仓库地址:本仓库是在本地 `/Users/cjun/Code/one` 新建的 git 仓库,还没有 remote。给个地址就能推上去并绑进工作区。
- 鉴权方式:目前是账号密码 + 签名 cookie。想改成纯 token 也可以,接口已经兼容 `Authorization: Bearer`。
- 图片上传:MVP 未做,正文里先用图片外链。