Image board style fedi front end with a emphasis on gallery view
  • JavaScript 68.6%
  • CSS 28.9%
  • HTML 2.4%
  • Python 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-07 12:05:13 -07:00
public Generate complete Unicode emoji search index 2026-09-07 12:05:13 -07:00
screenshots Add README screenshots 2026-07-12 20:31:08 -07:00
tests Generate complete Unicode emoji search index 2026-09-07 12:05:13 -07:00
tools Generate complete Unicode emoji search index 2026-09-07 12:05:13 -07:00
.gitignore Stop tracking TODO.md (private scratch pad) 2026-07-21 20:36:17 -07:00
AGENTS.md Generate complete Unicode emoji search index 2026-09-07 12:05:13 -07:00
Caddyfile.example Prepare project for initial release 2026-07-12 02:25:19 -07:00
CHANGELOG.md Put a post's attachments in the order you drag them into 2026-08-18 01:56:17 -07:00
DEV_NOTES.md Render post references as >> quotelinks and quote via FEP-e232 2026-08-02 18:51:28 -07:00
LICENSE Prepare project for initial release 2026-07-12 02:25:19 -07:00
package-lock.json Release 0.3.0 2026-08-16 16:46:06 -07:00
package.json Generate complete Unicode emoji search index 2026-09-07 12:05:13 -07:00
README.md Generate complete Unicode emoji search index 2026-09-07 12:05:13 -07:00
replies_example.png Mark a stickied thread with a thumbtack, not a blue rule 2026-08-03 18:04:38 -07:00

chan-fe

Imageboard-style frontend for Mitra and Mastodon-compatible fediverse APIs.

chan-fe renders timelines as board catalogs with thread views, reactions, polls, media galleries, and posting tools. The application is a set of static files and does not require a server-side runtime of its own.

Status and compatibility

chan-fe is experimental, but actively usable for daily browsing and posting on a Mitra instance.

Mitra is the primary target and the only server exercised against in earnest. Several features use Mitra extensions, including custom post titles, emoji reactions, conversation visibility, and group-backed boards.

Mitra v5.8.0 or newer is recommended. Catalogs fetch the thread OPs a timeline page omits, and on v5.8.0+ those land in a single batched GET /api/v1/statuses?id[]=… request (up to 200 ids) using each status's conversation.root_id. Older instances answer 404 there and fall back to fetching one OP at a time — everything still works, but a busy board costs substantially more requests to load.

The frontend otherwise uses Mastodon-compatible /api/* and /oauth/* endpoints. OAuth sign-in against Pleroma has worked, but broader Pleroma/Mastodon compatibility has not been tested extensively. Expect rough edges outside Mitra.

Post references and quotes

chan-fe presents every reference to another post the same way — a red >> quotelink that previews on hover and expands in place when clicked, labelled (You), (Cross-thread) or (Dead) as it applies. The posts an OP quotes start out expanded, so a thread opened from the catalog shows what it is about; clicking the ref collapses one again. Two different things are written to the wire, because replying and quoting mean different things to every other frontend.

Replying. Clicking a post number opens the composer with >>ref in the body; clicking another while it's open appends a second. They are sent as plain markdown links to the referenced posts, so a reader on any server gets a working link and the post still reads as a reply. Only the author of the post being replied to is notified automatically (Mitra's prepare_mentions), so each further reference carries an @mention — chan-fe hides it next to the ref.

Quoting. Quote post and Copy >>ref write Mitra's [[<object-url>|>>ref]] microsyntax, which federates as a real quote (FEP-e232 object links) — several per post, up to Mitra's limit of 10. Other frontends render a quote as an embedded card, which suits "talking about this post" and is why replies don't use it. Note that quoting notifies nobody, on any server.

Two constraints come from Mitra and shape what the composer offers: only public posts can be quoted, and only public posts may carry quotes. A reference in a followers-only or direct post is posted as a plain markdown link instead, and quoting is offered as public-only.

Servers other than Mitra don't parse the microsyntax, so signing in to one falls back to plain markdown links plus the single-quote quote_id parameter. In the other direction Pleroma, Mastodon, and Misskey keep the first reference as their one quote and show the rest as links in the post body — which is what chan-fe renders them as anyway.

Highlights

  • 4chan-style board catalogs and chronological thread views
  • Mitra group-backed custom boards
  • >> post references with hover previews, and multi-post quotes (FEP-e232)
  • OAuth sign-in, posting, replies, editing, moderation, polls, and media uploads
  • Likes, reposts, bookmarks, and Unicode/custom emoji reactions
  • Inline reply and profile previews, media galleries, and opt-in media embeds
  • Local hashtag catalogs linked from posts and federated search results
  • Thread watcher and paginated fediverse notifications
  • Configurable NSFW handling, activity previews, mention collapsing, and an Anonymous presentation mode
  • No required build step

See CHANGELOG.md for the detailed initial feature inventory and future release notes.

Installation

Clone the repository onto the machine that will serve the frontend:

git clone https://codeberg.org/nak/chan-fe.git
cd chan-fe

The simplest deployment is to let Mitra serve chan-fe itself by pointing its web_client_dir setting at this public/ directory — chan-fe then replaces mitra-web as the instance's main frontend. See Serve chan-fe from Mitra directly.

Alternatively, serve public/ from a separate HTTPS origin whose hostname is the default Mitra instance, and reverse-proxy /api/*, /oauth/*, and /media/* to Mitra. Either keeps normal API requests same-origin while still allowing users to sign in to other compatible instances directly.

No Node.js installation or build step is required when serving public/ directly.

Configuration

Frontend configuration lives in public/config.js. It does not contain generated credentials or secrets; OAuth applications are registered dynamically with the selected instance.

OAUTH_CONFIG.defaultInstance defaults to the hostname serving chan-fe. This is correct when that hostname reverse-proxies the Mitra endpoints described above. If the frontend uses a different hostname, either set the default in public/config.js or provide it to the optional production build:

npm run build -- --default-instance example.com

The configured instance is only the sign-in dialog's default. Users may still enter another compatible instance.

Board definitions, labels, and timeline API paths also live in public/config.js.

Deployment

Serve chan-fe from Mitra directly

The simplest way to run chan-fe as your instance's main frontend is to let Mitra serve it, replacing its default web client. Clone the repository onto the Mitra host and point Mitra's top-level web_client_dir setting at the public/ directory:

# Mitra config.yaml
web_client_dir: /srv/chan-fe/public

Restart Mitra. It now serves chan-fe at its own origin, so /api/*, /oauth/*, and /media/* are same-origin and OAUTH_CONFIG.defaultInstance — which defaults to the serving hostname — is correct with no extra configuration. No separate web server or reverse proxy is needed; sign-in to other compatible instances still works directly at their own origins.

For a minified bundle, run the production build and point web_client_dir at dist/ instead:

npm install
npm run build
# then set web_client_dir to the dist/ directory, e.g. /srv/chan-fe/dist

Serve the static files directly

Point the web root at public/. Route API requests before the static-file SPA fallback:

  1. Reverse-proxy /api/*, /oauth/*, and /media/* to Mitra.
  2. Serve existing static files.
  3. Fall back to index.html for client-side routes.

Caddyfile.example contains a reference configuration with the required route order and suggested security headers. Copy the relevant blocks into your existing Caddy configuration rather than replacing it wholesale.

If the static-file handler runs before the proxy rules, API requests may receive index.html instead of JSON.

Optional production build

The optional build bundles and minifies JavaScript into dist/app.js, writes external source maps, and copies the other static assets into dist/. Large on-demand features such as the complete emoji search index are emitted as lazy chunks under dist/chunks/:

npm install
npm run build

Serve dist/ instead of public/ after building. To bake in an instance on a different hostname:

npm run build -- --default-instance example.com

Deployment helper

The deployment helper builds and uploads dist/ with rsync:

npm run deploy -- example.com user@frontend.example:/var/www/chan-fe/

It uses --delete and --delay-updates. Check the destination carefully: files at the destination that are absent from dist/ will be removed.

Screenshots

Catalog Thread view
Board catalog Thread view
Compose Emoji picker Media gallery
Compose modal Emoji picker Media gallery

Development

There is no required development build step. Application code lives in public/ and is plain JavaScript, HTML, and CSS.

Local static server

python3 tools/serve.py --port 8080
# open http://localhost:8080/

tools/serve.py sends Cache-Control: no-cache so browser refreshes reliably pick up JavaScript and CSS edits.

You can also use Python's standard static server:

python3 -m http.server 8080 --directory public

When developing on localhost, set the default instance in public/config.js or use the production build flag described above. The live instance must allow the required cross-origin API requests unless a local reverse proxy is used.

Project layout

public/
├── index.html          # app shell and modals
├── style.css           # application styles
├── config.js           # OAuth, board, and endpoint configuration
├── catalog.js          # catalog sorting and thread-root helpers
├── threading.js        # thread-root helpers for contexts and parent maps
├── media.js            # media size, dimension, URL, and format helpers
├── gallery.js          # multi-attachment gallery overlay
├── data.js             # API access and timeline hydration
├── emoji.js            # emoji search helpers and curated aliases
├── emoji-data.js       # generated Unicode/CLDR emoji search index
├── UNICODE-LICENSE.txt # license for the Unicode/CLDR source data
├── app.js              # routing, state, rendering, compose, auth, and interactions
├── images/             # local presentation assets
├── css/                # vendored Font Awesome stylesheets
└── webfonts/           # vendored Font Awesome font files

tools/
├── build.mjs           # optional production bundle
├── update-emoji-data.mjs # regenerate the pinned emoji search index
├── deploy.sh           # build and rsync helper
└── serve.py            # local no-cache static server

tests/
├── catalog.test.js
├── data.test.js
├── security.test.js
└── threading.test.js

screenshots/            # README screenshots

Testing

The Node built-in test suite covers the core pure-logic modules and URL-scheme security checks:

npm test

To refresh the committed emoji index after intentionally updating the pinned Unicode Emoji and CLDR versions in tools/update-emoji-data.mjs:

npm run emoji:update

Watch mode:

npm run test:watch

See DEV_NOTES.md for architecture notes and browser verification guidance.

Security

  • Fediverse post, profile, and instance HTML passes through a local tag, attribute, and URL-scheme allowlist before reaching the rendered DOM.
  • YouTube and PeerTube frames are created only after an explicit click. PeerTube URLs must be HTTPS canonical /w/<video-id> links, are converted to constrained embed URLs, and run in a sandbox without forms, popups, top-level navigation, or downloads.
  • OAuth bearer tokens are stored in localStorage. This keeps the static app simple, but means any same-origin script execution could read the token. Serve chan-fe over HTTPS, retain a restrictive CSP, do not add untrusted third-party scripts, and keep the frontend origin under your control.
  • The API wrapper attaches a bearer token only to the selected signed-in instance origin or the configured same-origin reverse proxy.

PeerTube embeds may originate on any HTTPS instance, so the example CSP uses frame-src https:. Tighten connect-src, img-src, media-src, and frame-src when deploying against a fixed set of instances.

Reference projects

  • bloat-fe — authentication flow inspiration
  • mitra-web — API behavior reference
  • FChannel — federated imageboard server with a different architecture

License

chan-fe is released under the MIT License.