Squalk: an old-school forum engine built on Nostr (NIP-29 groups, NIP-7D threads)
Squalk: an old-school forum engine built on Nostr (NIP-29 groups, NIP-7D threads)
Squalk:基于 Nostr 构建的复古论坛引擎(支持 NIP-29 群组与 NIP-7D 讨论串)
Squalk is a forum built on Nostr that permits to manage simple or large communities; in fact you can choose to setup it in “simple” or “full” mode. Simple mode expose a single forum, while in Full mode you can have as many forum as you like. Each forum includes a chat feature in the right-hand sidebar, which is useful for quickly interacting with members. Squalk 是一个基于 Nostr 构建的论坛,允许管理小型或大型社区;实际上,你可以选择将其设置为“简单(simple)”或“完整(full)”模式。简单模式仅展示单个论坛,而在完整模式下,你可以拥有任意数量的论坛。每个论坛的右侧边栏都包含聊天功能,方便与成员进行快速互动。
Tech stack
技术栈
Squalk is built on Nostr and implement NIP-29 and NIP-7D. It needs a personal relay that supports NIP-29 to host the group(s) and a Blossom server for the uploads; Pyramid includes both and is the suggested solution. Squalk 基于 Nostr 构建,并实现了 NIP-29 和 NIP-7D 协议。它需要一个支持 NIP-29 的个人中继(Relay)来托管群组,以及一个用于上传文件的 Blossom 服务器;Pyramid 同时包含这两者,是推荐的解决方案。
Configuration
配置
Squalk is configured entirely through environment variables (all prefixed PUBLIC_, since they are read in the browser). Copy .env.example to .env and fill in the values; SvelteKit also reads .env.development (used by npm run dev) and .env.production (used by npm run build).
Squalk 完全通过环境变量进行配置(所有变量均以 PUBLIC_ 为前缀,因为它们是在浏览器中读取的)。将 .env.example 复制为 .env 并填入相应值;SvelteKit 还会读取 .env.development(用于 npm run dev)和 .env.production(用于 npm run build)。
| Variable | Required | Default | Description |
|---|---|---|---|
| 变量 | 必填 | 默认值 | 描述 |
| PUBLIC_RELAY_URL | yes | — | WebSocket URL of the NIP-29 relay hosting the group(s), e.g. wss://relay.example.com. |
| PUBLIC_RELAY_URL | 是 | — | 托管群组的 NIP-29 中继的 WebSocket URL,例如 wss://relay.example.com。 |
| PUBLIC_MODE | no | simple | simple (a single forum) or full (multiple rooms). The admin can later upgrade simple → full at runtime. |
| PUBLIC_MODE | 否 | simple | simple(单个论坛)或 full(多个房间)。管理员后续可在运行时将 simple 升级为 full。 |
| PUBLIC_GROUP_ID | in simple mode | — | The single forum’s group id. Required when PUBLIC_MODE=simple; ignored in full mode, where rooms are selected at runtime. |
| PUBLIC_GROUP_ID | 简单模式下必填 | — | 单个论坛的群组 ID。当 PUBLIC_MODE=simple 时必填;在 full 模式下会被忽略,此时房间在运行时选择。 |
| PUBLIC_TITLE | no | group name | Title shown in the top bar. When empty it falls back to the group’s name. |
| PUBLIC_TITLE | 否 | 群组名称 | 顶部栏显示的标题。留空时将回退显示为群组名称。 |
| PUBLIC_JOINCODE | no | no | yes to show an invite-code field when a join request is rejected (for code-gated relays). |
| PUBLIC_JOINCODE | 否 | no | 设置为 yes 以便在加入请求被拒绝时显示邀请码字段(适用于有代码门槛的中继)。 |
| PUBLIC_SSR | no | no | yes to render pages on the server (crawlable HTML, real 404s). The build then targets Node (node build) instead of a static bundle; see Deploying. |
| PUBLIC_SSR | 否 | no | 设置为 yes 以在服务器端渲染页面(可抓取的 HTML,真实的 404 错误)。构建目标将变为 Node(node build)而非静态包;详见部署说明。 |
| PUBLIC_SSR_WARM | no | yes | With PUBLIC_SSR=yes, each client-side navigation also asks the server to fetch and cache that page, so a later refresh, shared link or crawler hit is served warm. Costs one extra relay query per navigation on the server; set to no to disable. |
| PUBLIC_SSR_WARM | 否 | yes | 当 PUBLIC_SSR=yes 时,每次客户端导航也会请求服务器获取并缓存该页面,以便后续刷新、分享链接或爬虫访问时能快速响应。每次导航会额外消耗一次服务器端中继查询;设置为 no 可禁用。 |
| PUBLIC_SSR_CACHE_FRESH | no | 300 | Seconds a server-rendered snapshot is served as is. Also the edge cache’s s-maxage. |
| PUBLIC_SSR_CACHE_FRESH | 否 | 300 | 服务器渲染快照保持不变的秒数。同时也是边缘缓存的 s-maxage。 |
| PUBLIC_SSR_CACHE_STALE | no | 21600 | Seconds after which a snapshot is no longer served while being refreshed in the background (until then a stale page is answered instantly and updated for the next visitor). Also the edge cache’s stale-while-revalidate. |
| PUBLIC_SSR_CACHE_STALE | 否 | 21600 | 快照不再直接提供服务并开始在后台刷新的秒数(在此之前,过期的页面会立即响应,并为下一位访问者更新)。同时也是边缘缓存的 stale-while-revalidate。 |
| PUBLIC_SEARCH | no | no | yes to show a search box at the top of the homepage. Requires a relay with NIP-50 search support. |
| PUBLIC_SEARCH | 否 | no | 设置为 yes 以在首页顶部显示搜索框。需要支持 NIP-50 搜索的中继。 |
| PUBLIC_LABELS | no | — | Comma-separated discussion labels offered when composing, e.g. bug,feature,question. |
| PUBLIC_LABELS | 否 | — | 发布时提供的逗号分隔的讨论标签,例如 bug,feature,question。 |
| PUBLIC_BLOSSOM_URL | no | — | Blossom server URL used for media uploads, e.g. https://blossom.primal.net. Uploads are disabled when unset. |
| PUBLIC_BLOSSOM_URL | 否 | — | 用于媒体上传的 Blossom 服务器 URL,例如 https://blossom.primal.net。未设置时禁用上传。 |
| PUBLIC_ACCENT_COLOR | no | #e32a6d | Override the accent (primary) color. Quote the value (“#00ff00”) — an unquoted leading # is read as a comment. The hover shade is derived automatically. |
| PUBLIC_ACCENT_COLOR | 否 | #e32a6d | 覆盖强调(主)颜色。请使用引号包裹值(如 “#00ff00”)——未加引号的开头 # 会被视为注释。悬停颜色会自动生成。 |
| PUBLIC_SECONDARY_COLOR | no | #ffaf25 | Override the secondary color. Same quoting rule and derived hover shade as above. |
| PUBLIC_SECONDARY_COLOR | 否 | #ffaf25 | 覆盖次要颜色。规则与上述相同,包含引号规则和自动生成的悬停颜色。 |
Customizing content
自定义内容
Squalk fills its sidebar links and personalizes the homepage and contacts page from NIP-23 long-form events (kind 30023) published to the same relay that hosts the group(s). Only events authored by a forum admin (a pubkey listed in a group’s NIP-29 39001 admin event) are surfaced — the relay query is open, so the admin set is the trust gate. Content is plain markdown. The sample .md files in the repo root (about.md, guidelines.md, homepage.md, contacts.md) are starting points you can adapt and publish. Squalk 通过发布到托管群组的同一中继上的 NIP-23 长文事件(kind 30023)来填充侧边栏链接,并个性化首页和联系页面。只有由论坛管理员(在群组的 NIP-29 39001 管理事件中列出的公钥)发布的事件才会被显示——中继查询是开放的,因此管理员列表是信任门槛。内容为纯 Markdown。仓库根目录下的示例 .md 文件(about.md, guidelines.md, homepage.md, contacts.md)是你可以修改并发布的起点。
Resources (sidebar links)
资源(侧边栏链接)
Resources appear in the left sidebar and are served at /resource/
| Tag | Required | Purpose |
|---|---|---|
| 标签 | 必填 | 用途 |
| [“t”, “squalk-resource”] | yes | marks the event as a resource |
| [“t”, “squalk-resource”] | 是 | 将事件标记为资源 |
| [“d”, " | yes | the d/identifier tag — also the URL slug (/resource/ |
| [“d”, " | 是 | d/identifier 标签 — 同时也是 URL 别名 (/resource/ |
| [“title”, " | recommended | label shown in the sidebar (falls back to the slug) |
| [“title”, " | 推荐 | 侧边栏显示的标签(若为空则回退显示为别名) |
| [“position”, " | optional | ordering hint, ascending |
| [“position”, " | 可选 | 排序提示,升序排列 |
The content field is the markdown body. Ordering: resources with a position come first, sorted ascending; ties and unpositioned resources fall back to alphabetical order by title. Because events are addressable, re-publishing with the same d slug updates the resource (newest wins). content 字段为 markdown 正文。排序规则:带有 position 的资源优先,按升序排列;平局或未设置 position 的资源按标题字母顺序排列。由于事件是可寻址的,使用相同的 d 别名重新发布即可更新资源(以最新版本为准)。
Partials (homepage & contacts)
局部页面(首页与联系页面)
Partials inject custom markdown into fixed slots. There are exactly two slots: home (rendered at the top of the homepage) and contacts (the contacts page). Publish a kind 30023 event with: 局部页面将自定义 markdown 注入到固定槽位中。共有两个槽位:home(渲染在首页顶部)和 contacts(联系页面)。发布一个 kind 30023 事件,包含以下标签:
| Tag | Required | Purpose |
|---|---|---|
| 标签 | 必填 | 用途 |
| [“t”, “squalk-partial”] | yes | marks the event as a partial |
| [“t”, “squalk-partial”] | 是 | 将事件标记为局部页面 |
| [“d”, “home”] or [“d”, “contacts”] | yes | the slot to fill (any other value is ignored) |
| [“d”, “home”] 或 [“d”, “contacts”] | 是 | 要填充的槽位(其他值将被忽略) |
| [“title”, " | optional | not displayed in the slot, but useful for clients |
| [“title”, " | 可选 | 不会在槽位中显示,但对客户端有用 |
The newest admin-authored event for a slot wins. The home partial renders above the room list / discussions feed; a leading image URL on its own line (see homepage.md) is rendered as a banner image. 每个槽位以管理员发布的最新事件为准。home 局部页面渲染在房间列表/讨论流上方;如果内容首行是一个独立的图片 URL(参考 homepage.md),它会被渲染为横幅图片。
Developing
开发
Once you’ve created a project and installed dependencies with npm install (or pnpm install or yarn), start a development server:
创建项目并使用 npm install(或 pnpm install / yarn)安装依赖后,启动开发服务器:
npm run dev
# or start the server and open the app in a new browser tab
# 或启动服务器并在新浏览器标签页中打开应用
npm run dev -- --open
Building
构建
Two deployment targets share the same code base, selected by PUBLIC_SSR: 两个部署目标共享同一代码库,通过 PUBLIC_SSR 选择:
- Static (default, PUBLIC_SSR=no) —
npm run build(or justbuild) writes a single-page bundle tobuild/; serve it from any web server withindex.htmlas the fallback for unknown paths. Everything is fetched by the browser. - 静态(默认,PUBLIC_SSR=no) —
npm run build(或直接build)将单页应用包写入build/目录;使用任何 Web 服务器托管,并将index.html设置为未知路径的回退页面。所有内容均由浏览器获取。 - Server-rendered (PUBLIC_SSR=yes) — just
build-ssrwrites a Node app tobuild/. Pages arrive as crawlable HTML (threads, rooms, resources, contacts, with description/Open Graph tags, JSON-LD, a live robots.txt and sitemap.xml, and real 404s), then the browser takes over exactly as in the static build. The server reads the relay anonymously, so it only ever renders public content; members see their… - 服务器渲染(PUBLIC_SSR=yes) —
build-ssr将 Node 应用写入build/目录。页面以可抓取的 HTML 形式呈现(包含讨论串、房间、资源、联系页面,带有描述/Open Graph 标签、JSON-LD、动态 robots.txt 和 sitemap.xml,以及真实的 404 错误),随后浏览器接管页面,过程与静态构建完全一致。服务器以匿名方式读取中继,因此它仅渲染公开内容;成员将看到他们的…