diff options
| author | grm <grm@eyesin.space> | 2026-09-13 00:38:24 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-13 00:38:24 +0300 |
| commit | d11e4c9544f544b3a024c7ac705a674b4096cf50 (patch) | |
| tree | a60be98a37b764dfcb655b2738c8a991a248103b /AGENTS.md | |
| parent | e974eb6f3feb06c8951f85c1d34161938905f807 (diff) | |
| download | blogspace-d11e4c9544f544b3a024c7ac705a674b4096cf50.tar.gz blogspace-d11e4c9544f544b3a024c7ac705a674b4096cf50.tar.bz2 blogspace-d11e4c9544f544b3a024c7ac705a674b4096cf50.zip | |
Add the Layout tab: five areas with modules, columns and a menu editor
A blog page is now header, left column, main content, right column and
footer, each holding an ordered list of modules (blog title, logo, menu,
archive by year/month, recent posts, custom HTML, footer text, RSS link,
site map). Side columns have percentage widths, can be hidden, and can
keep their space when empty; on phones they stack under the posts. The
menu becomes its own ordered list mixing pages and outside links.
Announcements are placed per column at its top or bottom.
Modules and menu items get tables; the migration derives them from each
blog's old theme (nav position, footer text, show-in-nav pages) so
existing blogs look the same. Custom HTML is deliberately stored and
served as-is (owner's decision; see AGENTS.md for why the blast radius
is the blogger's own origin).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'AGENTS.md')
| -rw-r--r-- | AGENTS.md | 76 |
1 files changed, 56 insertions, 20 deletions
@@ -29,6 +29,11 @@ table and deployment notes. - 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**. + The one escape hatch is the **custom HTML module** (Layout tab), which the + owner chose to store and output **unsanitised**. The blast radius is the + blogger's own origin: the session cookie is host-only on the base domain + and `HttpOnly`, management POSTs need the HMAC `_csrf` token, and the + base-domain blog is only editable by the superadmin. - App runs plain HTTP behind a reverse proxy; auth is a JWT cookie. - Stdlib `net/http` ServeMux with method+pattern routes. No router library. @@ -38,14 +43,16 @@ table and deployment notes. 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, sections) +internal/store/ models + hand-written SQL (users, blogs, pages, posts, images, sections, modules, menu) 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 (sections = announcements), theme.go + routes.go (all routes), handlers_*.go (sections = announcements, layout = modules + menu) + theme.go (colours/fonts/layout switches), layout.go (module kinds, archive grouping) templates/ (embedded; layouts/, partials/, auth/, dashboard/, admin/, blog/) - static/ (dashboard.css "paper & ink" look — plain CSS, no variables/flex; blog.css) + static/ (dashboard.css "paper & ink" look — plain CSS, no variables/flex; + blog.css — floats except the three-column `.cols`, which is flexbox) ``` ## Key mechanics @@ -79,27 +86,53 @@ internal/web/ server.go (host router, middleware, render helpers) 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 + Flash messages travel as `?ok=` query param via `redirectOK` (a `#anchor` + in the target is kept after the query). 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`; defaults must reproduce the look blogs had + value to an allowlist (hex colours, enum strings, uuid image ids, column + percentages) — this is what makes inlining the generated CSS safe. Add + new options in the struct, `normalize`, `ThemeFromForm`, the CSS template + and `dashboard/design.html`; defaults must reproduce the look blogs had before the option existed. Non-CSS options (`ShowDates`, `DateFormat` via - `FormatDate`, `FooterShowRSS`, `Favicon`) are read by the blog templates. + `FormatDate`, `Favicon`, `Logo`) are read by the blog templates. Optional colours (`LinkHover`, `NavHover`) are `""` = inherit and come - from a colour input paired with a `<name>_custom` checkbox. Image fields + from a colour input paired with a `<name>_custom` checkbox, shown/hidden + with CSS only (`.hoverpick > input:not(:checked) ~ …`). Image fields use the `imagepick` partial's radios: `none` clears, a uuid selects, no value keeps (`pickImage`); an upload in `<name>_file` wins. - Options that only apply to one menu layout (`NavAlign` for horizontal - menus, `NavSidebarWidth` for the sidebar) are shown/hidden in - `design.html` with CSS only (`#np-*:checked ~ .only-*` in dashboard.css, - same trick for the `_custom` hover checkboxes) — no JS, and browsers - without `:checked` just show everything. `Presets()`/`WithPreset` are the colour schemes (`POST /b/{sub}/design/preset`, - colours only); `POST /b/{sub}/design/reset` stores `DefaultTheme()`. + colours only); `POST /b/{sub}/design/reset` stores `DefaultTheme()` and + calls `ResetModules`. + The layout switches (`HeaderOn/LeftOn/RightOn/FooterOn`, `LeftWidth`, + `RightWidth`, `KeepColumns`) also live in the theme but are edited from the Layout tab + (`LayoutFromForm`, `SetAreaOn`); `ThemeFromForm` never touches them. +- **Layout** (`modules` table, `store/modules.go`, `web/layout.go`, + `handlers_layout.go`, `/b/{sub}/layout…`): a blog page is six areas — + `header`, `left`, `above`, `below` (the two custom-HTML slots inside the + main column, one module at most), `right`, `footer` — each an ordered + list of modules (`kind`: title, logo, menu, archive, recent, html, rss, + text, sitemap; `moduleKinds` says which kinds an area accepts). Modules + have an optional `title` (heading), `body` (raw HTML or footer text) and + `count` (recent posts). `blogView` builds a `Layout` (`buildLayout`, + areas that are switched off are dropped) and only fetches the archive + index / recent posts when a visible module needs them. A side column is + laid out only when it has modules, or always with `theme.keep_columns` + (`HasLeft/HasRight` → `body.has-*` + classes, which also lift the `.wrap` max-width so the page goes + full-width; column widths in the theme CSS under `@media (min-width: 701px)`); + below 700px `.cols` becomes block flow so the order is main → left → + right → footer. Everything is rendered by the `module` template in + `layouts/blog.html`. New blogs get `insertDefaultModules` (title + menu + in the header, RSS in the footer) — keep it in step with the layout + migration's defaults. +- **Menu** (`menu_items` table, `store/menu.go`): the single ordered list + the Menu module shows — page entries (`page_id`, unique per page, cascade + on delete) and custom links (`label` + `url`, validated by + `validLinkURL`: http(s), mailto or a `/path`). `Page.ShowInNav` is + derived (`EXISTS` on `menu_items`); `CreatePage`/`UpdatePage` add or + remove the entry so the page form's checkbox and the Layout tab agree. - **Favicon**: `static/favicon.svg` + `favicon.png` are the defaults, linked from both layouts. A blog can set `theme.favicon` (an image id) which the blog layout links instead. `GET /favicon.ico` on every host @@ -116,9 +149,11 @@ internal/web/ server.go (host router, middleware, render helpers) `s.readUpload(r, "inline_image")` + `appendImageMD` (the no-JS path). - **Announcements** (`sections` table, `store/sections.go`, `handlers_sections.go`, `/b/{sub}/announcements…`): per-blog notices with - `placement` (above|below|sidebar), `style` (plain|note|warning), `enabled` - and `sort_order`. `blogView` loads the enabled ones and `splitSections` - routes `sidebar` to the left-sidebar layout only (otherwise above). + `placement` (`<column>-<position>`: left|main|right × top|bottom, split by + `Section.Column()/Position()`), `style` (plain|note|warning), `enabled` + and `sort_order`. `blogView` loads the enabled ones and `placeNotices` + groups them by placement, moving ones for a side column that is not laid + out into the main column. Rendered by the `notices` template in `layouts/blog.html`; CSS classes `notice notice-<style>` use translucent colours so they fit any theme. - **Dashboard nav**: `dashnav.html` marks the current tab with `hasPrefix @@ -160,4 +195,5 @@ superadmin password to `admin`. Production refuses both. ## 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. +user self-registration, custom domains per blog, image resizing, search, +sanitising the custom HTML module (owner's decision, see above). |
