• 首页
  • 微语
  • 相册
  • 足迹
  • 邻居
  • 留言
  • 归档
  • 关于
  • 搜索
  • 夜间模式
    ©2016-2026  幻影博客 Theme by OneBlog

    幻影博客博客

    搜索
    标签
    # e4a # 域名 # 源码 # 教程 # 内容 # 代挂 # app # 网站 # 软件 # QQ
    技术教程

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

    2026.10.17 / 34 阅读 / 0 评论 / 6822 字
    技术教程

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

    阅读 34 评论 0 发表于2026.10.17
    一个非营利的独立博客索引,以及它背后的一点设计取舍与技术实现。站点地址:https://ruae.com

    为什么又做了一个站

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

    你写得再好,只要没有推荐算法的入口,就只能在几个熟人的 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)
    analyzeAI 打标签、生成摘要、识别营销内容
    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 能打开、正文不用登录就能读——欢迎来投一封信。

    申请加入:https://ruae.com/join
    收信标准:https://ruae.com/treaty
    订阅更新:https://ruae.com/rss

    本文由HuanYing撰写。站点由幻影博客维护,本身也是这份名录里的一位写信人。

    本文著作权归作者 [ HuanYing ] 享有,未经作者书面授权,禁止转载,封面图片来源于 [ 互联网 ] ,本文仅供个人学习、研究和欣赏使用。如有异议,请联系博主及时处理。
    取消回复

    发表留言
    回复

    首页微语相册足迹邻居留言归档关于
    Copyright©2016-2026  All Rights Reserved.  Load:0.041 s
    鲁ICP备2022028556号-1
    Theme by OneBlog V3.6.5
    夜间模式

    本站已加入BLOGS·CN

    开源不易,请尊重作者版权,保留基本的版权信息。