aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md113
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.