Squalk: 一个基于Nostr构建的经典论坛引擎(NIP-29群组,NIP-7D话题)
摘要
Squalk是一个基于Nostr构建的论坛引擎,实现了NIP-29和NIP-7D,用于管理简单或大型社区,具有可自定义的模式和聊天功能。
查看缓存全文
缓存时间: 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_MODE | 否 | simple | simple(单一论坛)或 full(多个房间)。管理员稍后可以在运行时将简单模式升级为完整模式。 |
PUBLIC_GROUP_ID | 在简单模式下 | — | 单一论坛的群组 ID。当 PUBLIC_MODE=simple 时必需;在完整模式下被忽略,房间在运行时选择。 |
PUBLIC_TITLE | 否 | 群组名称 | 顶部栏显示的标题。为空时回退到群组名称。 |
PUBLIC_JOINCODE | 否 | no | 设为 yes 可在加入请求被拒绝时显示邀请码字段(用于代码门控的中继)。 |
PUBLIC_SSR | 否 | no | 设为 yes 可在服务器端渲染页面(可爬取的 HTML,真实的 404 页面)。构建时会针对 Node (node build) 而非静态捆绑包;参见部署。 |
PUBLIC_SSR_WARM | 否 | yes | 当 PUBLIC_SSR=yes 时,每次客户端导航也会请求服务器获取并缓存该页面,以便后续刷新、分享链接或爬虫访问时提供热缓存内容。每次服务器导航会额外产生一次中继查询;设为 no 可禁用。 |
PUBLIC_SSR_CACHE_FRESH | 否 | 300 | 服务器渲染快照保持原样提供的秒数。也是边缘缓存的 s-maxage。 |
PUBLIC_SSR_CACHE_STALE | 否 | 21600 | 快照不再提供(同时在后台刷新)的秒数(在此时间之前,过期页面会立即响应,并为下一位访问者更新)。也是边缘缓存的 stale-while-revalidate。 |
PUBLIC_SEARCH | 否 | no | 设为 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.md、guidelines.md、homepage.md、contacts.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 install 或 yarn)安装依赖后,启动开发服务器:
npm run dev
# 或启动服务器并在新的浏览器标签页中打开应用
npm run dev -- --open
构建
两个部署目标共享相同的代码库,通过 PUBLIC_SSR 选择:
- 静态(默认,
PUBLIC_SSR=no) —npm run build(或just build)将单页应用捆绑包写入build/;将其从任何 Web 服务器提供服务,使用index.html作为未知路径的回退。所有内容由浏览器获取。 - 服务器渲染(
PUBLIC_SSR=yes) —just build-ssr将 Node 应用写入build/。页面以可爬取的 HTML 形式到达(线程、房间、资源、联系人,带有描述/Open Graph 标签、JSON-LD、实时的robots.txt和sitemap.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_HOST、DEPLOY_DIR 和 DEPLOY_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
Nibble 是一种类 C 的系统编程语言,用 3000 行 C 代码实现,无需外部依赖或堆分配即可生成 LLVM IR。它支持 defer、递归、多种类型、结构体、指针,并包含图形演示。
互联网边缘的社区建设
本文介绍了基于Nostr的社区建设,使用Pyramid中继软件和Jumble客户端,实现去中心化、可移植的社区,无需依赖中央服务器。
Show HN:textlog —— 一个安静、纯文本的微博客平台,开源,无 JavaScript
textlog 是一个新的开源、纯文本微博客平台,不使用 JavaScript,支持 280 字短笺、话题标签和对话,旨在提供更安静的社交体验。
cl-bbs: 用Common Lisp重写的类schemeBBS文本公告板
cl-bbs 是一个用 Common Lisp 编写的高性能匿名文本公告板引擎,忠实复刻了原始 SchemeBBS 的风格。它提供格式化支持、图片预览以及零 JavaScript 渲染等功能。
我开发了一个本地工具,用于导出、搜索和继续来自OpenRouter、LM Studio和AI Studio的聊天,可使用llama.cpp或OpenRouter
ThreadShelf是一款开源工具,支持用户在本地存档、搜索和继续来自OpenRouter、LM Studio和AI Studio的聊天,具备语义搜索和llama.cpp集成功能。