万界书简:把散落各处的信,收进同一份目录

幻影  (UID: 2412) [复制链接]
帖子链接已复制到剪贴板
帖子已经有人评论啦,不支持删除!

139 14
一个非营利的独立博客索引,以及它背后的一点设计取舍与技术实现。站点地址:万界书简 - 来自不同世界的信

为什么又做了一个站

独立博客最难的不是写,是被看见。

你写得再好,只要没有推荐算法的入口,就只能在几个熟人的 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
这家伙太懒了,什么也没留下。
已有评论 ( 14 )
提示:您必须 登录 才能查看此内容。
域名市场
   域名载入中...
创建新帖
自助推广购买广告位
确认删除
确定要删除这篇帖子吗?删除后将无法恢复。
删除成功
帖子已成功删除,页面将自动刷新。
删除失败
删除帖子时发生错误,请稍后再试。