aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-12 11:57:18 +0300
committergrm <grm@eyesin.space>2026-09-12 11:57:18 +0300
commitbb19bb9fe26ea229fb9d8beb680149558f0bb9f5 (patch)
treea319f9743143854b36e0c667b25788f9059d208d
parentf0eaf46755e04b806e2df07a2fabd4060a72d54c (diff)
downloadblogspace-bb19bb9fe26ea229fb9d8beb680149558f0bb9f5.tar.gz
blogspace-bb19bb9fe26ea229fb9d8beb680149558f0bb9f5.tar.bz2
blogspace-bb19bb9fe26ea229fb9d8beb680149558f0bb9f5.zip
Add AGENTS.md for AI coding sessions
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
-rw-r--r--AGENTS.md124
1 files changed, 124 insertions, 0 deletions
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..a880214
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,124 @@
+# AGENTS.md — notes for AI coding sessions
+
+**Keep this file up to date.** When you change conventions, routes, the data
+model, the dev workflow or anything else described here, update this file in
+the same commit. It is the first thing a future session reads.
+
+## What this is
+
+Blogspace: a multi-tenant blog host. One Go binary + Postgres. The base domain
+serves the superadmin's own blog plus the management site (`/login`,
+`/dashboard`, `/b/<sub>/…`, `/admin/`); every other blog is served at
+`<sub>.BASE_DOMAIN`. See `README.md` for the user-facing description, config
+table and deployment notes.
+
+## Hard constraints (from the product owner)
+
+- **No JavaScript required.** Pages must work with JS disabled, on old
+ browsers and on phones. Forms only; destructive actions use a GET confirm
+ page followed by a POST. Tiny progressive enhancements are OK if the
+ page works without them (see `posts.html` filter `<noscript>` button).
+- Server-rendered `html/template` only. No frontend build step, no
+ framework, no CDN assets.
+- Postgres holds **everything**, including images (`images.data bytea`).
+- Posts are Markdown (goldmark → bluemonday). No WYSIWYG.
+- Changes are live immediately — there is no draft/preview system. The UX
+ is "save, then refresh your blog tab"; keep the "View blog ↗" links.
+- Theme customisation is a structured form only; **no custom CSS input**.
+- App runs plain HTTP behind a reverse proxy; auth is a JWT cookie.
+- Stdlib `net/http` ServeMux with method+pattern routes. No router library.
+
+## Layout
+
+```
+cmd/blogspace/ main.go (serve|seed|migrate, superadmin + root blog bootstrap), seed.go
+internal/config/ env → Config; RootSubdomain = "www"
+internal/db/ pgxpool + goose; migrations/*.sql embedded
+internal/store/ models + hand-written SQL (users, blogs, pages, posts, images)
+internal/auth/ bcrypt, JWT issue/parse, cookie, HMAC CSRF
+internal/markdown/ Render(md) → sanitized HTML
+internal/slug/ Make/Valid/WithSuffix (Greek transliteration included)
+internal/web/ server.go (host router, middleware, render helpers)
+ routes.go (all routes), handlers_*.go, theme.go
+ templates/ (embedded; layouts/, partials/, auth/, dashboard/, admin/, blog/)
+ static/ (dashboard.css, blog.css)
+```
+
+## Key mechanics
+
+- **Host routing** (`web/server.go` `ServeHTTP`): base domain or `www.` →
+ root mux with `ctxHostSub = "www"`; `<sub>.base` → subdomain mux; anything
+ else 404. Public blog handlers get the blog via the `hostBlog` wrapper;
+ management handlers get it via `withBlog` (`/b/{sub}/…`, owner or
+ superadmin). Both put it in `ctxBlog`; templates see it as `.Blog`.
+- **Root blog** is a normal `blogs` row with subdomain `www`, owned by the
+ first superadmin, created by `ensureRootBlog` on startup. Managed at
+ `/b/www/`. `Config.BlogURL("www")` returns the root URL.
+- **Route precedence**: on the root mux the blog's `GET /{page}` and
+ `GET /{page}/{post}` coexist with literal management routes. Any new
+ top-level management path must be added to `reservedPageSlugs`
+ (`handlers_pages.go`) and to `TestRootRoutePrecedence`. `/static/{file}`
+ is a single segment on purpose (a `/static/` prefix pattern conflicts
+ with `/{page}/{post}`); static files must stay flat.
+- **Auth**: HS256 JWT in `session` cookie (`HttpOnly`, `SameSite=Lax`,
+ 7 days). Claims carry `uid` + `ver` (= `users.token_version`); the
+ session middleware re-loads the user every request and drops the session
+ if disabled or version mismatch. Password change / reset / disable bump
+ `token_version`. Every POST behind `requireAuth` must include
+ `<input type="hidden" name="_csrf" value="{{.CSRF}}">`; the check runs in
+ `requireAuth`, which also caps the body at `MAX_UPLOAD_MB + 1 MB`.
+- **Templates**: each page file is parsed together with its layout
+ (`layouts/dashboard.html` or `layouts/blog.html` for `blog/*`) and all
+ `partials/*.html`. Page files define `content` (and optionally `title`).
+ Handlers call `s.render(w, r, "dir/file.html", map[string]any{...})`;
+ page data is under `.Data.<key>`, common fields (`.User`, `.Blog`,
+ `.CSRF`, `.Flash`, `.Error`, `.BlogURL`, `.RootURL`) are top level.
+ Flash messages travel as `?ok=` query param via `redirectOK`. Add new
+ templates to `TestAllTemplatesParse`.
+- **Theme** (`web/theme.go`): struct stored as jsonb on `blogs.theme`.
+ `ParseTheme` merges over `DefaultTheme()` and `normalize()` clamps every
+ value to an allowlist (hex colours, enum strings, uuid image ids) — this
+ is what makes inlining the generated CSS safe. Add new options in the
+ struct, `normalize`, `ThemeFromForm`, the CSS template and
+ `dashboard/design.html`.
+- **Images**: `/media/{uuid}` served on every host with immutable cache
+ headers. Uploads are content-sniffed (png/jpeg/gif/webp). Deleting an
+ image clears theme references to it.
+- **Slugs**: auto-generated from the title; on collision generated slugs
+ get `-2`, `-3`…, user-typed slugs return a 409 with a message.
+- **Errors**: `store.ErrNotFound` / `store.ErrConflict` are the sentinels
+ (`store.wrap`). Handlers use `s.serverError` (logs, 500) and
+ `s.plainError(status, msg)` (small standalone HTML page).
+
+## Dev workflow
+
+```sh
+make dev # postgres in docker + app (DEV=1: templates/static reloaded from disk)
+make seed # admin/admin (superadmin, owns root blog), alice/alicealice
+make test # unit tests; no DB needed
+make db-reset # wipe dev data
+```
+Port 8080 may be taken on the owner's machine: `make dev ADDR=:8090 PUBLIC_PORT=8090`.
+Browsers resolve `*.blogspace.localhost` to loopback; with curl use
+`-H 'Host: alice.blogspace.localhost' localhost:8090/…`.
+
+`DEV=1` also allows a missing `JWT_SECRET` and defaults the bootstrap
+superadmin password to `admin`. Production refuses both.
+
+## Conventions
+
+- Run `gofmt -w .`, `go vet ./...`, `go test ./...` before committing.
+- Migrations: add `internal/db/migrations/NNNNN_name.sql` with goose
+ `-- +goose Up/Down` sections; they run automatically at startup. Never
+ edit an applied migration.
+- New store methods take `ctx` first and scope queries by `blog_id` so a
+ blogger can never touch another blog's rows.
+- Keep handlers thin: validate → call store → `redirectOK`. Re-render the
+ form with `"error"` in the data map on validation failure (4xx status).
+- Match the existing comment density; comments explain *why*, not *what*.
+- Commit messages: short imperative subject, body explains the reason.
+
+## Things deliberately not built (ask before adding)
+
+Drafts/preview, custom CSS, comments, multiple blogs per user, email,
+user self-registration, custom domains per blog, image resizing, search.