diff options
| author | grm <grm@eyesin.space> | 2026-09-12 11:57:18 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-12 11:57:18 +0300 |
| commit | bb19bb9fe26ea229fb9d8beb680149558f0bb9f5 (patch) | |
| tree | a319f9743143854b36e0c667b25788f9059d208d | |
| parent | f0eaf46755e04b806e2df07a2fabd4060a72d54c (diff) | |
| download | blogspace-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.md | 124 |
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. |
