khannurien e4d50f047c
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s
v3: the journal feed breathes — one mosaic running unbroken down the page reads as a slab, so the composer now cuts it into sub-blocks, each its own grid with a themable rule between. A break can only fall where a row starts clean and isn't reached into from above, which the exact tiling makes cheap to test: every row closes flush, so the only boundaries ruled out are the ones a portrait or a hero straddles.
Two things set the rhythm, and neither is a row counter — that beats like a metronome. It comes due on a count of *cards*: a block is a set of stories and a hero is one story however many cells it takes, so a stretch thick with big footprints reaches the count over more rows than a stretch of squares does, and block heights end up tracking the size mix (measured: four to five rows through square-heavy runs, six to seven where art clusters). And the break *makes* its own section front rather than waiting for one, promoting the card that opens a new block to a hero. Waiting was the first attempt and it failed on the numbers — a banner lands at the start of a row on its own two to five times in thirty-six, so the preference almost never fired and a max-rows cap did all the work. Promotion always fits, since a break only falls on a row with all four cells free. A tail guard keeps the last cut from leaving a rule with a fragment under it.

The break sits between two grids rather than inside one: a grid item would be stretched to the 9rem row track, and out here its height is its own, down to nothing. Three custom properties are the whole contract a theme writes against — the air around it, the rule through it, an optional glyph centred on that rule — with `none` on the last two meaning the real absence of each, so a theme can remove the separation entirely rather than leave an empty band. Themes wanting something a rule can't be reach past the properties to plain CSS: nyt pairs hairlines in the grid's own pen and clips them to the art inset (which moves up to the feed, since a property on the grid can't reach a sibling); brutalist spends air around the 2px border it already divides everything else with; kanagawa takes a comment divider, geocities the rainbow its header ends on, melange a thinner band of the mélange. The page gutter moves to a feed wrapper so every block lines up on the same edges, and nyt's masking frame now runs once per block, which is what makes each of them read frameless and not just the feed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:01:42 +00:00
2026-06-29 20:25:10 +00:00
2026-06-27 10:42:31 +00:00
2026-03-15 17:15:46 +00:00

gerbeur

A small invite-only social platform for sharing links and files. Users can post URLs and media (YouTube, SoundCloud, Bandcamp, images, audio, video, …), vote, comment, follow each other, build playlists, and search content. A real-time WebSocket layer handles live presence, vote counts, and notifications. The UI is localized (English and French) and ships multiple visual themes.

Stack

Layer Technology
Runtime Deno 2.x
API Oak (HTTP + WebSocket)
Database SQLite via node:sqlite
Frontend React 19 + Vite 8

Development

Prerequisites

  • Deno 2.x

Setup

cp .env.example .env
# Edit .env: set GERBEUR_JWT_SECRET to the output of: openssl rand -hex 32

Run

deno task dev

This starts both the API server (port 8000, with file watching) and the Vite dev server (port 3000) concurrently.

Open http://localhost:3000. On first run a default admin / admin account is created — change the password immediately.

Environment variables

See .env.example for the full list with descriptions. Key variables:

Variable Description Default
GERBEUR_JWT_SECRET JWT signing secret — required, generate with openssl rand -hex 32 —
GERBEUR_PUBLIC_URL Public-facing URL of the server (no trailing slash) — used for CORS, WebSocket origin, email links, OG URLs http://localhost:GERBEUR_PORT
GERBEUR_PORT Internal port Oak listens on 8000
GERBEUR_LISTEN_HOST Network interface Oak binds to; use 127.0.0.1 to restrict to loopback 0.0.0.0
GERBEUR_FRONTEND_URL Frontend base URL for email links and CORS; auto-added to allowed origins — the only variable needed when the frontend runs on a separate host GERBEUR_PUBLIC_URL
GERBEUR_ALLOWED_ORIGINS Comma-separated extra origins for CORS/WebSocket; PUBLIC_URL and FRONTEND_URL are always included — typically only needed in dev for the Vite server "" (empty)
GERBEUR_SITE_NAME Site name used in the browser tab, app header, OG meta tags, and emails gerbeur
GERBEUR_SMTPS_URL SMTPS connection URL for outgoing email (smtps://user:pass@host:465) unset
GERBEUR_FROM_EMAIL Sender address for outgoing emails — required when GERBEUR_SMTPS_URL is set unset
GERBEUR_WELCOME_EMAIL_BODY Markdown body for the account-creation welcome email; supports {{username}} and {{site_name}} built-in template
VITE_API_PROTOCOL API protocol baked into the frontend bundle (see Production) http
VITE_API_HOSTNAME API hostname baked into the frontend bundle localhost
VITE_API_PORT API port baked into the frontend bundle 8000

Production

The standard deployment runs API and frontend in a single container. The API server (Oak) serves the compiled frontend as static files, so both share the same origin — no VITE_API_* build args needed. Set GERBEUR_PUBLIC_URL to the externally-visible URL; it is automatically allowed for HTTP/WebSocket requests.

docker build -t gerbeur .

docker build -t gerbeur .

docker run -d \
  -p 8000:8000 \
  -v gerbeur-db:/app/api/sql \
  -v gerbeur-uploads:/app/api/uploads \
  -e GERBEUR_JWT_SECRET=$(openssl rand -hex 32) \
  -e GERBEUR_PUBLIC_URL=https://example.com \
  -e GERBEUR_SITE_NAME=mysite \
  --name gerbeur \
  gerbeur

The two volumes are required for persistence:

  • gerbeur-db — SQLite database (api/sql/gerbeur.db), initialized automatically on first run
  • gerbeur-uploads — user-uploaded files (api/uploads/)

Separate API and frontend (optional)

If you need to run the API on a different host than the frontend, pass the API location as build args so it gets baked into the frontend bundle. In that setup, add the frontend origin to GERBEUR_ALLOWED_ORIGINS so cross-origin HTTP/WebSocket requests are accepted:

docker build \
  --build-arg VITE_API_PROTOCOL=https \
  --build-arg VITE_API_HOSTNAME=api.example.com \
  --build-arg VITE_API_PORT=443 \
  -t gerbeur .

Reverse proxy

Put a reverse proxy (nginx, Caddy, …) in front of the container to handle TLS. Forward everything to port 8000. Example Caddyfile:

example.com {
    reverse_proxy localhost:8000
}

Project structure

api/
  main.ts          # Entry point — Oak application, middleware, routes
  config.ts        # Environment variables
  middleware/      # errorMiddleware, authMiddleware
  routes/          # HTTP routes + WebSocket
  services/        # Business logic
    providers/     # Link metadata providers (YouTube, SoundCloud, Bandcamp, …)
  model/           # Row types, type guards
  lib/             # JWT, pagination, slugify, upload, static helpers, …
  db/
    schema.sql     # Database schema
    init.ts        # First-run database initialisation
    migrate.ts     # Migration runner
    migrations/    # Versioned schema migrations
  sql/
    gerbeur.db     # SQLite database (not committed)
  uploads/         # User-uploaded files — avatars, dumps, thumbnails, … (not committed)
src/               # React frontend (Vite)
  config/          # API base URL, validation constants, feed tabs, upload limits
  pages/           # Route-level components
    index/         # Feed views (hot, new, followed, journal)
  components/      # Shared UI components
    form/          # Reusable form controls
  contexts/        # Auth, WebSocket, player, follows, theme
  hooks/           # Data fetching and UI hooks
  utils/           # Formatting, URL, hot-score, waveform, … helpers
  locales/         # Lingui message catalogues (en, fr)
  themes/          # Per-theme CSS files
  model.ts         # Shared frontend types
  i18n.ts          # Lingui runtime setup
public/            # Static assets (favicon, manifest, service worker)
Description
🚚 tiny community engine for sharing bookmarks
https://pub.sqrt.fr/vincent/gerbeur/
Readme MIT 11 MiB
Languages
TypeScript 87.9%
CSS 10.4%
JavaScript 1.5%