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

ONE · 一个博客

一个能跑通全流程的博客 MVP:后台登录 → 写文章(长文 / 短文)→ 发布 → 前台按「07 融合 + Twitter 信息流」风格展示。

  • 后端:Go(net/http + database/sql,SQLite / PostgreSQL 双支持),在 backend/
  • 前台 + 后台:Vue 3 + Vue Router + Vite,在 frontend/(pnpm 管理依赖)
  • Markdown:服务端用 goldmark 渲染入库,编辑器实时预览用 marked + DOMPurify

一条命令跑起来

make dev        # 后端 8080 + 前端 3000(前端带热更新,已配好 /api 代理)

打开 http://localhost:3000 是前台,http://localhost:3000/admin 是后台。 停掉:make stop

想要单端口(前端构建产物由 Go 直接托管,接近生产形态):

make start      # 先 pnpm run build,再起 Go,只开 8080

打开 http://localhost:8080 。

其它:make web 只构建前端、make server 只起后端、make test 跑后端测试、make clean 清构建产物、make db-reset 清空本地 SQLite 数据。

后台登录

默认账号密码来自环境变量,未设置时是 admin / admin(启动日志会打印出来):

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 仍会由正文首句生成,供归档与标签列表索引。

部署

生产形态是「一个二进制 + 一个静态目录」:

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 示例:
[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 未做,正文里先用图片外链。
S
Description
No description provided
Readme
683 KiB
Languages
Vue 38.9%
CSS 20.1%
Go 19.6%
HTML 11.1%
JavaScript 9.4%
Other 0.9%