diff --git a/docs/superpowers/plans/2026-03-22-paging.md b/docs/superpowers/plans/2026-03-22-paging.md deleted file mode 100644 index 9b44775..0000000 --- a/docs/superpowers/plans/2026-03-22-paging.md +++ /dev/null @@ -1,888 +0,0 @@ -# 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. diff --git a/docs/superpowers/specs/2026-03-22-paging-design.md b/docs/superpowers/specs/2026-03-22-paging-design.md deleted file mode 100644 index 80e3ee6..0000000 --- a/docs/superpowers/specs/2026-03-22-paging-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Paging Feature Design - -## Overview - -Introduce offset-based pagination to the admin post list and the generated static site (both post listings and category listings). - -## Data Layer - -### New `sites` column - -Add `posts_per_page INTEGER NOT NULL DEFAULT 10` to the `sites` table. This setting controls the number of posts per page on the **generated static site only**. - -### New SQL queries - -- `CountPostsOfSite(siteID, showDeleted)` — returns total post count for the site -- `CountPostsOfCategory(categoryID)` — returns total published post count for a category - -### Model changes - -**`models.Site`** — add field: -```go -PostsPerPage int -``` - -**New shared type** (`models/paging.go`): -```go -type PageInfo struct { - CurrentPage int - TotalPages int - PostsPerPage int -} -``` - -Existing `db.PagingParams` and queries (`SelectPostsOfSite`, `SelectPostsOfCategory`) already support `LIMIT/OFFSET` and remain unchanged. - -## Admin Section - -### Post list pagination - -- **Page size: hardcoded at 25** (not tied to the `PostsPerPage` site setting) -- Handler (`handlers/posts.go` `Index()`) reads a `page` query parameter (default 1) -- Computes offset as `(page - 1) * 25` -- Fetches total post count via new `CountPosts()` service method to build `PageInfo` -- Passes `PageInfo` to template - -### Service changes - -- `ListPosts()` accepts paging params from the handler instead of hardcoding them -- New `CountPosts()` method that calls the count query - -### Template (`views/posts/index.html`) - -- Full numbered pagination with Previous/Next below the post list: `< 1 2 3 ... 10 >` -- Preserves existing query params (e.g. `?filter=deleted`) when paginating -- Both regular post list and trash view are paginated - -### Site settings form - -- Add "Posts per page" number input to `views/sitesettings/general.html` -- Add `PostsPerPage` field to `UpdateSiteSettingsParams` -- Server-side validation: minimum 1, maximum 100 - -## Generated Static Site - -### URL structure - -Post listing pages: -- `/posts/` — page 1 -- `/posts/page/2/` — page 2 -- `/posts/page/N/` — page N - -Category listing pages: -- `/categories//` — page 1 -- `/categories//page/2/` — page 2 -- `/categories//page/N/` — page N - -### Site root - -`/` (site root) shows the same content as `/posts/` (page 1 of all posts). - -### Builder changes (`providers/sitebuilder/builder.go`) - -- Instead of rendering one `posts_list.html` with all posts, generate multiple page files -- Uses `site.PostsPerPage` from the site setting to determine page size -- Same pattern for category pages - -### Publisher changes (`services/publisher/iter.go`) - -- Existing iterator fetches posts in batches of 50 internally — this stays as-is -- The builder chunks posts into pages of `PostsPerPage` size and renders each page as a separate HTML file - -### Template (`layouts/simplecss/templates/posts_list.html`) - -- Receives `PageInfo` plus the posts for that page -- Renders **Previous / Next** links only (no numbered pagination) -- Previous link hidden on page 1; Next link hidden on last page - -## Approach - -Offset-based pagination using the existing `db.PagingParams` infrastructure. Page number maps to offset: `offset = (page - 1) * postsPerPage`. diff --git a/handlers/posts.go b/handlers/posts.go index 3326533..a133758 100644 --- a/handlers/posts.go +++ b/handlers/posts.go @@ -6,7 +6,6 @@ import ( "github.com/gofiber/fiber/v3" "lmika.dev/lmika/weiro/models" - "lmika.dev/lmika/weiro/providers/db" "lmika.dev/lmika/weiro/services/categories" "lmika.dev/lmika/weiro/services/posts" ) @@ -19,43 +18,22 @@ type PostsHandler struct { 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, - }) + posts, err := ph.PostService.ListPosts(c.Context(), req.Filter == "deleted") 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 + return posts }), html(func(c fiber.Ctx) error { return c.Render("posts/index", fiber.Map{ - "req": req, - "posts": result.Posts, - "pageInfo": pageInfo, + "req": req, + "posts": posts, }) })) } diff --git a/layouts/simplecss/templates/categories_single.html b/layouts/simplecss/templates/categories_single.html index e9e7116..deaeb02 100644 --- a/layouts/simplecss/templates/categories_single.html +++ b/layouts/simplecss/templates/categories_single.html @@ -8,10 +8,4 @@ {{ .HTML }} {{ template "_post_meta.html" . }} -{{ end }} -{{ if or .PrevURL .NextURL }} - -{{ end }} +{{ end }} \ No newline at end of file diff --git a/layouts/simplecss/templates/posts_list.html b/layouts/simplecss/templates/posts_list.html index 6a2eca6..5f10f1e 100644 --- a/layouts/simplecss/templates/posts_list.html +++ b/layouts/simplecss/templates/posts_list.html @@ -5,10 +5,4 @@ {{ template "_post_meta.html" . }} -{{ end }} -{{ if or .PrevURL .NextURL }} - -{{ end }} +{{ end }} \ No newline at end of file diff --git a/models/paging.go b/models/paging.go deleted file mode 100644 index b4e514b..0000000 --- a/models/paging.go +++ /dev/null @@ -1,37 +0,0 @@ -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 -} - -// 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 -} diff --git a/models/sites.go b/models/sites.go index 81bf6be..16cbef4 100644 --- a/models/sites.go +++ b/models/sites.go @@ -27,10 +27,9 @@ type Site struct { GUID string Created time.Time - Title string - Tagline string - Timezone string - PostsPerPage int + Title string + Tagline string + Timezone string } type SitePublishTarget struct { diff --git a/providers/db/gen/sqlgen/models.go b/providers/db/gen/sqlgen/models.go index ae58594..788c292 100644 --- a/providers/db/gen/sqlgen/models.go +++ b/providers/db/gen/sqlgen/models.go @@ -57,14 +57,13 @@ type PublishTarget struct { } type Site struct { - ID int64 - OwnerID int64 - Guid string - Title string - Tagline string - CreatedAt int64 - Timezone string - PostsPerPage int64 + ID int64 + OwnerID int64 + Guid string + Title string + Tagline string + CreatedAt int64 + Timezone string } type Upload struct { diff --git a/providers/db/gen/sqlgen/posts.sql.go b/providers/db/gen/sqlgen/posts.sql.go index ef3d170..8bff191 100644 --- a/providers/db/gen/sqlgen/posts.sql.go +++ b/providers/db/gen/sqlgen/posts.sql.go @@ -9,28 +9,6 @@ import ( "context" ) -const countPostsOfSite = `-- name: CountPostsOfSite :one -SELECT COUNT(*) FROM posts -WHERE site_id = ?1 AND ( - CASE CAST (?2 AS TEXT) - WHEN 'deleted' THEN deleted_at > 0 - ELSE deleted_at = 0 - END -) -` - -type CountPostsOfSiteParams struct { - SiteID int64 - PostFilter string -} - -func (q *Queries) CountPostsOfSite(ctx context.Context, arg CountPostsOfSiteParams) (int64, error) { - row := q.db.QueryRowContext(ctx, countPostsOfSite, arg.SiteID, arg.PostFilter) - var count int64 - err := row.Scan(&count) - return count, err -} - const hardDeletePost = `-- name: HardDeletePost :exec DELETE FROM posts WHERE id = ? ` diff --git a/providers/db/gen/sqlgen/sites.sql.go b/providers/db/gen/sqlgen/sites.sql.go index 80ccbc0..bd80fb3 100644 --- a/providers/db/gen/sqlgen/sites.sql.go +++ b/providers/db/gen/sqlgen/sites.sql.go @@ -28,20 +28,18 @@ INSERT INTO sites ( title, tagline, timezone, - posts_per_page, created_at -) VALUES (?, ?, ?, ?, ?, ?, ?) +) VALUES (?, ?, ?, ?, ?, ?) RETURNING id ` type InsertSiteParams struct { - OwnerID int64 - Guid string - Title string - Tagline string - Timezone string - PostsPerPage int64 - CreatedAt int64 + OwnerID int64 + Guid string + Title string + Tagline string + Timezone string + CreatedAt int64 } func (q *Queries) InsertSite(ctx context.Context, arg InsertSiteParams) (int64, error) { @@ -51,7 +49,6 @@ func (q *Queries) InsertSite(ctx context.Context, arg InsertSiteParams) (int64, arg.Title, arg.Tagline, arg.Timezone, - arg.PostsPerPage, arg.CreatedAt, ) var id int64 @@ -104,7 +101,7 @@ func (q *Queries) SelectAllSitesWithOwners(ctx context.Context) ([]SelectAllSite } const selectSiteByGUID = `-- name: SelectSiteByGUID :one -SELECT id, owner_id, guid, title, tagline, created_at, timezone, posts_per_page FROM sites WHERE guid = ? +SELECT id, owner_id, guid, title, tagline, created_at, timezone FROM sites WHERE guid = ? ` func (q *Queries) SelectSiteByGUID(ctx context.Context, guid string) (Site, error) { @@ -118,13 +115,12 @@ func (q *Queries) SelectSiteByGUID(ctx context.Context, guid string) (Site, erro &i.Tagline, &i.CreatedAt, &i.Timezone, - &i.PostsPerPage, ) return i, err } const selectSiteByID = `-- name: SelectSiteByID :one -SELECT id, owner_id, guid, title, tagline, created_at, timezone, posts_per_page FROM sites WHERE id = ? +SELECT id, owner_id, guid, title, tagline, created_at, timezone FROM sites WHERE id = ? ` func (q *Queries) SelectSiteByID(ctx context.Context, id int64) (Site, error) { @@ -138,13 +134,12 @@ func (q *Queries) SelectSiteByID(ctx context.Context, id int64) (Site, error) { &i.Tagline, &i.CreatedAt, &i.Timezone, - &i.PostsPerPage, ) return i, err } const selectSitesOwnedByUser = `-- name: SelectSitesOwnedByUser :many -SELECT id, owner_id, guid, title, tagline, created_at, timezone, posts_per_page FROM sites WHERE owner_id = ? ORDER BY title ASC +SELECT id, owner_id, guid, title, tagline, created_at, timezone FROM sites WHERE owner_id = ? ORDER BY title ASC ` func (q *Queries) SelectSitesOwnedByUser(ctx context.Context, ownerID int64) ([]Site, error) { @@ -164,7 +159,6 @@ func (q *Queries) SelectSitesOwnedByUser(ctx context.Context, ownerID int64) ([] &i.Tagline, &i.CreatedAt, &i.Timezone, - &i.PostsPerPage, ); err != nil { return nil, err } @@ -180,15 +174,14 @@ func (q *Queries) SelectSitesOwnedByUser(ctx context.Context, ownerID int64) ([] } const updateSite = `-- name: UpdateSite :exec -UPDATE sites SET title = ?, tagline = ?, timezone = ?, posts_per_page = ? WHERE id = ? +UPDATE sites SET title = ?, tagline = ?, timezone = ? WHERE id = ? ` type UpdateSiteParams struct { - Title string - Tagline string - Timezone string - PostsPerPage int64 - ID int64 + Title string + Tagline string + Timezone string + ID int64 } func (q *Queries) UpdateSite(ctx context.Context, arg UpdateSiteParams) error { @@ -196,7 +189,6 @@ func (q *Queries) UpdateSite(ctx context.Context, arg UpdateSiteParams) error { arg.Title, arg.Tagline, arg.Timezone, - arg.PostsPerPage, arg.ID, ) return err diff --git a/providers/db/posts.go b/providers/db/posts.go index 7f58d1a..218e931 100644 --- a/providers/db/posts.go +++ b/providers/db/posts.go @@ -13,17 +13,6 @@ type PagingParams struct { Offset int64 } -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, - }) -} - func (db *Provider) SelectPostsOfSite(ctx context.Context, siteID int64, showDeleted bool, pp PagingParams) ([]*models.Post, error) { var filter = "" if showDeleted { diff --git a/providers/db/provider_test.go b/providers/db/provider_test.go index 0a2e6df..06f03c0 100644 --- a/providers/db/provider_test.go +++ b/providers/db/provider_test.go @@ -3,7 +3,6 @@ package db_test import ( "context" "encoding/base64" - "fmt" "path/filepath" "testing" "time" @@ -230,45 +229,6 @@ func TestProvider_Posts(t *testing.T) { require.NoError(t, err) assert.Empty(t, posts) }) - - 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) - }) } func TestProvider_PublishTargets(t *testing.T) { diff --git a/providers/db/sites.go b/providers/db/sites.go index d1167ca..28d83f6 100644 --- a/providers/db/sites.go +++ b/providers/db/sites.go @@ -42,13 +42,12 @@ func (db *Provider) SelectSitesOwnedByUser(ctx context.Context, ownerID int64) ( func (db *Provider) SaveSite(ctx context.Context, site *models.Site) error { if site.ID == 0 { 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), + OwnerID: site.OwnerID, + Guid: site.GUID, + Title: site.Title, + Tagline: site.Tagline, + Timezone: site.Timezone, + CreatedAt: timeToInt(site.Created), }) if err != nil { return err @@ -58,11 +57,10 @@ func (db *Provider) SaveSite(ctx context.Context, site *models.Site) error { } return db.queries.UpdateSite(ctx, sqlgen.UpdateSiteParams{ - Title: site.Title, - Tagline: site.Tagline, - Timezone: site.Timezone, - PostsPerPage: int64(site.PostsPerPage), - ID: site.ID, + Title: site.Title, + Tagline: site.Tagline, + Timezone: site.Timezone, + ID: site.ID, }) } @@ -103,13 +101,12 @@ func (db *Provider) SelectAllSitesWithOwners(ctx context.Context) ([]SiteWithOwn 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(), + ID: row.ID, + OwnerID: row.OwnerID, + GUID: row.Guid, + Title: row.Title, + Timezone: row.Timezone, + Tagline: row.Tagline, + Created: time.Unix(row.CreatedAt, 0).UTC(), } } diff --git a/providers/sitebuilder/builder.go b/providers/sitebuilder/builder.go index 9e5199d..1a4275d 100644 --- a/providers/sitebuilder/builder.go +++ b/providers/sitebuilder/builder.go @@ -122,8 +122,7 @@ func (b *Builder) BuildSite(outDir string) error { } func (b *Builder) renderPostListWithCategories(bctx buildContext, ctx context.Context) error { - // Collect all posts - var allPosts []postSingleData + var posts []postSingleData for mp := range b.site.PostIter(ctx) { post, err := mp.Get() if err != nil { @@ -133,70 +132,17 @@ func (b *Builder) renderPostListWithCategories(bctx buildContext, ctx context.Co if err != nil { return err } - allPosts = append(allPosts, rp) + posts = append(posts, rp) } - postsPerPage := b.site.PostsPerPage - if postsPerPage < 1 { - postsPerPage = 10 + pl := postListData{ + commonData: commonData{Site: b.site}, + Posts: posts, } - 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/%d/", page-1) - } - } - if page < totalPages { - nextURL = fmt.Sprintf("/posts/%d/", page+1) - } - - pl := postListData{ - commonData: commonData{Site: b.site}, - Posts: allPosts[start:end], - PageInfo: pageInfo, - PrevURL: prevURL, - NextURL: nextURL, - } - - // Page 1 renders at both root and /posts/ - var paths []string - if page == 1 { - paths = []string{"", "/posts"} - } else { - paths = []string{fmt.Sprintf("/posts/%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 + return b.createAtPath(bctx, "", func(f io.Writer) error { + return b.renderTemplate(f, tmplNamePostList, pl) + }) } func (b *Builder) renderFeeds(ctx buildContext, postIter iter.Seq[models.Maybe[*models.Post]], opts feedOptions) error { @@ -372,8 +318,7 @@ func (b *Builder) renderCategoryPages(ctx buildContext, goCtx context.Context) e continue } - // Collect all posts for this category - var allPosts []postSingleData + var posts []postSingleData for mp := range b.site.PostIterByCategory(goCtx, cwc.ID) { post, err := mp.Get() if err != nil { @@ -383,7 +328,7 @@ func (b *Builder) renderCategoryPages(ctx buildContext, goCtx context.Context) e if err != nil { return err } - allPosts = append(allPosts, rp) + posts = append(posts, rp) } var descHTML bytes.Buffer @@ -393,68 +338,22 @@ func (b *Builder) renderCategoryPages(ctx buildContext, goCtx context.Context) e } } - postsPerPage := b.site.PostsPerPage - if postsPerPage < 1 { - postsPerPage = 10 + data := categorySingleData{ + commonData: commonData{Site: b.site}, + Category: &cwc.Category, + DescriptionHTML: template.HTML(descHTML.String()), + Posts: posts, + Path: fmt.Sprintf("/categories/%s", cwc.Slug), } - totalPages := (len(allPosts) + postsPerPage - 1) / postsPerPage - if totalPages < 1 { - totalPages = 1 + if err := b.createAtPath(ctx, data.Path, func(f io.Writer) error { + return b.renderTemplate(f, tmplNameCategorySingle, data) + }); err != nil { + return err } - 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/%d/", basePath, page-1) - } - } - if page < totalPages { - nextURL = fmt.Sprintf("%s/%d/", basePath, page+1) - } - - path := basePath - if page > 1 { - path = fmt.Sprintf("%s/%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 { + // Per-category feeds + if err := b.renderCategoryFeed(ctx, cwc, posts); err != nil { return err } } @@ -572,9 +471,6 @@ func (b *Builder) writeUploads(ctx buildContext, uploads []models.Upload) error } func (b *Builder) writeStaticAssets(ctx buildContext) error { - if b.opts.StaticFS == nil { - return nil - } return fs.WalkDir(b.opts.StaticFS, ".", func(path string, d os.DirEntry, err error) error { if err != nil { return err diff --git a/providers/sitebuilder/builder_test.go b/providers/sitebuilder/builder_test.go index a5a9bbf..cbe116b 100644 --- a/providers/sitebuilder/builder_test.go +++ b/providers/sitebuilder/builder_test.go @@ -38,7 +38,6 @@ func TestBuilder_BuildSite(t *testing.T) { } site := pubmodel.Site{ - Site: models.Site{PostsPerPage: 10}, BaseURL: "https://example.com", PostIter: func(ctx context.Context) iter.Seq[models.Maybe[*models.Post]] { return func(yield func(models.Maybe[*models.Post]) bool) { diff --git a/providers/sitebuilder/tmpls.go b/providers/sitebuilder/tmpls.go index e0ece37..cea02f5 100644 --- a/providers/sitebuilder/tmpls.go +++ b/providers/sitebuilder/tmpls.go @@ -61,10 +61,7 @@ type postSingleData struct { type postListData struct { commonData - Posts []postSingleData - PageInfo models.PageInfo - PrevURL string - NextURL string + Posts []postSingleData } type layoutData struct { @@ -88,7 +85,4 @@ type categorySingleData struct { DescriptionHTML template.HTML Posts []postSingleData Path string - PageInfo models.PageInfo - PrevURL string - NextURL string } diff --git a/services/posts/list.go b/services/posts/list.go index dd25bae..15e14d3 100644 --- a/services/posts/list.go +++ b/services/posts/list.go @@ -12,36 +12,29 @@ type PostWithCategories struct { Categories []*models.Category } -type ListPostsResult struct { - Posts []*PostWithCategories - TotalCount int64 -} - -func (s *Service) ListPosts(ctx context.Context, showDeleted bool, paging db.PagingParams) (ListPostsResult, error) { +func (s *Service) ListPosts(ctx context.Context, showDeleted bool) ([]*PostWithCategories, error) { site, ok := models.GetSite(ctx) if !ok { - return ListPostsResult{}, models.SiteRequiredError + return nil, models.SiteRequiredError } - posts, err := s.db.SelectPostsOfSite(ctx, site.ID, showDeleted, paging) + posts, err := s.db.SelectPostsOfSite(ctx, site.ID, showDeleted, db.PagingParams{ + Offset: 0, + Limit: 25, + }) if err != nil { - return ListPostsResult{}, err - } - - count, err := s.db.CountPostsOfSite(ctx, site.ID, showDeleted) - if err != nil { - return ListPostsResult{}, err + return nil, 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 + return nil, err } result[i] = &PostWithCategories{Post: post, Categories: cats} } - return ListPostsResult{Posts: result, TotalCount: count}, nil + return result, nil } func (s *Service) GetPost(ctx context.Context, pid int64) (*models.Post, error) { diff --git a/services/sites/services.go b/services/sites/services.go index 86e34b2..06afe15 100644 --- a/services/sites/services.go +++ b/services/sites/services.go @@ -77,12 +77,11 @@ func (s *Service) FirstRun(ctx context.Context, req FirstRunRequest) (newUser mo } newSite = models.Site{ - Title: defaultIfEmpty(req.SiteName, "New Site"), - GUID: models.NewNanoID(), - OwnerID: newUser.ID, - Timezone: "UTC", - PostsPerPage: 10, - Created: time.Now(), + Title: defaultIfEmpty(req.SiteName, "New Site"), + GUID: models.NewNanoID(), + OwnerID: newUser.ID, + Timezone: "UTC", + Created: time.Now(), } if err := s.db.SaveSite(ctx, &newSite); err != nil { return newUser, newSite, err @@ -130,11 +129,10 @@ func (s *Service) ListAllSitesWithOwners(ctx context.Context) ([]db.SiteWithOwne } type UpdateSiteSettingsParams struct { - SiteID int64 `form:"siteID"` - Name string `form:"name"` - Tagline string `form:"tagline"` - Timezone string `form:"timezone"` - PostsPerPage int `form:"postsPerPage"` + SiteID int64 `form:"siteID"` + Name string `form:"name"` + Tagline string `form:"tagline"` + Timezone string `form:"timezone"` } func (s *Service) UpdateSiteSettings(ctx context.Context, params UpdateSiteSettingsParams) (models.Site, error) { @@ -148,17 +146,9 @@ func (s *Service) UpdateSiteSettings(ctx context.Context, params UpdateSiteSetti 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 diff --git a/sql/queries/posts.sql b/sql/queries/posts.sql index 5a4c18e..dae1f39 100644 --- a/sql/queries/posts.sql +++ b/sql/queries/posts.sql @@ -1,12 +1,3 @@ --- 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 -); - -- name: SelectPostsOfSite :many SELECT * FROM posts diff --git a/sql/queries/sites.sql b/sql/queries/sites.sql index 0609b12..8fe2469 100644 --- a/sql/queries/sites.sql +++ b/sql/queries/sites.sql @@ -14,16 +14,15 @@ INSERT INTO sites ( title, tagline, timezone, - posts_per_page, created_at -) VALUES (?, ?, ?, ?, ?, ?, ?) +) VALUES (?, ?, ?, ?, ?, ?) RETURNING id; -- name: HasUsersAndSites :one SELECT (SELECT COUNT(*) FROM users) > 0 AND (SELECT COUNT(*) FROM sites) > 0 AS has_users_and_sites; -- name: UpdateSite :exec -UPDATE sites SET title = ?, tagline = ?, timezone = ?, posts_per_page = ? WHERE id = ?; +UPDATE sites SET title = ?, tagline = ?, timezone = ? WHERE id = ?; -- name: SelectAllSitesWithOwners :many SELECT s.id, s.guid, s.title, s.owner_id, u.username diff --git a/sql/schema/05_posts_per_page.up.sql b/sql/schema/05_posts_per_page.up.sql deleted file mode 100644 index 1bea8f9..0000000 --- a/sql/schema/05_posts_per_page.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE sites ADD COLUMN posts_per_page INTEGER NOT NULL DEFAULT 10; diff --git a/views/posts/index.html b/views/posts/index.html index 7786539..bbf445d 100644 --- a/views/posts/index.html +++ b/views/posts/index.html @@ -62,22 +62,4 @@ {{ end }} {{ end }} - - {{ if gt .pageInfo.TotalPages 1 }} - - {{ end }} \ No newline at end of file diff --git a/views/sitesettings/general.html b/views/sitesettings/general.html index 6f1833b..ca3e7a9 100644 --- a/views/sitesettings/general.html +++ b/views/sitesettings/general.html @@ -41,13 +41,6 @@ -
- -
- -
Number of posts per page on the generated site.
-
-