From 7c4dc0885e444f7ce95571dd572765e90f9bf87d Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 22 Mar 2026 13:12:28 +1100 Subject: [PATCH] Add paging implementation plan Co-Authored-By: Claude Opus 4.6 --- docs/superpowers/plans/2026-03-22-paging.md | 888 ++++++++++++++++++++ 1 file changed, 888 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-22-paging.md diff --git a/docs/superpowers/plans/2026-03-22-paging.md b/docs/superpowers/plans/2026-03-22-paging.md new file mode 100644 index 0000000..9b44775 --- /dev/null +++ b/docs/superpowers/plans/2026-03-22-paging.md @@ -0,0 +1,888 @@ +# Paging Feature Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add offset-based pagination to the admin post list and the generated static site (posts and category listings). + +**Architecture:** Add a `posts_per_page` column to the `sites` table for configurable page size on the generated site. Admin uses a hardcoded page size of 25. The existing `db.PagingParams` and `LIMIT/OFFSET` SQL infrastructure is reused. A shared `models.PageInfo` type carries pagination state to templates. + +**Tech Stack:** Go, SQLite, sqlc, Fiber v3, html/template, Bootstrap + +--- + +### Task 1: Add `posts_per_page` column and regenerate sqlc + +**Files:** +- Create: `sql/schema/05_posts_per_page.up.sql` +- Modify: `sql/queries/sites.sql:10-19` (InsertSite query) +- Modify: `sql/queries/sites.sql:24-25` (UpdateSite query) +- Regenerate: `providers/db/gen/sqlgen/` (sqlc output) + +- [ ] **Step 1: Create migration file** + +Create `sql/schema/05_posts_per_page.up.sql`: +```sql +ALTER TABLE sites ADD COLUMN posts_per_page INTEGER NOT NULL DEFAULT 10; +``` + +- [ ] **Step 2: Update the InsertSite SQL query** + +In `sql/queries/sites.sql`, update the InsertSite query (lines 10-19) to include `posts_per_page`: +```sql +-- name: InsertSite :one +INSERT INTO sites ( + owner_id, + guid, + title, + tagline, + timezone, + posts_per_page, + created_at +) VALUES (?, ?, ?, ?, ?, ?, ?) +RETURNING id; +``` + +- [ ] **Step 3: Update the UpdateSite SQL query** + +In `sql/queries/sites.sql`, update line 24-25: +```sql +-- name: UpdateSite :exec +UPDATE sites SET title = ?, tagline = ?, timezone = ?, posts_per_page = ? WHERE id = ?; +``` + +- [ ] **Step 4: Regenerate sqlc** + +Run: `sqlc generate` +Expected: `providers/db/gen/sqlgen/` files updated with new `PostsPerPage` field on `Site` struct, updated `InsertSiteParams` and `UpdateSiteParams`. + +- [ ] **Step 5: Run tests to verify nothing broke** + +Run: `go test ./...` +Expected: All existing tests pass. + +- [ ] **Step 6: Commit** + +```bash +git add sql/schema/05_posts_per_page.up.sql sql/queries/sites.sql providers/db/gen/sqlgen/ +git commit -m "feat: add posts_per_page column to sites table" +``` + +--- + +### Task 2: Update Site model and DB provider for `PostsPerPage` + +**Files:** +- Modify: `models/sites.go:24-33` (Site struct) +- Modify: `providers/db/sites.go:42-65` (SaveSite) +- Modify: `providers/db/sites.go:102-112` (dbSiteToSite) + +- [ ] **Step 1: Add `PostsPerPage` to `models.Site`** + +In `models/sites.go`, add to the `Site` struct (after `Timezone`): +```go +PostsPerPage int +``` + +- [ ] **Step 2: Update `dbSiteToSite` in `providers/db/sites.go`** + +In `providers/db/sites.go`, update `dbSiteToSite` (line 102) to map the new field: +```go +func dbSiteToSite(row sqlgen.Site) models.Site { + return models.Site{ + ID: row.ID, + OwnerID: row.OwnerID, + GUID: row.Guid, + Title: row.Title, + Timezone: row.Timezone, + Tagline: row.Tagline, + PostsPerPage: int(row.PostsPerPage), + Created: time.Unix(row.CreatedAt, 0).UTC(), + } +} +``` + +- [ ] **Step 3: Update `SaveSite` to include `PostsPerPage`** + +In `providers/db/sites.go`, update the `InsertSite` call (line 44) to include `PostsPerPage`: +```go +newID, err := db.queries.InsertSite(ctx, sqlgen.InsertSiteParams{ + OwnerID: site.OwnerID, + Guid: site.GUID, + Title: site.Title, + Tagline: site.Tagline, + Timezone: site.Timezone, + PostsPerPage: int64(site.PostsPerPage), + CreatedAt: timeToInt(site.Created), +}) +``` + +Update the `UpdateSite` call (line 59) to include `PostsPerPage`: +```go +return db.queries.UpdateSite(ctx, sqlgen.UpdateSiteParams{ + Title: site.Title, + Tagline: site.Tagline, + Timezone: site.Timezone, + PostsPerPage: int64(site.PostsPerPage), + ID: site.ID, +}) +``` + +- [ ] **Step 4: Run tests** + +Run: `go test ./...` +Expected: All tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add models/sites.go providers/db/sites.go sql/queries/sites.sql providers/db/gen/sqlgen/ +git commit -m "feat: add PostsPerPage to Site model and DB provider" +``` + +--- + +### Task 3: Add `CountPostsOfSite` SQL query and DB method + +**Files:** +- Modify: `sql/queries/posts.sql` (add count query) +- Modify: `providers/db/posts.go` (add CountPostsOfSite method) +- Modify: `providers/db/provider_test.go` (add test) +- Regenerate: `providers/db/gen/sqlgen/` + +- [ ] **Step 1: Write the failing test** + +Add to `providers/db/provider_test.go` inside `TestProvider_Posts`: +```go +t.Run("count posts of site", func(t *testing.T) { + countSite := &models.Site{ + OwnerID: user.ID, + GUID: models.NewNanoID(), + Title: "Count Blog", + } + require.NoError(t, p.SaveSite(ctx, countSite)) + + now := time.Date(2026, 3, 22, 12, 0, 0, 0, time.UTC) + for i := 0; i < 3; i++ { + post := &models.Post{ + SiteID: countSite.ID, + GUID: models.NewNanoID(), + Title: fmt.Sprintf("Post %d", i), + Body: "body", + Slug: fmt.Sprintf("/post-%d", i), + CreatedAt: now, + } + require.NoError(t, p.SavePost(ctx, post)) + } + + count, err := p.CountPostsOfSite(ctx, countSite.ID, false) + require.NoError(t, err) + assert.Equal(t, int64(3), count) + + // Soft-delete one post + posts, err := p.SelectPostsOfSite(ctx, countSite.ID, false, db.PagingParams{Limit: 10, Offset: 0}) + require.NoError(t, err) + require.NoError(t, p.SoftDeletePost(ctx, posts[0].ID)) + + count, err = p.CountPostsOfSite(ctx, countSite.ID, false) + require.NoError(t, err) + assert.Equal(t, int64(2), count) + + count, err = p.CountPostsOfSite(ctx, countSite.ID, true) + require.NoError(t, err) + assert.Equal(t, int64(1), count) +}) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `go test ./providers/db/ -run TestProvider_Posts/count_posts_of_site -v` +Expected: FAIL — `CountPostsOfSite` method does not exist. + +- [ ] **Step 3: Add SQL query** + +Add to `sql/queries/posts.sql`: +```sql +-- name: CountPostsOfSite :one +SELECT COUNT(*) FROM posts +WHERE site_id = sqlc.arg(site_id) AND ( + CASE CAST (sqlc.arg(post_filter) AS TEXT) + WHEN 'deleted' THEN deleted_at > 0 + ELSE deleted_at = 0 + END +); +``` + +Run: `sqlc generate` + +- [ ] **Step 4: Add DB provider method** + +Add to `providers/db/posts.go`: +```go +func (db *Provider) CountPostsOfSite(ctx context.Context, siteID int64, showDeleted bool) (int64, error) { + filter := "active" + if showDeleted { + filter = "deleted" + } + return db.queries.CountPostsOfSite(ctx, sqlgen.CountPostsOfSiteParams{ + SiteID: siteID, + PostFilter: filter, + }) +} +``` + +Note: check the generated `sqlgen.CountPostsOfSiteParams` struct name and fields after `sqlc generate` — adjust if the field names differ. + +- [ ] **Step 5: Run test to verify it passes** + +Run: `go test ./providers/db/ -run TestProvider_Posts/count_posts_of_site -v` +Expected: PASS + +- [ ] **Step 6: Run all tests** + +Run: `go test ./...` +Expected: All pass. + +- [ ] **Step 7: Commit** + +```bash +git add sql/queries/posts.sql providers/db/posts.go providers/db/provider_test.go providers/db/gen/sqlgen/ +git commit -m "feat: add CountPostsOfSite query and DB method" +``` + +--- + +### Task 4: Add `models.PageInfo` type + +**Files:** +- Create: `models/paging.go` + +- [ ] **Step 1: Create `models/paging.go`** + +```go +package models + +// PageInfo carries pagination state for templates. +type PageInfo struct { + CurrentPage int + TotalPages int + PostsPerPage int +} + +// HasPrevious returns true if there is a previous page. +func (p PageInfo) HasPrevious() bool { + return p.CurrentPage > 1 +} + +// HasNext returns true if there is a next page. +func (p PageInfo) HasNext() bool { + return p.CurrentPage < p.TotalPages +} + +// PreviousPage returns the previous page number. +func (p PageInfo) PreviousPage() int { + return p.CurrentPage - 1 +} + +// NextPage returns the next page number. +func (p PageInfo) NextPage() int { + return p.CurrentPage + 1 +} +``` + +- [ ] **Step 2: Run tests** + +Run: `go test ./...` +Expected: All pass (no tests yet for this type, but it should compile). + +- [ ] **Step 3: Commit** + +```bash +git add models/paging.go +git commit -m "feat: add PageInfo model for pagination" +``` + +--- + +### Task 5: Add pagination to admin post list (service + handler) + +**Files:** +- Modify: `services/posts/list.go:15-38` (ListPosts signature and implementation) +- Modify: `handlers/posts.go:18-39` (Index handler) + +- [ ] **Step 1: Update `ListPosts` to accept paging params and return count** + +Replace `services/posts/list.go` `ListPosts` method: +```go +type ListPostsResult struct { + Posts []*PostWithCategories + TotalCount int64 +} + +func (s *Service) ListPosts(ctx context.Context, showDeleted bool, paging db.PagingParams) (ListPostsResult, error) { + site, ok := models.GetSite(ctx) + if !ok { + return ListPostsResult{}, models.SiteRequiredError + } + + posts, err := s.db.SelectPostsOfSite(ctx, site.ID, showDeleted, paging) + if err != nil { + return ListPostsResult{}, err + } + + count, err := s.db.CountPostsOfSite(ctx, site.ID, showDeleted) + if err != nil { + return ListPostsResult{}, err + } + + result := make([]*PostWithCategories, len(posts)) + for i, post := range posts { + cats, err := s.db.SelectCategoriesOfPost(ctx, post.ID) + if err != nil { + return ListPostsResult{}, err + } + result[i] = &PostWithCategories{Post: post, Categories: cats} + } + return ListPostsResult{Posts: result, TotalCount: count}, nil +} +``` + +- [ ] **Step 2: Update the admin handler** + +Replace `handlers/posts.go` `Index` method: +```go +func (ph PostsHandler) Index(c fiber.Ctx) error { + var req struct { + Filter string `query:"filter"` + Page int `query:"page"` + } + if err := c.Bind().Query(&req); err != nil { + return fiber.ErrBadRequest + } + + const perPage = 25 + if req.Page < 1 { + req.Page = 1 + } + + result, err := ph.PostService.ListPosts(c.Context(), req.Filter == "deleted", db.PagingParams{ + Offset: int64((req.Page - 1) * perPage), + Limit: perPage, + }) + if err != nil { + return err + } + + totalPages := int(result.TotalCount+int64(perPage)-1) / perPage + if totalPages < 1 { + totalPages = 1 + } + + pageInfo := models.PageInfo{ + CurrentPage: req.Page, + TotalPages: totalPages, + PostsPerPage: perPage, + } + + return accepts(c, json(func() any { + return result.Posts + }), html(func(c fiber.Ctx) error { + return c.Render("posts/index", fiber.Map{ + "req": req, + "posts": result.Posts, + "pageInfo": pageInfo, + }) + })) +} +``` + +Note: add `"lmika.dev/lmika/weiro/providers/db"` and `"lmika.dev/lmika/weiro/models"` to imports in `handlers/posts.go`. + +- [ ] **Step 3: Verify it compiles** + +Run: `go build ./...` +Expected: Compiles successfully. + +- [ ] **Step 4: Run tests** + +Run: `go test ./...` +Expected: All pass. + +- [ ] **Step 5: Commit** + +```bash +git add services/posts/list.go handlers/posts.go +git commit -m "feat: add pagination to admin post list handler and service" +``` + +--- + +### Task 6: Add pagination UI to admin post list template + +**Files:** +- Modify: `views/posts/index.html` + +- [ ] **Step 1: Add pagination controls to admin template** + +Add pagination controls after the post list in `views/posts/index.html`. Insert before the closing `` tag: + +```html +{{ if gt .pageInfo.TotalPages 1 }} + +{{ end }} +``` + +- [ ] **Step 2: Add `Pages` method to `PageInfo`** + +Add to `models/paging.go`: +```go +// Pages returns a slice of page numbers for rendering numbered pagination. +func (p PageInfo) Pages() []int { + pages := make([]int, p.TotalPages) + for i := range pages { + pages[i] = i + 1 + } + return pages +} +``` + +- [ ] **Step 3: Verify it compiles and test manually** + +Run: `go build ./...` +Expected: Compiles. + +- [ ] **Step 4: Commit** + +```bash +git add views/posts/index.html models/paging.go +git commit -m "feat: add pagination controls to admin post list" +``` + +--- + +### Task 7: Add site settings form for `PostsPerPage` + +**Files:** +- Modify: `views/sitesettings/general.html:17-48` (form) +- Modify: `services/sites/services.go:131-158` (UpdateSiteSettingsParams and UpdateSiteSettings) + +- [ ] **Step 1: Add `PostsPerPage` to `UpdateSiteSettingsParams`** + +In `services/sites/services.go`, update the struct (line 131): +```go +type UpdateSiteSettingsParams struct { + SiteID int64 `form:"siteID"` + Name string `form:"name"` + Tagline string `form:"tagline"` + Timezone string `form:"timezone"` + PostsPerPage int `form:"postsPerPage"` +} +``` + +- [ ] **Step 2: Update `UpdateSiteSettings` to handle `PostsPerPage`** + +In `services/sites/services.go`, update `UpdateSiteSettings` (line 138) to validate and set the new field: +```go +func (s *Service) UpdateSiteSettings(ctx context.Context, params UpdateSiteSettingsParams) (models.Site, error) { + site, err := s.GetSiteByID(ctx, params.SiteID) + if err != nil { + return models.Site{}, err + } + + _, err = time.LoadLocation(params.Timezone) + if err != nil { + return models.Site{}, errors.Wrap(err, "invalid timezone") + } + + postsPerPage := params.PostsPerPage + if postsPerPage < 1 { + postsPerPage = 1 + } else if postsPerPage > 100 { + postsPerPage = 100 + } + + site.Title = params.Name + site.Tagline = params.Tagline + site.Timezone = params.Timezone + site.PostsPerPage = postsPerPage + + if err := s.db.SaveSite(ctx, &site); err != nil { + return models.Site{}, err + } + + return site, nil +} +``` + +- [ ] **Step 3: Add form field to settings template** + +In `views/sitesettings/general.html`, add after the Timezone field (after line 43, before the submit button row): +```html +
+ +
+ +
Number of posts per page on the generated site.
+
+
+``` + +- [ ] **Step 4: Verify it compiles** + +Run: `go build ./...` +Expected: Compiles. + +- [ ] **Step 5: Commit** + +```bash +git add services/sites/services.go views/sitesettings/general.html +git commit -m "feat: add posts per page setting to site settings" +``` + +--- + +### Task 8: Add pagination to generated site post list + +**Files:** +- Modify: `providers/sitebuilder/tmpls.go:62-65` (postListData) +- Modify: `providers/sitebuilder/builder.go:124-146` (renderPostListWithCategories) +- Modify: `layouts/simplecss/templates/posts_list.html` + +- [ ] **Step 1: Update `postListData` to include `PageInfo`** + +In `providers/sitebuilder/tmpls.go`, update `postListData` (line 62): +```go +type postListData struct { + commonData + Posts []postSingleData + PageInfo models.PageInfo + PrevURL string + NextURL string +} +``` + +- [ ] **Step 2: Rewrite `renderPostListWithCategories` to paginate** + +Replace `renderPostListWithCategories` in `providers/sitebuilder/builder.go` (line 124): +```go +func (b *Builder) renderPostListWithCategories(bctx buildContext, ctx context.Context) error { + // Collect all posts + var allPosts []postSingleData + for mp := range b.site.PostIter(ctx) { + post, err := mp.Get() + if err != nil { + return err + } + rp, err := b.renderPostWithCategories(ctx, post) + if err != nil { + return err + } + allPosts = append(allPosts, rp) + } + + postsPerPage := b.site.PostsPerPage + if postsPerPage < 1 { + postsPerPage = 10 + } + + totalPages := (len(allPosts) + postsPerPage - 1) / postsPerPage + if totalPages < 1 { + totalPages = 1 + } + + for page := 1; page <= totalPages; page++ { + start := (page - 1) * postsPerPage + end := start + postsPerPage + if end > len(allPosts) { + end = len(allPosts) + } + + pageInfo := models.PageInfo{ + CurrentPage: page, + TotalPages: totalPages, + PostsPerPage: postsPerPage, + } + + var prevURL, nextURL string + if page > 1 { + if page == 2 { + prevURL = "/posts/" + } else { + prevURL = fmt.Sprintf("/posts/page/%d/", page-1) + } + } + if page < totalPages { + nextURL = fmt.Sprintf("/posts/page/%d/", page+1) + } + + pl := postListData{ + commonData: commonData{Site: b.site}, + Posts: allPosts[start:end], + PageInfo: pageInfo, + PrevURL: prevURL, + NextURL: nextURL, + } + + // Determine output path(s) for this page + var paths []string + if page == 1 { + // Page 1 renders at both root and /posts/ + paths = []string{"", "/posts"} + } else { + paths = []string{fmt.Sprintf("/posts/page/%d", page)} + } + + for _, path := range paths { + if err := b.createAtPath(bctx, path, func(f io.Writer) error { + return b.renderTemplate(f, tmplNamePostList, pl) + }); err != nil { + return err + } + } + } + + return nil +} +``` + +- [ ] **Step 3: Update the post list template with prev/next links** + +Replace `layouts/simplecss/templates/posts_list.html`: +```html +{{ range .Posts }} +
+ {{ if .Post.Title }}

{{ .Post.Title }}

{{ end }} + {{ .HTML }} + {{ template "_post_meta.html" . }} +
+{{ end }} +{{ if or .PrevURL .NextURL }} + +{{ end }} +``` + +- [ ] **Step 4: Run tests** + +Run: `go test ./...` +Expected: Existing builder test may need updating (see next step). + +- [ ] **Step 5: Update builder test** + +The test in `providers/sitebuilder/builder_test.go` creates a `pubmodel.Site` without `PostsPerPage`, which will default to 0. Update the test site to set `PostsPerPage`: +```go +site := pubmodel.Site{ + Site: models.Site{PostsPerPage: 10}, + BaseURL: "https://example.com", + PostIter: func(ctx context.Context) iter.Seq[models.Maybe[*models.Post]] { + // ... existing code ... + }, +} +``` + +The expected `index.html` content stays the same since both posts fit on one page. + +- [ ] **Step 6: Run tests** + +Run: `go test ./...` +Expected: All pass. + +- [ ] **Step 7: Commit** + +```bash +git add providers/sitebuilder/tmpls.go providers/sitebuilder/builder.go layouts/simplecss/templates/posts_list.html providers/sitebuilder/builder_test.go +git commit -m "feat: add pagination to generated site post list" +``` + +--- + +### Task 9: Add pagination to generated site category pages + +**Files:** +- Modify: `providers/sitebuilder/tmpls.go:82-88` (categorySingleData) +- Modify: `providers/sitebuilder/builder.go:315-362` (renderCategoryPages) +- Modify: `layouts/simplecss/templates/categories_single.html` + +- [ ] **Step 1: Update `categorySingleData` to include pagination** + +In `providers/sitebuilder/tmpls.go`, update `categorySingleData` (line 82): +```go +type categorySingleData struct { + commonData + Category *models.Category + DescriptionHTML template.HTML + Posts []postSingleData + Path string + PageInfo models.PageInfo + PrevURL string + NextURL string +} +``` + +- [ ] **Step 2: Rewrite `renderCategoryPages` to paginate** + +Replace `renderCategoryPages` in `providers/sitebuilder/builder.go` (line 315): +```go +func (b *Builder) renderCategoryPages(ctx buildContext, goCtx context.Context) error { + for _, cwc := range b.site.Categories { + if cwc.PostCount == 0 { + continue + } + + // Collect all posts for this category + var allPosts []postSingleData + for mp := range b.site.PostIterByCategory(goCtx, cwc.ID) { + post, err := mp.Get() + if err != nil { + return err + } + rp, err := b.renderPostWithCategories(goCtx, post) + if err != nil { + return err + } + allPosts = append(allPosts, rp) + } + + var descHTML bytes.Buffer + if cwc.Description != "" { + if err := b.mdRenderer.RenderTo(goCtx, &descHTML, cwc.Description); err != nil { + return err + } + } + + postsPerPage := b.site.PostsPerPage + if postsPerPage < 1 { + postsPerPage = 10 + } + + totalPages := (len(allPosts) + postsPerPage - 1) / postsPerPage + if totalPages < 1 { + totalPages = 1 + } + + basePath := fmt.Sprintf("/categories/%s", cwc.Slug) + + for page := 1; page <= totalPages; page++ { + start := (page - 1) * postsPerPage + end := start + postsPerPage + if end > len(allPosts) { + end = len(allPosts) + } + + pageInfo := models.PageInfo{ + CurrentPage: page, + TotalPages: totalPages, + PostsPerPage: postsPerPage, + } + + var prevURL, nextURL string + if page > 1 { + if page == 2 { + prevURL = basePath + "/" + } else { + prevURL = fmt.Sprintf("%s/page/%d/", basePath, page-1) + } + } + if page < totalPages { + nextURL = fmt.Sprintf("%s/page/%d/", basePath, page+1) + } + + path := basePath + if page > 1 { + path = fmt.Sprintf("%s/page/%d", basePath, page) + } + + data := categorySingleData{ + commonData: commonData{Site: b.site}, + Category: &cwc.Category, + DescriptionHTML: template.HTML(descHTML.String()), + Posts: allPosts[start:end], + Path: path, + PageInfo: pageInfo, + PrevURL: prevURL, + NextURL: nextURL, + } + + if err := b.createAtPath(ctx, path, func(f io.Writer) error { + return b.renderTemplate(f, tmplNameCategorySingle, data) + }); err != nil { + return err + } + } + + // Per-category feeds (use all posts, not paginated) + if err := b.renderCategoryFeed(ctx, cwc, allPosts); err != nil { + return err + } + } + + return nil +} +``` + +- [ ] **Step 3: Update category single template with prev/next links** + +Replace `layouts/simplecss/templates/categories_single.html`: +```html +{{ if .DescriptionHTML }}
{{ .DescriptionHTML }}
{{ end }} +{{ range .Posts }} +
+ {{ if .Post.Title }}

{{ .Post.Title }}

{{ end }} + {{ .HTML }} + {{ template "_post_meta.html" . }} +
+{{ end }} +{{ if or .PrevURL .NextURL }} + +{{ end }} +``` + +Note: check the current content of `categories_single.html` first — preserve any existing structure (like `

` headings) that may not have been captured in the exploration. Read the file before editing. + +- [ ] **Step 4: Run tests** + +Run: `go test ./...` +Expected: All pass. + +- [ ] **Step 5: Commit** + +```bash +git add providers/sitebuilder/tmpls.go providers/sitebuilder/builder.go layouts/simplecss/templates/categories_single.html +git commit -m "feat: add pagination to generated site category pages" +``` + +--- + +### Task 10: Final verification + +- [ ] **Step 1: Run full test suite** + +Run: `go test ./...` +Expected: All tests pass. + +- [ ] **Step 2: Build the project** + +Run: `go build ./...` +Expected: Clean build with no errors. + +- [ ] **Step 3: Commit any remaining changes** + +If any files were missed, stage and commit them.