From 839343a3108734a15e331b542a20ed9c77ef582b Mon Sep 17 00:00:00 2001 From: grm Date: Wed, 16 Sep 2026 00:00:57 +0300 Subject: Add a search box module and a search page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Readers had no way to find a post. The new "search" module goes in the header (a bar under the nav) or a side column (a box with a heading) and is a plain GET form to /search, so it works without JavaScript. The results page lists the published posts of every page whose title or Markdown body matches the query — case-insensitive, each word literal, spaces meaning "anything in between", in order — as title, date and a short snippet with the match marked, 20 per page. modules.kind is a CHECK constraint, so a migration widens it; "search" becomes a reserved page slug so the literal route keeps winning over /{page}. moduleHasSettings now takes the module: a header search box has no heading, hence nothing to edit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- internal/store/posts.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'internal/store') diff --git a/internal/store/posts.go b/internal/store/posts.go index 706d132..1bffd51 100644 --- a/internal/store/posts.go +++ b/internal/store/posts.go @@ -75,6 +75,21 @@ func (bs *BlogStore) PublishedPosts(ctx context.Context, pageID int64, limit, of return posts, total, err } +// SearchPublishedPosts returns a page of the published posts, from every page +// of the blog, whose title or Markdown body matches the case-insensitive +// regular expression; the caller builds it from the reader's words. Title and +// body are searched as one text so a query can span them. +func (bs *BlogStore) SearchPublishedPosts(ctx context.Context, pattern string, limit, offset int) ([]Post, int, error) { + posts, err := bs.collectPosts(ctx, `SELECT `+postCols+` FROM posts p JOIN pages g ON g.id=p.page_id + WHERE p.published AND (p.title || E'\n' || p.body_md) ~* $1 ORDER BY p.created_at DESC, p.id DESC LIMIT $2 OFFSET $3`, pattern, limit, offset) + if err != nil { + return nil, 0, err + } + var total int + err = bs.db.QueryRow(ctx, `SELECT count(*) FROM posts WHERE published AND (title || E'\n' || body_md) ~* $1`, pattern).Scan(&total) + return posts, total, err +} + // PostRef is the light-weight row the archive module lists. type PostRef struct { Title string -- cgit v1.2.3