Skip to content

Commit 5e09870

Browse files
committed
feat: initial bitsocial-previewer
Multi-tenant link-preview + redirect server for Bitsocial clients, replacing the archived plebbit-previewer. Resolves a client profile by Host header (5chan -> s.5chan.app, seedit -> s.seedit.app), renders OpenGraph/Twitter cards via @pkcprotocol/pkc-js getComment, then redirects browsers into the app's hash route.
0 parents  commit 5e09870

17 files changed

Lines changed: 800 additions & 0 deletions

.dockerignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
.git
3+
.env
4+
*.log
5+
test
6+
.github
7+
deploy
8+
README.md
9+
.DS_Store

.env.example

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copy to .env and adjust for the host.
2+
3+
# Port Caddy proxies s.5chan.app to. Loopback bind keeps it off the public net.
4+
PORT=3924
5+
BIND_HOST=127.0.0.1
6+
7+
# ── Protocol (pkc-js) connection — pick ONE approach ─────────────────────────
8+
#
9+
# A) Recommended on VPS B: reuse the warm local pkc daemon the seeder runs.
10+
PKC_RPC_URLS=ws://127.0.0.1:9138
11+
#
12+
# B) Or run standalone over the local IPFS node (leave PKC_RPC_URLS empty):
13+
# IPFS_GATEWAY_URLS=http://127.0.0.1:6473
14+
# KUBO_RPC_URLS=http://127.0.0.1:50019/api/v0
15+
# PUBSUB_KUBO_RPC_URLS=http://127.0.0.1:50019/api/v0
16+
#
17+
# With neither set, it falls back to public gateways (fine for laptop review,
18+
# not for production).
19+
20+
DEBUG=bitsocial-previewer:*

.github/workflows/docker.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: build-and-push
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
tags: ['v*']
7+
workflow_dispatch:
8+
9+
jobs:
10+
docker:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
packages: write
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- uses: docker/metadata-action@v5
19+
id: meta
20+
with:
21+
images: ghcr.io/bitsocialnet/bitsocial-previewer
22+
tags: |
23+
type=raw,value=latest,enable={{is_default_branch}}
24+
type=ref,event=tag
25+
type=sha,format=short
26+
27+
- uses: docker/login-action@v3
28+
with:
29+
registry: ghcr.io
30+
username: ${{ github.actor }}
31+
password: ${{ secrets.GITHUB_TOKEN }}
32+
33+
- uses: docker/build-push-action@v6
34+
with:
35+
context: .
36+
push: true
37+
tags: ${{ steps.meta.outputs.tags }}
38+
labels: ${{ steps.meta.outputs.labels }}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
.env
3+
*.log
4+
.DS_Store

Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Debian-slim (not alpine): pkc-js pulls in heavy libp2p/native deps that are
2+
# happiest on glibc, matching the old previewer's node:20 base.
3+
FROM node:20-bookworm-slim
4+
5+
WORKDIR /app
6+
7+
# Install production deps first for better layer caching.
8+
COPY package.json ./
9+
RUN npm install --omit=dev --no-audit --no-fund
10+
11+
COPY . .
12+
13+
ENV NODE_ENV=production
14+
EXPOSE 3924
15+
16+
CMD ["node", "start.js"]

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# bitsocial-previewer
2+
3+
Link-preview + redirect server for Bitsocial clients. When someone shares a
4+
post link on Twitter/Telegram/Discord/etc., this server renders the
5+
OpenGraph/Twitter card for that post, then redirects real browsers into the app.
6+
7+
It is the successor to the old [`plebbit-previewer`](https://github.com/plebbit/plebbit-previewer)
8+
(archived). Unlike that one — which forced every link to a single domain — this
9+
is **multi-tenant**: one running instance serves many clients and many mirror
10+
domains, picking the right profile from the request's `Host` header.
11+
12+
```
13+
┌──────────── s.5chan.app/biz/thread/<cid>
14+
share link ───────► │ bitsocial-previewer ──► OG/Twitter card (for scrapers)
15+
(no #/ hash) └──────────── 302-ish JS redirect ──► 5chan.app/#/biz/thread/<cid>
16+
```
17+
18+
## How it works
19+
20+
1. 5chan 0.9.1 copies **path-based** share links (no `#/` hash) so the path
21+
reaches the server: `https://s.5chan.app/<board>/thread/<cid>`.
22+
2. The server matches the path to a client profile + route, fetches the post by
23+
cid via `@pkcprotocol/pkc-js` (`getComment`), and builds the card.
24+
3. Browsers get redirected into the app's hash route; scrapers read the tags.
25+
26+
`<board>` is usually a short **directory code** (e.g. `biz`). We pass it straight
27+
through to the app (which resolves it); the cid is authoritative for the preview,
28+
so the previewer never needs to resolve codes itself.
29+
30+
### Recognised share-link formats (5chan)
31+
32+
| Incoming on `s.5chan.app` | Preview | Redirects to |
33+
|---|---|---|
34+
| `/<board>/thread/<cid>` | rich (title/text/image) | `5chan.app/#/<board>/thread/<cid>` |
35+
| `/<board>/catalog` | generic board card | `5chan.app/#/<board>/catalog` |
36+
| `/<board>` | generic board card | `5chan.app/#/<board>` |
37+
| `/p/<address>/c/<cid>` (legacy) | rich | `5chan.app/#/<address>/thread/<cid>` |
38+
39+
Unresolvable cid → still redirects to the app with a generic card (short cache),
40+
so a share link never dead-ends.
41+
42+
## Layout
43+
44+
| File | Purpose |
45+
|---|---|
46+
| [`lib/clients.js`](lib/clients.js) | **The per-client design.** Profiles (hostnames, app URL, routes) + host→profile + route matching. |
47+
| [`lib/html.js`](lib/html.js) | OG/Twitter tag rendering + redirect (HTML-escaped, XSS-safe). Pure. |
48+
| [`lib/pkc.js`](lib/pkc.js) | `@pkcprotocol/pkc-js` init + cached `getComment`. |
49+
| [`lib/media.js`](lib/media.js) | Comment media URL + external-link `og:image` scraping. |
50+
| [`start.js`](start.js) | Express wiring. |
51+
| [`config.js`](config.js) | Env-driven config (port, bind host, pkc options). |
52+
53+
## Run locally
54+
55+
```bash
56+
npm install
57+
npm start # listens on 127.0.0.1:3924, public-gateway fallback
58+
# in another shell:
59+
curl -s 'http://localhost:3924/biz/thread/<cid>' | grep -i og:
60+
```
61+
62+
```bash
63+
npm test # routing + HTML unit tests (no network/install needed for the pure logic)
64+
```
65+
66+
## Deploy (Caddy)
67+
68+
Deployed under `/opt/bitsocial-previewer` like the box's other services. Config
69+
(protocol endpoints, port) is inline in [`docker-compose.yml`](docker-compose.yml),
70+
which uses `network_mode: host` so the previewer reaches the same warm local node
71+
the seeder uses (`getComment` resolves instantly from pinned 5chan content).
72+
73+
```bash
74+
# on the box, in /opt/bitsocial-previewer (source synced or git-cloned):
75+
docker compose up -d --build # builds + runs on 127.0.0.1:3924
76+
docker compose logs -f --tail=50
77+
```
78+
79+
CI also publishes `ghcr.io/bitsocialnet/bitsocial-previewer:latest`
80+
([workflow](.github/workflows/docker.yml)); once that package is made public you
81+
can `docker compose pull && docker compose up -d` instead of building on the box.
82+
83+
Point the local Caddy at it with [`deploy/Caddyfile.snippet`](deploy/Caddyfile.snippet),
84+
then do the Cloudflare DNS change in [`deploy/cloudflare.md`](deploy/cloudflare.md).
85+
86+
## Add a client or a mirror
87+
88+
- **New mirror domain for 5chan:** add it to the `5chan` profile's `hostnames`
89+
in [`lib/clients.js`](lib/clients.js), add a Caddy block + a DNS A record.
90+
- **New client (e.g. seedit):** fill in the `seedit` profile template
91+
(hostnames, `appBaseUrl`, route formats) and enable it.

config.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Runtime config, driven entirely by environment variables so the same image
2+
// runs locally and on the server. See .env.example for the production values.
3+
4+
const list = (value) =>
5+
(value || '')
6+
.split(',')
7+
.map((s) => s.trim())
8+
.filter(Boolean)
9+
10+
// Build @pkcprotocol/pkc-js options. Two modes:
11+
//
12+
// • RPC mode (recommended on VPS B): set PKC_RPC_URLS to the local daemon the
13+
// seeder already runs (ws://127.0.0.1:9138). getComment is served by that
14+
// warm, content-pinned node — no second IPFS node, fastest + most reliable.
15+
//
16+
// • Standalone mode (default, runs anywhere): in-process pkc-js reading from
17+
// IPFS gateways. On the server, point IPFS_GATEWAY_URLS at the local gateway
18+
// (http://127.0.0.1:6473); with no env set it falls back to public gateways
19+
// so `npm start` works on a laptop for review.
20+
const buildPkcOptions = () => {
21+
const options = {}
22+
23+
const rpc = list(process.env.PKC_RPC_URLS)
24+
if (rpc.length) options.pkcRpcClientsOptions = rpc
25+
26+
const kubo = list(process.env.KUBO_RPC_URLS)
27+
if (kubo.length) options.kuboRpcClientsOptions = kubo
28+
29+
const pubsub = list(process.env.PUBSUB_KUBO_RPC_URLS)
30+
if (pubsub.length) options.pubsubKuboRpcClientsOptions = pubsub
31+
32+
const gateways = list(process.env.IPFS_GATEWAY_URLS)
33+
if (gateways.length) {
34+
options.ipfsGatewayUrls = gateways
35+
} else if (!rpc.length) {
36+
// public fallback only when not delegating to a local daemon
37+
options.ipfsGatewayUrls = ['https://ipfs.io', 'https://ipfsgateway.xyz', 'https://gateway.forumindex.com']
38+
}
39+
40+
return options
41+
}
42+
43+
export default {
44+
port: Number(process.env.PORT || 3924),
45+
// Bind to loopback by default: with host networking on the server, only the
46+
// local Caddy reaches it (never the public internet). Set BIND_HOST=0.0.0.0
47+
// when running in bridge networking with a published port.
48+
bindHost: process.env.BIND_HOST || '127.0.0.1',
49+
pkcOptions: buildPkcOptions(),
50+
}

deploy/Caddyfile.snippet

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Add this to the server's Caddyfile. Caddy auto-provisions the Let's Encrypt
2+
# certificate the moment s.5chan.app resolves to this box (DNS-only A record).
3+
4+
s.5chan.app {
5+
encode zstd gzip
6+
reverse_proxy 127.0.0.1:3924
7+
}
8+
9+
# Mirror domains: list more hostnames on one block (they share the cert + the
10+
# same upstream), and add each to the 5chan profile's `hostnames` in
11+
# lib/clients.js so the server recognises them:
12+
#
13+
# s2.5chan.app, s.5chan.eth.limo {
14+
# encode zstd gzip
15+
# reverse_proxy 127.0.0.1:3924
16+
# }

deploy/cloudflare.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Cloudflare DNS for `s.5chan.app`
2+
3+
Goal: stop redirecting `s.5chan.app``5chan.app`, and instead point it at the
4+
previewer on the VPS so Caddy can serve link previews.
5+
6+
## Steps (DNS-only A record + Caddy)
7+
8+
1. **Remove the old redirect.** In the `5chan.app` zone → **Rules**, delete the
9+
Redirect Rule / Page Rule that sends `s.5chan.app``5chan.app`. Also delete
10+
any placeholder DNS record for `s` that the redirect relied on.
11+
12+
2. **Add the A record.** Zone → **DNS → Records → Add record**:
13+
- Type: `A`
14+
- Name: `s`
15+
- IPv4 address: `<your VPS IP>`
16+
- Proxy status: **DNS only** (grey cloud)
17+
- TTL: Auto
18+
19+
Grey-cloud lets Caddy obtain a Let's Encrypt cert directly and lets social
20+
scrapers (Twitter/Telegram/Discord/…) crawl the previews without challenges.
21+
22+
3. **Wait for propagation**, then confirm TLS + a real preview:
23+
```bash
24+
curl -sI https://s.5chan.app/health
25+
curl -s https://s.5chan.app/<board>/thread/<cid> | grep -i 'og:'
26+
```
27+
The HTML should contain `og:title` / `og:description` and redirect a browser
28+
into `https://5chan.app/#/<board>/thread/<cid>`.
29+
30+
4. **Validate the card** with the Twitter/Telegram/Discord link-preview tools.
31+
32+
## Later (optional): put Cloudflare in front
33+
34+
To gain Cloudflare CDN/WAF, flip the A record to **Proxied** (orange) and set
35+
**SSL/TLS → Overview → Full (strict)**. Because proxied TLS-ALPN renewal is
36+
blocked, Caddy then needs either a **Cloudflare Origin Certificate** installed,
37+
or the **DNS-01** challenge via a Cloudflare API token. Keep bot-fight settings
38+
relaxed for `s.5chan.app` so OG scrapers aren't challenged.

docker-compose.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
services:
2+
bitsocial-previewer:
3+
image: ghcr.io/bitsocialnet/bitsocial-previewer:latest
4+
build:
5+
context: .
6+
container_name: bitsocial-previewer
7+
restart: unless-stopped
8+
9+
# Host networking so the previewer reaches the local node the seeder uses
10+
# (127.0.0.1:9138 / :50019 / :6473). BIND_HOST=127.0.0.1 keeps the preview
11+
# port private to the host, so only the local Caddy can proxy to it.
12+
network_mode: host
13+
14+
environment:
15+
NODE_ENV: production
16+
PORT: "3924"
17+
BIND_HOST: 127.0.0.1
18+
DEBUG: bitsocial-previewer:*
19+
20+
# Protocol fetch: reuse the warm local pkc daemon (same as the seeder).
21+
PKC_RPC_URLS: ws://127.0.0.1:9138
22+
# Decoupled alternative — comment out PKC_RPC_URLS and use the local node:
23+
# IPFS_GATEWAY_URLS: http://127.0.0.1:6473
24+
# KUBO_RPC_URLS: http://127.0.0.1:50019/api/v0
25+
# PUBSUB_KUBO_RPC_URLS: http://127.0.0.1:50019/api/v0
26+
27+
logging:
28+
driver: json-file
29+
options:
30+
max-size: 10m
31+
max-file: "5"

0 commit comments

Comments
 (0)