5.9 KiB
Categories Feature Design
Overview
Add flat, many-to-many categories to Weiro. Categories are managed via a dedicated admin page and assigned to posts on the post edit form. On the published static site, categories appear as labels on posts, archive pages per category, a category index page, and per-category RSS/JSON feeds. Categories with no published posts are hidden from the published site.
Data Model
New Tables (migration 04_categories.up.sql)
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site_id INTEGER NOT NULL,
guid TEXT NOT NULL,
name TEXT NOT NULL,
slug TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE
);
CREATE INDEX idx_categories_site ON categories (site_id);
CREATE UNIQUE INDEX idx_categories_guid ON categories (guid);
CREATE UNIQUE INDEX idx_categories_site_slug ON categories (site_id, slug);
CREATE TABLE post_categories (
post_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
PRIMARY KEY (post_id, category_id),
FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE
);
CREATE INDEX idx_post_categories_category ON post_categories (category_id);
New Go Model (models/categories.go)
type Category struct {
ID int64 `json:"id"`
SiteID int64 `json:"site_id"`
GUID string `json:"guid"`
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
}
slugis auto-generated fromname(e.g. "Go Programming" ->go-programming), editable by the user.descriptionis Markdown, rendered on the category archive page. Defaults to empty string.
Admin UI
Category Management Page
Route: /sites/:siteID/categories
- Lists all categories for the site showing name, slug, and post count.
- "New category" button navigates to a create/edit form.
- Edit form fields: Name, Slug (auto-generated but editable), Description (Markdown textarea).
- Delete button with confirmation. Deletes the category and its post associations; does not delete the posts.
Handler: CategoriesHandler (new, in handlers/categories.go).
Templates: views/categories/index.html, views/categories/edit.html.
Post Edit Form Changes
- A multi-select checkbox list of all available categories, displayed in a right sidebar alongside the main title/body editing area on the left.
- Selected category IDs sent with the form submission.
CreatePostParamsgainsCategoryIDs []int64.
Post List (Admin)
- Category names shown as small labels next to each post title.
Static Site Output
Category Index Page (/categories/)
Lists all categories that have at least one published post. For each category:
- Category name as a clickable link to the archive page
- Post count
- First sentence/line of the description as a brief excerpt
Category Archive Pages (/categories/<slug>/)
- Category name as heading
- Full Markdown description rendered below the heading
- List of published posts in the category, ordered by
published_atdescending
Post Pages
Each post page displays its category names as clickable links to the corresponding category archive pages.
Feeds
Per-category feeds:
/categories/<slug>/feed.xml(RSS)/categories/<slug>/feed.json(JSON Feed)
Main site feeds (/feed.xml, /feed.json) gain category metadata on each post entry.
Empty Category Handling
Categories with no published posts are hidden from the published site: no index entry, no archive page, no feed generated. They remain visible and manageable in the admin UI.
SQL Queries
New file: sql/queries/categories.sql
SelectCategoriesOfSite— all categories for a site, ordered by nameSelectCategory— single category by IDSelectCategoryByGUID— single category by GUIDSelectCategoriesOfPost— categories for a given post (via join table)SelectPostsOfCategory— published, non-deleted posts in a category, ordered bypublished_atdescCountPostsOfCategory— count of published posts per categoryInsertCategory/UpdateCategory/DeleteCategory— CRUDInsertPostCategory/DeletePostCategory— manage the join tableDeletePostCategoriesByPost— clear all categories for a post (delete-then-reinsert on save)
Service Layer
New services/categories Package
Service struct with methods:
ListCategories(ctx) ([]Category, error)— all categories for the current site (from context)GetCategory(ctx, id) (*Category, error)CreateCategory(ctx, params) (*Category, error)— auto-generates slug from nameUpdateCategory(ctx, params) (*Category, error)DeleteCategory(ctx, id) error— deletes category and post associations, queues site rebuild
Changes to services/posts
UpdatePost— after saving the post, deletes existingpost_categoriesrows and re-inserts for the selected category IDsGetPost/ListPosts— loads each post's categories for admin display
Changes to Publishing Pipeline
pubmodel.Sitegains new fields:- Category list (with post counts and description excerpts for the index page)
- A function to iterate published posts by category
sitebuilder.Builder.BuildSitegains additional goroutines for:- Rendering the category index page
- Rendering each category archive page
- Rendering per-category feeds
- New templates:
tmplNameCategoryList,tmplNameCategorySingle
Rebuild Triggers
Saving or deleting a category queues a site rebuild, same as post state changes.
DB Provider
providers/db/ gains wrapper methods for all new sqlc queries, following the same pattern as existing post methods (e.g. SaveCategory, SelectCategoriesOfPost, etc.).