一个非营利的独立博客索引,以及它背后的一点设计取舍与技术实现。站点地址:万界书简 - 来自不同世界的信
为什么又做了一个站
独立博客最难的不是写,是被看见。
你写得再好,只要没有推荐算法的入口,就只能在几个熟人的 RSS 阅读器里流转。而一旦投到平台上,流量是平台的,排版是平台的,哪天平台改规则,你连搬家的力气都不想出。
万界书简想做的,是把「被看见」这件事,从算法手里拿回来一小块:
- 不产出内容,只做索引。每一封书简点开,都是作者的原文,流量、版权、评论都留在作者自己的站点。
- 不做推荐。没有热度榜,没有「猜你喜欢」,没有付费收录。你今天看到什么,只取决于谁最近写了新东西。
- 不做镜像。不改一个字,不搬一张图,只提供一个「去哪里读」的入口。
目前收录了九十多位写信人、三千多封书简。数字不大,但每一个都是真实的、还在更新的独立站点。
名字:世界与信
万界——每个博客都是一个自成一体的世界。有自己的域名、自己的排版、自己的语气,甚至自己的时间流速。有人日更,有人半年才写一篇,但那个世界一直亮着。
书简——他们从自己的世界里寄出来的信。我们不做内容农场,只做邮局。
这套命名不是修辞装饰,它决定了整站的语言。站内没有「博主」「文章」「推荐」这些平台词,取而代之的是一整套自己的词表:
| 通用说法 |
这里叫 |
| 博主 / 作者 |
写信人 |
| 文章 / 帖子 |
书简 |
| 首页推荐 |
投递 |
| 随机打开一篇 |
拆开 |
| 收录标准 |
收信标准 |
| 用户协议 |
公约 |
| 长期不更新 |
停笔 |
词换了,气质就跟着变了。你点的是「拆开一封信」,而不是「随机阅读一篇文章」——前者有动作、有期待,后者只是个功能。
LOGO:两笔画完一个世界
品牌标记只用两笔:
① 世界之环 —— 一个半径 11.5 的圆,在顶部留了 30° 的缺口。
那道缺口不是设计缺陷,是投递口:信从这个世界的缝隙里被送出来。
② 信封折口 —— 两端精确落在环上,折尖朝下,落在圆心偏下的位置。
它在说:这封信属于这个世界,不是漂在别处的。
两笔都用了圆头线帽(stroke-linecap: round),因为信纸的折痕是柔的,不是刀切的。
这个图形要在 16px 的浏览器标签页里还能被认出来,所以正式图标外面套了一个深底圆角方块(#0D1013)。纯线条在 16px 下会糊成一团灰,加个深底之后,它至少还是一个可辨的色块。同一个图形做了 .ico 三帧、16/32 PNG、180 苹果触控图、192/512 PWA 图标,外加一个 SVG 版本给现代浏览器取矢量。
有意思的是,这个标记后来还被拿去做交互:点「拆开」的时候,那道折口会当场被「画」出来——像把一封信当着你的面折好。
视觉:三种颜色,三种字体
整套视觉只用了三个核心色:
| 变量 |
色值 |
用途 |
--void |
#0D1013 |
深色段落:首屏、公约、页脚 |
--paper |
#F5F3EE |
浅色主体,不是纯白,偏纸 |
--amber |
#C07830 |
契约金,全站唯一的强调色 |
--paper 故意不用 #FFF。纯白在长时间阅读时会刺眼,而一点点米黄更像纸,也更像「信」这件事本身。
契约金是唯一的强调色,用得极其克制:只出现在链接悬停、当前导航、投递按钮、信件折口这些「需要你动手」的地方。它不负责装饰,负责指路。
字体三档:
- 思源宋体(Noto Serif SC)——标题与正文的骨架,衬线才有「信」的重量
- 思源黑体(Noto Sans SC)——标签、辅助信息、按钮,负责让界面安静
- Instrument Serif —— 只用于数字与英文小标题(如
95、DELIVERY),给冷冰冰的数字一点铸版印刷的味道
三种字体各管一件事,不越界。
三个自己想的视觉决策
一、全站的缩略图是 CSS 手绘的。
你在信墙和书简卡片上看到的那些「博客首页截图」,没有一张是真实截图,也没有一张是 AI 生成的图。它们是用 div 拼出来的抽象版式——顶部一条深色导航、几行粗细不一的灰条、偶尔一块色块。同一套规则根据博客 ID 生成不同的排布,所以每个站看起来都不一样,但风格是统一的。
好处很实在:零外链、零加载、零版权风险,而且永远不会因为对方站点改版而变得图文不符。
二、「写信第 N 年」是一条刻度条。
每个写信人旁边都有一条十格刻度,用满了多少格,就是写了多少年。把「坚持」这种抽象的东西,变成可以一眼看出的长度。
三、「投递」是一场有收束感的过场。
点「拆开」之后,不是弹一个转圈 loading,而是底部浮出一条状态带:标记出现、折口被画出来、目标名字浮现,然后跳转。全程不到一秒。
抽谁不是随机的——写得勤的人权重更高(抓取正常 3 倍,近期有失败 1 倍),但久未更新的人也不会永远抽不到。首页那句「写得越勤,被投到的机会越大」,在代码里是真实的权重,不是文案。
架构:一个二进制,五个队列
站点在技术上刻意做得简单——能单进程跑的,绝不拆成微服务。
┌──────────────────────────────┐
Nginx (443) ───► │ Go 单进程 (127.0.0.1:8080) │
│ │
│ Web 门户 SSR + 后台 SPA │
│ Worker asynq 任务消费者 │
│ Cron 定时调度 │
└───────┬──────────────┬───────┘
│ │
MySQL 5.7 Redis
(业务数据) (队列 + 验证码 + 频控)
为什么是单进程
bloghub all 一条命令同时起 Web、Worker、Scheduler 三部分,共用一份配置、一个数据库连接池、一套日志。
对一个小型索引站来说,拆成三个服务只会带来三倍的部署复杂度、三倍的故障点,换不来任何实际收益。真要扩,serve / worker / scheduler 三个子命令也可以分开跑,只是现在没必要。
模板和静态资源全部内嵌
前台模板、CSS/JS、图标、后台 React 打包产物、甚至数据库迁移脚本,全部通过 go:embed 编译进二进制。
结果是:部署只需要传一个文件。没有 npm build,没有 cp -r dist/,没有「忘记同步某个静态文件」这类事故。代价是改样式要重新编译,但这是开发阶段的事,跟运维无关。
这里踩过一个坑值得一提:开发模式(dev)用 filepath.Glob 读磁盘,匹配不到文件会静默跳过;生产模式(prod)用 template.ParseFS 读嵌入 FS,匹配不到文件会直接报错退出。模板列表里有一个早就不存在的空目录,本地永远发现不了,一上服务器就崩。这类「只在 prod 暴露」的差异,是做单二进制部署时最需要盯的地方。
五条任务队列
用 asynq(基于 Redis)把异步任务按重要性和耗时分开:
| 队列 |
用来做什么 |
critical |
验证码、通知邮件——用户正等着的结果 |
fetch |
拉取 RSS、抓取正文 |
content |
正文提取与清洗(readability) |
analyze |
AI 打标签、生成摘要、识别营销内容 |
default |
阅读量回写、统计重算、过期数据清理 |
分开的意义是互不阻塞:AI 分析慢,不会拖住验证码;一次大批量抓取,也不会让阅读量回写排队一小时。
调度器负责定时投递:抓取扫描 30 秒一次、阅读量与点赞回写 60 秒一次、抓取频率调整每小时一次、统计重算 10 分钟一次、过期数据每天凌晨清理。
数据怎么流动
RSS 地址
│ ① 定时/手动触发,按站点间隔与并发上限抓取
▼
RSS 解析(gofeed)
│ ② 新条目入库,去重靠 GUID + 链接哈希
▼
正文提取(go-readability)
│ ③ 只留正文,去掉导航、广告、评论区
▼
AI 分析
│ ④ 打标签、写摘要、判断是否为采集站或 AI 批量产出
▼
门户渲染(html/template 服务端渲染)
│ ⑤ 每次请求实时查库,改完刷新即生效
▼
读者 ──► /out/{id} ──► 作者原文(计一次阅读,流量回到作者站点)
第 ⑤ 步是刻意选的:门户用服务端渲染,不是 SPA。因为一个内容索引站最重要的读者之一,是搜索引擎和 RSS 阅读器。SSR 让每篇文章都有完整的 HTML、独立的 canonical、结构化的 JSON-LD,不需要为了 SEO 再补一套预渲染。
后台则相反,用 React + Ant Design 做成单页应用——运营要的是表格、筛选、批量操作,这些恰恰是 SPA 擅长、SSR 不擅长的。两套前端、两种渲染方式,各做自己合适的事。
安全上做了这些
- 会话走 HttpOnly Cookie,前端拿不到 token,XSS 也偷不走
- 所有写操作校验 CSRF 双提交令牌
- 管理员密码用 bcrypt(cost 12),改密码需要验证原密码,改完把其它设备上的会话全部踢下线
- 后台分三种角色:
super / reviewer / operator,按角色做接口级门禁
- 关键操作写审计日志(谁、什么时候、对哪条数据、从哪个 IP)
- 抓取端声明真实 UA、遵守
robots.txt、拒绝解析到内网 IP(防 SSRF)
部署长什么样
./bloghub all --config config.yaml
就这一行。前面挂 Nginx 做 TLS 与反向代理,systemd 管进程与自动重启,数据库和 Redis 用宝塔面板装好即可。
配置里最需要小心的两处:
- MySQL 的 DSN 必须带
charset=utf8mb4&parseTime=true&loc=Local
- Nginx 必须透传
X-Forwarded-Proto,否则程序判断不出 HTTPS,Cookie 的 Secure 标记会错,表现就是「能登录,一刷新就掉线」
边界:我们不做的事
这部分比功能列表更能说明一个站在想什么。
不镜像、不转存。 只存标题、摘要、发布时间和原文链接。正文完整内容仅在站内做摘要展示,点开永远回到作者那儿。
不碰付费收录。 收录不收费,排序不卖钱。写得好就是写得好,没钱也能被看见。
不催更。 半年不更新只是状态从「在更新」变成「半年没更新」,不会被踢出名录。只有站点彻底打不开超过 15 天才会暂停投递,恢复访问就自动恢复。
不记录读者。 不埋点、不做用户画像、不追踪你点了什么、停留多久。阅读量只统计「从这儿跳去原文」的次数,仅此而已。
随时可停笔。 博主自己可以在后台暂停投递、修改 RSS、重新提交审核,不需要联系我们。
最近更新了什么
这一轮迭代主要在三件事上:
1. 一致性收口。
错误页(404 / 500)从旧皮肤迁到了门户皮肤,现在无论从哪个入口走错,看到的都是同一套顶栏、页脚和字体。旧项目名残留(比如验证码邮件里的「Blogger 管理面板」)全站扫了一遍清掉。
2. 投递体验重做。
原来的全屏黑色遮罩被替换成底部状态带——不再遮挡页面,不会让人以为站点挂了。动效只保留一个动作:把信封折口画出来。
3. 后台补齐了一批运营能力。
- 邮件模板可视化配置:验证码、通知等模板支持 HTML,改完立即生效,不用改代码发版
- 博客列表新增「抓取异常」筛选——这类站状态仍是「正常」,只是连续抓取失败,以前混在正常列表里根本筛不出来
- 管理员可以自助修改密码,改完自动踢掉其它设备的会话
- 站点页脚、备案号、版权信息全部改为后台可配
4. 无障碍与降级。
所有动效都响应 prefers-reduced-motion:系统开了「减少动态效果」,描线、缩放、位移全部关掉,直接给结果。
5. 顺带修的几个隐蔽 bug。
导航栏在滚动时抖动(overflow-x: hidden 让 body 变成了滚动容器,position: sticky 的定位基准跟着漂)、后台博客列表的 SQL 参数被覆盖导致 expected 9 arguments, got 6、随机投递只在前端已渲染的 6 条里打转(改成服务端从全量投递池抽签)。
最后
万界书简不是一个「产品」,更像一份公共目录。
它不打算长大到能跟平台竞争,也没想过靠它赚钱。它只是想证明一件事:不靠算法,独立博客之间也能互相找到。
如果你的站点满足这几条——内容是自己写的、域名是自己的、RSS 能打开、正文不用登录就能读——欢迎来投一封信。
申请加入:申请收录 - 万界书简
收信标准:收信标准 - 万界书简
订阅更新:ruae.com/rss