diff options
| author | grm <grm@eyesin.space> | 2026-09-15 23:12:51 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-15 23:12:51 +0300 |
| commit | 2c85ca1a46ddf4652f8fffc53d6bcfbe32b2b4ff (patch) | |
| tree | ecd5efd574b7d7463e4b2c4361c1994c4f28f561 /AGENTS.md | |
| parent | 55732bdfc25933bab26220ccd82fe5e172d9e673 (diff) | |
| download | blogspace-2c85ca1a46ddf4652f8fffc53d6bcfbe32b2b4ff.tar.gz blogspace-2c85ca1a46ddf4652f8fffc53d6bcfbe32b2b4ff.tar.bz2 blogspace-2c85ca1a46ddf4652f8fffc53d6bcfbe32b2b4ff.zip | |
Add post tags, with a tag page and two side-column modules
Posts can now carry tags, set on the post form as a checklist of the
blog's existing tags plus a comma-separated box for new ones (no JS).
A tag is a name and a unique slug, so "Go" and "go" are one tag and
Greek tags get readable URLs; tags no post uses any more are deleted.
On the blog, tags appear under the post date and link to /tag/<slug>,
which lists the published posts from every page, paginated like a page.
Two new layout modules show them: a Tags list (with counts, by use) and
a Tag cloud (alphabetical, sized by use). Both are only fetched when a
visible module needs them.
The article loop and pager move to a shared postlist partial; while
there, the pager stops adding a trailing slash — /news/?p=2 was a 404
because a {page} wildcard never matches one.
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 | 39 |
1 files changed, 31 insertions, 8 deletions
@@ -101,7 +101,11 @@ internal/web/ server.go (host router, middleware, render helpers) 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. + with `/{page}/{post}`); static files must stay flat. The public + `GET /tag/{tag}` is a literal first segment, so it beats `/{page}/{post}` + on every host (`tag` is reserved too). A `{page}` wildcard never matches + a trailing slash, so listings put `?p=N` straight after `base` (`/`, + `/news`, `/tag/go`) — `/news/?p=2` would be a 404. - **Auth**: login lives only at `/webadmin` on the root domain (deliberately not `/login`, and not linked from public pages); `/webadmin` on a blog host redirects there with `next=/b/<sub>/`. HS256 JWT in `session` cookie (`HttpOnly`, `SameSite=Lax`, @@ -151,12 +155,13 @@ internal/web/ server.go (host router, middleware, render helpers) `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 + list of modules (`kind`: title, logo, menu, archive, recent, tags, + tagcloud, 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; tags to list, 0 = all). `blogView` + builds a `Layout` (`buildLayout`, areas that are switched off are dropped) + and only fetches the archive index / recent posts / tag counts when a + visible module needs them (`NeedsArchive/MaxRecent/NeedsTags`). A side column is laid out only when it has modules, or always with `theme.keep_columns` (`HasLeft/HasRight` → `body.has-*` classes; column widths in the theme CSS under `@media (min-width: 701px)`). @@ -268,8 +273,26 @@ internal/web/ server.go (host router, middleware, render helpers) drives ordering, the archive, the feed and the displayed dates. Blank keeps the current value; a new post starts at now. Times are in the server's zone, which is also the one dates are rendered in. +- **Tags** (`tags` + `post_tags` tables, `store/tags.go`, `web/tags.go`): + a tag is `name` + unique `slug` (`slug.Make(name)`), so the slug is both + the URL (`/tag/<slug>`, `handleBlogTag`, published posts of every page, + paginated) and the dedupe key — "Go" and "go" are one tag, the first + spelling wins. `Post.Tags` rides along in `postCols` as two arrays. + The post form offers the blog's existing tags as checkboxes (`tag`) plus + a comma-separated `new_tags` box; `parseTags` normalises both into one + list (20 per post, 40 runes each; nothing slug-worthy → dropped) and + `SetPostTags` stores it after the post is saved. Tags no post carries + are deleted (`deleteOrphanTags`, also after post/page deletion), so the + form only lists tags in use. `TagCounts` counts published posts only: + a tag on hidden posts alone shows nowhere public and its page is a 404. + Tags render under the post date (`posttags` in `partials/postlist.html`, + which also holds the article loop + pager shared by `blog/page.html` + and `blog/tag.html`); the two modules are `tags` (by use, `count`) and + `tagcloud` (alphabetical, `cloudSizes` → `tc-1…tc-5`). - **Slugs**: auto-generated from the title; on collision generated slugs get `-2`, `-3`…, user-typed slugs return a 409 with a message. + `slug.Clean` is `Make` without the "untitled" fallback, for inputs that + should be rejected instead (tags). - **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). @@ -297,7 +320,7 @@ superadmin password to `admin`. Production refuses both. `internal/db/migrations/control/` for users and the registry; goose `-- +goose Up/Down` sections; they run automatically at startup. Never edit an applied migration. Blog chain so far: `00001_init`, `00002_language`, - `00003_files`; control: `00001_init`, `00002_upload_limit`. Both chains were re-baselined at 00001 after + `00003_files`, `00004_tags`; control: `00001_init`, `00002_upload_limit`. Both chains were re-baselined at 00001 after the move to per-blog databases; deployments from before it have `goose_db_version` rows 2–7 in the control DB that must be deleted once (README "Upgrading from a single database") or the next control migration |
