我的书签现状:浏览器收藏夹三千条,从「CSS 技巧」到「某篇讲分布式锁的文章」,没有标签、没有备注、换台电脑就剩一半,搜索只能匹配标题。市面上的书签服务倒是功能全,代价是把"我关心什么"这种画像交出去。
Treasury 是我自己的解法:单密码登录的私有书签库,粘贴链接自动抓标题和描述,标签整理,全文搜索,数据在自己的 Neon Postgres 里。「私有部署」这个词在这个项目里的意思是:数据库是我自己的实例,登录是单密码而不是多用户系统——它就是给我一个人用的服务,恰好架在公网上。
怎么用
登录后顶部一个输入框:粘贴链接,可选填标签(空格或逗号分隔),点收藏。按钮变成「抓取中…」的同时,服务端去抓目标页面的 <title> 和 meta description。卡片列表上每条书签带域名、描述和可点击的标签,点任何一个标签就筛选同类;搜索框即时过滤,关键词同时匹配标题、描述、网址和标签四个字段。
抓标题,零依赖
抓取实现是我觉得这个项目里最干净的部分。常规做法是丢个 cheerio 进去解析 HTML,但为了取一个标题引一个解析器,不值得。lib/fetch-meta.ts 手写:8 秒超时,流式读取封顶 200KB,正则同时匹配 <title> 和 meta description 的两种属性顺序(name...content 和 content...name 都有人写),顺手处理 &#x 十六进制实体转义。最关键的设计是任何失败都降级而不是报错——抓不到标题就用域名顶上,返回 {title: url.hostname, ok: false},收藏这个动作永远不会被一次失败的抓取阻塞。用户存的是链接,标题只是锦上添花。
标签用 Postgres 的原生数组字段存(text[]),不用单独建关联表——个人书签量级的标签查询,一个 array 字段加 GIN 都嫌多,直接全量读出来在内存里聚合成标签云,按出现次数排序。
诚实的搜索
搜索功能 README 里写了实现方式:全量 SELECT 出来在内存过滤。这是刻意的选择——个人书签几千条的量级,内存过滤毫秒级返回,还能支持任意字段的模糊匹配;上数据库全文索引(tsvector 之类)是为十万级准备的技术,对我这个规模是过度设计。代码注释里原话是「内存过滤:个人书签量级下足够快」。等哪天真慢了再换不迟,现在换是在给想象中的用户写代码。
双胞胎项目
这个项目和 RSS 阅读器是同一晚前后一小时创建的仓库,账号体系(timingSafeEqual 恒定时间比较、HS256 JWT Cookie、middleware 边缘鉴权)、SSRF 防护、Drizzle 参数绑定,两边代码是同一份。写第二个的时候明显快很多——私部署小服务的"模板"算是沉淀下来了。两者的区别只在数据流:书签是「写入时抓取」,不需要 Cron;RSS 是「定时抓取」,必须有调度。同一个骨架,两种血型。
重复添加的处理也顺手做了:POST 先查链接是否存在,存在就把已有书签原样返回,标个 duplicate: true,前端提示一句——不报错,因为"重复收藏"不是错误,是用户忘了存过。
源码在 GitHub,127KB 的仓库,一个下午的事。