← 返回信息流
AI 资讯Hacker News·5 天前

Waldi:专注写作与阅读的安静空间

原标题:Waldi: A quiet place to write, and to be read

速览

Waldi 是一个专注于减少干扰的写作与阅读平台,强调为用户创造宁静的创作和阅读环境。它可能面向作家、博主以及深度阅读爱好者。目前尚未披露具体技术细节,但这一概念在信息过载时代具有独特价值。

AI 深度解读

背景

Waldi 是一个专注于安静写作与阅读的多租户博客平台,由一位自 2016 年开始写博客的作者创建。作者在 2016 年创办个人博客 dissolve 时,已经错过了博客的黄金时代。他提到,波斯语博客圈曾是早期互联网最大、最活跃的角落之一,他在那里找到了最亲密的朋友。然而,随着这些平台逐渐衰落或消失,有时甚至带走了人们十年的写作成果,大家纷纷散落到 Telegram、Instagram 和 Twitter 等平台——这些地方从未让写作重新拥有私密、从容的空间。Waldi 正是对这一历史背景的回应,基于两个信念:第一,博客消亡的原因不是写作本身,而是分发——新博客发表后无人问津,而社交网络保证了噪音的受众;第二,读者的阅读量应该是有终点的——关注作者的一小部分文章、一篇陌生人的意外之作,然后就是“今天的全部内容”。

核心内容

Waldi 是一个运行在单个 Go 二进制文件加 Postgres 之上的平台,使用服务器渲染的 html/template 页面,并包含一个 Tiptap 编辑器岛屿。每个作者获得一个 username.<base-domain> 或者自己的自定义域名;同一个二进制文件通过运行时读取请求的 Host 头来服务所有博客,因此无需管理每个租户的部署。

平台的核心循环是:发布→一百个陌生人阅读→收到关注或私信。没有公开的点赞数、没有评论、没有排行榜。回复采用私信形式,每篇文章在算法评判之前都会获得一个保证的读者基数——这样新作者永远不会发布后无人问津。

Waldi 首先以波斯语发布,因为作者认为这是对波斯语博客圈的亏欠。但平台设计为双语(参见 Multi-language / i18n),英语是一等公民,而非翻译目标。产品规则(私信代替评论、任何地方都没有公开指标、一个永远不会被构建的功能硬列表)记录在 ROADMAP.md 和 docs/DESIGN.md 中。

技术架构方面,每个请求经过 internal/web/server.go 的 ServeHTTP,首先检查 Host 头是否映射到一个博客(internal/web/host.go 的 isBlogHost)。子域名匹配(username.<base-domain> 或 .localhost / .waldi.test 开发环境)通过 BlogFromHost 实现;否则进行自定义域名查询(通过 Postgres,并缓存到进程内 customDomainCache,具有正负 TTL)。处理程序根据解析的主机是博客还是主应用域分支(internal/web/blog.go、handlers_read.go)。会话 cookie 通过 cookieDomain 限定范围,使得主域上的登录可以延续到 username.<base-domain> 子域名,但不能用于自定义域名。

代码包结构如下:

  • internal/web:HTTP 层(路由、处理程序、模板数据结构和渲染、会话/语言环境/主机解析)。
  • internal/store:所有 SQL 操作,封装 *pgxpool.Pool。每个实体一个文件(users.go、posts.go、follows.go、letters.go、events.go、invitations.go、settings.go、stats.go)。处理程序直接调用 store 方法——没有仓库/服务接口层。
  • internal/post:纯领域逻辑,用于 Tiptap 文档模型:JSON schema 验证、清理后的 HTML 渲染、图片 URL 修复、嵌入、脚注。不涉及 DB 或 HTTP——是单元测试最密集的包。
  • internal/jobs:每日后台任务(摘要邮件、随机选择),以 CLI 子命令运行,而非队列/工作进程。
  • internal/mail:SMTP 发送,通过小型 Mailer 接口;当未配置 SMTP(开发环境)时使用 NoopMailer。
  • internal/storage:图片上传存储:本地磁盘或 S3 兼容(生产环境使用 Cloudflare R2),上传时调整为 WebP 格式。
  • internal/cdn:发布时清除 Cloudflare 缓存(可选,需要 zone ID/token)。
  • internal/i18n:翻译目录,供 Go 和模板使用。
  • internal/telegrambot:管理机器人(见下文)。
  • internal/importcommon:所有导入器共用的 HTML 转文档转换。
  • internal/importblogir:blog.ir 导入器;为添加其他平台支持提供参考实现(见 CONTRIBUTING.md)。
  • internal/migrate:运行 migrations/ 中编号的只追加 SQL 文件。
  • cmd/waldi:CLI 入口点;main.go 中分发子命令(serve、migrate、digest、reader-digest、wildcard、weekly-stats、invite、user、import、posts)。

Web 模板位于 web/templates/*.html,通过 template.ParseFS 加载(开发环境从磁盘,部署环境使用 embed.FS)。数据为单个 web.PageData 结构体,每种页面类型带一个可选的子视图。CSS 位于 web/static/css/main.css,手写,无框架。编辑器位于 web/editor/(Tiptap 岛屿,TypeScript),通过 esbuild 直接打包到 web/static/js/editor.js。编辑器文档模式在服务器端 internal/post/doc.go 中强制实施——是服务器(而非客户端)阻止作者生成无效文档。

数据库使用 Postgres,迁移文件 migrations/00N_*.sql。核心表包括:users、sessions、posts(doc jsonb 为 Tiptap 事实来源,html 在发布时预渲染/清理并原样提供)、follows(含 source_post_id 用于关注归因)、letters、impressions/readings(追加型事件日志)、wildcards、digests,以及自定义域名、邀请和管理员设置。完整生产环境设置见 DEPLOY.md。

平台允许自行托管,许可证为 MIT,DEPLOY.md 记录了完整生产环境设置,包括假设使用 Cloudflare/R2/Caddy 的部分以及如何替换它们。但有两个注意事项:waldi.blog 是规范社区,是作者运营和支持的唯一实例;自行托管的实例是独立的安静岛屿——保证读者循环仅取决于实例拥有的读者数量。路线图优先满足 waldi.blog 的需求;欢迎自行托管者提交 issue,但修复会优先处理托管特有的问题。

国际化方面,翻译字符串位于扁平 JSON 目录中,嵌入到二进制文件(internal/i18n/locales/en.json、fa.json)。每个面向用户的字符串通过 T(lang, key, args...) 查找(internal/i18n/catalog.go),模板跨语言共享——没有每种语言的模板重复,web/templates/*.html 中的每个字符串都通过 PageData.T 路由。每个请求的语言解析优先级:登录用户存储的语言环境 → locale cookie → CF-IPCountry 头(通过 i18n.LangFromCountry)→ 默认值(fa)。用户可以在账户设置中更改语言,这会更新 cookie、持久化到账户,并清除缓存页面。要添加新语言,只需添加 internal/i18n/locales/<code>.json(镜像 en.json 中的每个键),在 internal/i18n/catalog.go 的 supported 中添加 "<code>": true,如果语言是 RTL 则在同一文件的 Dir() 中添加 case(默认 LTR),可选地扩展 i18n.LangFromCountry(internal/i18n/country.go)以便来自该国家的访问者自动获取。不需要模板更改——目录是唯一增长的。

三个定时任务,全部在 internal/jobs/ 中,作为 CLI 子命令在主机 crontab 上运行(非队列):

  • 0 3 * * * ./deploy/backup-db.sh # 每日 Postgres 备份到 R2
查看原文 →github.com