前端(新增 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/)
223 lines
11 KiB
Markdown
223 lines
11 KiB
Markdown
# 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 未做,正文里先用图片外链。
|