diff options
| author | gramanas <grm@eyesin.space> | 2026-09-12 11:24:17 +0300 |
|---|---|---|
| committer | gramanas <grm@eyesin.space> | 2026-09-12 11:24:17 +0300 |
| commit | 3eb04b1a2bdf9e53231fe862cfd76327371a9741 (patch) | |
| tree | b38b2d82a47233fd8e0bb18c59e4a8f3dd2412d7 /README.md | |
| download | blogspace-3eb04b1a2bdf9e53231fe862cfd76327371a9741.tar.gz blogspace-3eb04b1a2bdf9e53231fe862cfd76327371a9741.tar.bz2 blogspace-3eb04b1a2bdf9e53231fe862cfd76327371a9741.zip | |
Initial multi-tenant blog host
Go + Postgres application serving a management dashboard on the base
domain and one public blog per subdomain. Markdown posts organised in
pages, form-based theme customisation, image uploads stored in Postgres,
JWT cookie sessions with CSRF, superadmin user management, RSS feeds.
Docker/compose deployment and a Makefile-driven dev environment with
seed data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 113 |
1 files changed, 113 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..c88918c --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# Blogspace + +A small multi-tenant blog host. One Go binary + Postgres. Bloggers log in at +`example.com` to manage their blog; each blog is served at `<name>.example.com`. +Server-rendered HTML, no JavaScript required, works on old browsers and phones. + +- Posts and page intros are written in **Markdown** (sanitized on save). +- Every blog has **pages** (Home, About, News, …); each page holds posts. +- **Design** form: background colour/image, fonts, widths, header, menu position, footer. +- Changes are live immediately: save, then refresh the blog tab. +- A **superadmin** creates bloggers, resets passwords, disables or deletes accounts. +- Images are stored in Postgres; everything is in one database. + +## Local development + +Requirements: Go 1.26+, Docker (for Postgres). + +```sh +make dev # starts Postgres in Docker, runs the app on :8080 with template hot-reload +make seed # (another terminal) demo data: superadmin admin/admin, blogger alice/alicealice +``` + +Then open: + +- Dashboard: http://blogspace.localhost:8080 (log in as `admin` or `alice`) +- Alice's blog: http://alice.blogspace.localhost:8080 + +Chrome and Firefox resolve any `*.localhost` name to your machine, so no DNS or +`/etc/hosts` changes are needed. If your browser does not, add lines like +`127.0.0.1 blogspace.localhost alice.blogspace.localhost` to `/etc/hosts`, or set +`BASE_DOMAIN=lvh.me` (a public name that resolves to 127.0.0.1). + +Port already taken? `make dev ADDR=:8090 PUBLIC_PORT=8090`. + +Other targets: `make test`, `make db-reset` (wipe dev data), `make build`. +In dev mode (`DEV=1`) templates and CSS are read from disk on each request, so +edits under `internal/web/templates` and `internal/web/static` show up on reload; +Go changes need a restart. + +## Configuration (environment) + +| Variable | Default | Meaning | +|---|---|---| +| `BASE_DOMAIN` | `blogspace.localhost` | Root domain; blogs are `<name>.BASE_DOMAIN` | +| `ADDR` | `:8080` | Listen address | +| `PUBLIC_PORT` | — | Appended to generated blog links (dev only; unset behind a proxy on :80/:443) | +| `DATABASE_URL` | local dev DSN | Postgres connection string | +| `JWT_SECRET` | — | **Required** outside dev; long random string (`openssl rand -hex 32`) | +| `SUPERADMIN_USERNAME` / `SUPERADMIN_PASSWORD` | `admin` / — | Created on first start if no superadmin exists | +| `MAX_UPLOAD_MB` | `5` | Image upload limit | +| `DEV` | `false` | Hot-reload templates, allow missing secrets | + +Migrations run automatically at startup. + +## Deployment (Docker) + +```sh +cp .env.example .env # edit BASE_DOMAIN, JWT_SECRET, passwords +docker compose up --build -d +``` + +The app listens on `127.0.0.1:8080` (see `APP_PORT`); put a reverse proxy in +front that terminates TLS and forwards **both** the root domain and the wildcard +with the original `Host` header. DNS needs two records: `A example.com` and +`A *.example.com` (or CNAMEs) pointing at the proxy. + +nginx example: + +```nginx +server { + listen 443 ssl; + server_name example.com *.example.com; # wildcard certificate + client_max_body_size 8m; # >= MAX_UPLOAD_MB + location / { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +Caddy: `example.com, *.example.com { reverse_proxy 127.0.0.1:8080 }` (wildcard +certificates need the DNS challenge). + +Backups: dump the Postgres volume (`docker compose exec db pg_dump -U blogspace blogspace > backup.sql`). +Images live in the database, so that one dump is everything. + +## Layout + +``` +cmd/blogspace/ main (serve | seed | migrate) +internal/config/ environment → Config +internal/db/ pgx pool + goose migrations (embedded SQL) +internal/store/ models and queries (users, blogs, pages, posts, images) +internal/auth/ bcrypt, JWT cookie sessions, CSRF tokens +internal/markdown/ goldmark + bluemonday +internal/slug/ title → slug +internal/web/ host router, handlers, templates, static CSS, theme +``` + +### How requests are routed + +`Host == BASE_DOMAIN` → management site (`/login`, `/dashboard`, `/b/<sub>/…`, `/admin/`). +`Host == <sub>.BASE_DOMAIN` → that blog's public pages (`/`, `/<page>`, `/<page>/<post>`, `/feed.xml`, `/media/<id>`). +Anything else → 404. + +### Auth notes + +Sessions are HS256 JWTs in an `HttpOnly` cookie. The token carries the user's +`token_version`; changing a password or disabling a user bumps it, which logs +out every existing session. Every POST carries a `_csrf` field derived from the +same secret, so old browsers without `SameSite` support are protected too. |
