Squalk: 一个基于Nostr构建的经典论坛引擎(NIP-29群组,NIP-7D话题)

Lobsters Hottest 工具

摘要

Squalk是一个基于Nostr构建的论坛引擎,实现了NIP-29和NIP-7D,用于管理简单或大型社区,具有可自定义的模式和聊天功能。

<p>我怀念经典论坛:那些缓慢、异步的线程,可以保持可读性和可搜索性多年,而不是知识在聊天记录中消散。Squalk是我尝试在Nostr之上重建这一点的项目,Nostr是一个开放协议,用户持有密钥对,帖子是签名的事件,可互换的中继存储并提供它们,而不是私有数据库。</p> <p>简要设计:论坛是中继托管数据的视图。房间是NIP-29中指定的群组,话题是NIP-7D事件,身份是用户自己的密钥对。我关心的结果是软件和社区解耦:任何其他支持相同规范的客户端(Flotilla、Nostrord)都可以读写相同的对话,如果我的部署消失,历史记录和身份会在中继上幸存。</p> <p>可能对这个人群感兴趣的技术细节:一个SvelteKit代码库可以构建为静态SPA或服务器渲染的Node应用;SSR模式仅渲染匿名视图(服务器端无密钥),以便话题可爬取,在中继查询前有stale-while-revalidate快照缓存。内容渲染为djot而不是markdown。部署是一个NIP-29中继(Zooid)加一个systemd单元。</p> <p>试点社区(真实话题,无需账户可浏览):<a href="https://nostr-proto.org" rel="ugc">https://nostr-proto.org</a> 测试实例,欢迎你来捣乱:<a href="https://squalk-test.dtonon.com" rel="ugc">https://squalk-test.dtonon.com</a></p> <p>注意事项:Nostr是小众的,密钥管理对于非爱好者来说仍然粗糙,中继端审核工具尚不成熟。</p> <p>欢迎对架构提供反馈!</p> <p><a href="https://lobste.rs/s/haq46t/squalk_old_school_forum_engine_built_on">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/21 16:26

dtonon/squalk 来源:https://github.com/dtonon/squalk

Squalk

Squalk 是一个基于 Nostr 构建的论坛,可用于管理小型或大型社区;实际上,你可以选择将其设置为“简单”或“完整”模式。简单模式展示一个单一论坛,而完整模式可以拥有任意数量的论坛。每个论坛在右侧边栏都包含一个聊天功能,方便与成员快速互动。

技术栈

Squalk 基于 Nostr 构建,并实现了 NIP-29 (https://github.com/nostr-protocol/nips/blob/master/29.md) 和 NIP-7D (https://github.com/nostr-protocol/nips/blob/master/7D.md)。它需要一个支持 NIP-29 的个人中继来托管群组,以及一个 Blossom 服务器用于文件上传;Pyramid (https://github.com/fiatjaf/pyramid) 包含了这两者,是推荐的解决方案。

配置

Squalk 完全通过环境变量进行配置(所有变量以 PUBLIC_ 为前缀,因为它们在浏览器端读取)。将 .env.example 复制为 .env 并填写相应的值;SvelteKit 也会读取 .env.development(用于 npm run dev)和 .env.production(用于 npm run build)。

变量必需默认值描述
PUBLIC_RELAY_URL托管群组的 NIP-29 中继的 WebSocket URL,例如 wss://relay.example.com
PUBLIC_MODEsimplesimple(单一论坛)或 full(多个房间)。管理员稍后可以在运行时将简单模式升级为完整模式。
PUBLIC_GROUP_ID在简单模式下单一论坛的群组 ID。当 PUBLIC_MODE=simple 时必需;在完整模式下被忽略,房间在运行时选择。
PUBLIC_TITLE群组名称顶部栏显示的标题。为空时回退到群组名称。
PUBLIC_JOINCODEno设为 yes 可在加入请求被拒绝时显示邀请码字段(用于代码门控的中继)。
PUBLIC_SSRno设为 yes 可在服务器端渲染页面(可爬取的 HTML,真实的 404 页面)。构建时会针对 Node (node build) 而非静态捆绑包;参见部署
PUBLIC_SSR_WARMyesPUBLIC_SSR=yes 时,每次客户端导航也会请求服务器获取并缓存该页面,以便后续刷新、分享链接或爬虫访问时提供热缓存内容。每次服务器导航会额外产生一次中继查询;设为 no 可禁用。
PUBLIC_SSR_CACHE_FRESH300服务器渲染快照保持原样提供的秒数。也是边缘缓存的 s-maxage
PUBLIC_SSR_CACHE_STALE21600快照不再提供(同时在后台刷新)的秒数(在此时间之前,过期页面会立即响应,并为下一位访问者更新)。也是边缘缓存的 stale-while-revalidate
PUBLIC_SEARCHno设为 yes 可在主页顶部显示搜索框。需要支持 NIP-50 搜索功能的中继。
PUBLIC_LABELS编写时提供的逗号分隔的讨论标签,例如 bug,feature,question
PUBLIC_BLOSSOM_URL用于媒体上传的 Blossom 服务器 URL,例如 https://blossom.primal.net。未设置时禁用上传。
PUBLIC_ACCENT_COLOR#e32a6d覆盖强调(主要)颜色。值需要用引号括起来("#00ff00")——未加引号的开头 # 会被视为注释。悬停色调会自动派生。
PUBLIC_SECONDARY_COLOR#ffaf25覆盖次要颜色。相同的引号规则和派生的悬停色调。

自定义内容

Squalk 通过发布在同一中继上的 NIP-23 长文本事件(kind 30023)来填充其侧边栏链接,并个性化主页和联系人页面。只有由论坛管理员(其公钥列在群组 NIP-29 39001 管理员事件中)创作的事件才会被展示——中继查询是开放的,因此管理员集是信任门控。内容是纯 Markdown 格式。仓库根目录中的示例 .md 文件(about.mdguidelines.mdhomepage.mdcontacts.md)是你可调整并发布的起点。

资源(侧边栏链接)

资源出现在左侧边栏,并通过 /resource/ 路径提供服务。发布一个 kind 30023 事件,包含:

标签必需用途
["t", "squalk-resource"]将事件标记为资源
["d", ""]d/标识符标签——也是 URL 路径段 (/resource/)
["title", ""]推荐在侧边栏显示的标签(回退到路径段)
["position", "<n>"]可选排序提示,升序

content 字段是 Markdown 正文。排序规则:具有 position 的资源优先显示,按升序排序;并列和未定位的资源按标题字母顺序排序。由于事件是可寻址的,使用相同 d 路径段重新发布会更新资源(最新版本生效)。

示例(从主页链接的 about 资源):

kind: 30023
tags:
  ["t", "squalk-resource"]
  ["d", "about"]
  ["title", "About"]
  ["position", "1"]
content: |
  # About this forum
  ...

局部内容(主页和联系人)

局部内容将自定义 Markdown 注入固定插槽。正好有两个插槽:home(渲染在主页顶部)和 contacts(联系人页面)。发布一个 kind 30023 事件,包含:

标签必需用途
["t", "squalk-partial"]将事件标记为局部内容
["d", "home"]["d", "contacts"]要填充的插槽(任何其他值将被忽略)
["title", "<title>"]可选不在插槽中显示,但对客户端有用

对于一个插槽,最新的管理员创作的事件获胜。home 局部内容渲染在房间列表/讨论流之上;其单独一行开头的图片 URL(参见 homepage.md)将渲染为横幅图片。

开发

创建项目并使用 npm install(或 pnpm installyarn)安装依赖后,启动开发服务器:

npm run dev
# 或启动服务器并在新的浏览器标签页中打开应用
npm run dev -- --open

构建

两个部署目标共享相同的代码库,通过 PUBLIC_SSR 选择:

  • 静态(默认,PUBLIC_SSR=nonpm run build(或 just build)将单页应用捆绑包写入 build/;将其从任何 Web 服务器提供服务,使用 index.html 作为未知路径的回退。所有内容由浏览器获取。
  • 服务器渲染(PUBLIC_SSR=yesjust build-ssr 将 Node 应用写入 build/。页面以可爬取的 HTML 形式到达(线程、房间、资源、联系人,带有描述/Open Graph 标签、JSON-LD、实时的 robots.txtsitemap.xml,以及真实的 404 页面),然后浏览器接管,与静态构建完全一样。服务器匿名读取中继,因此它只渲染公共内容;成员在客户端运行后才能看到他们的私有房间。

使用 npm run preview(静态)或 node --env-file=.env.production build(服务器)在本地预览构建。

部署

just deploy <host> 使用 rsync 将静态捆绑包同步到主机的 ~/squalk/ 并清除 Cloudflare 缓存。 just deploy-ssr <mode> 使用 --mode <mode> 进行构建(因此 vite 会将 .env.<mode> 内置),传输 Node 构建产物、package.json/package-lock.json.env.<mode>(在应用目录中作为 .env,因为服务器在运行时读取 PUBLIC_* 值),运行 npm ci --omit=dev 并重启实例的 systemd 单元。

所有特定于实例的内容都保存在 .env.<mode>.local(git 忽略,永不传输)中:DEPLOY_HOSTDEPLOY_DIRDEPLOY_SERVICE(所有必需),以及用于缓存清除的 Cloudflare 凭据(CF_ZONE_ID/CF_API_TOKEN)。多个实例可以通过为每个实例分配自己的模式、目录、单元和端口来共存。

在主机上你需要:

  • Node 22 或更新版本(中继客户端使用内置的 WebSocket)。
  • 来自 deploy/production-example.service 的单元,将 ORIGIN 设置为公共 URL——它用于规范链接、robots.txt 和站点地图。
  • PORT 指定的端口前放置一个反向代理,替换之前提供静态文件的服务。使用 Caddy:
forum.example.com {
  reverse_proxy 127.0.0.1:3000
}
  • 如果使用 Cloudflare,设置一个缓存规则来缓存 HTML 并尊重源站头:页面和快照使用 Cache-Control: public, max-age=0, s-maxage=<PUBLIC_SSR_CACHE_FRESH>, stale-while-revalidate=<PUBLIC_SSR_CACHE_STALE> 发送(默认提供五分钟,然后在后台刷新长达六小时),这与服务器自身内存缓存使用的时间窗口相同。just deploy-ssr 在每次发布后清除缓存。

相似文章

Show HN: Nibble

Hacker News Top

Nibble 是一种类 C 的系统编程语言,用 3000 行 C 代码实现,无需外部依赖或堆分配即可生成 LLVM IR。它支持 defer、递归、多种类型、结构体、指针,并包含图形演示。

互联网边缘的社区建设

Lobsters Hottest

本文介绍了基于Nostr的社区建设,使用Pyramid中继软件和Jumble客户端,实现去中心化、可移植的社区,无需依赖中央服务器。