aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-16 00:20:06 +0300
committergrm <grm@eyesin.space>2026-09-16 00:20:06 +0300
commit5119018feeaa22c47c0e91e15d3b9414dd6e0772 (patch)
treedf5c5813c3c92e21b1230516f849de811adf212e
parent839343a3108734a15e331b542a20ed9c77ef582b (diff)
downloadblogspace-5119018feeaa22c47c0e91e15d3b9414dd6e0772.tar.gz
blogspace-5119018feeaa22c47c0e91e15d3b9414dd6e0772.tar.bz2
blogspace-5119018feeaa22c47c0e91e15d3b9414dd6e0772.zip
Put the header search box in the menu row, and stop saying "Search"
The header search was a band of its own under the menu; now it floats at the right end of the menu row and only gets a thin bar when the header has no menu. The forms are a single input — Enter and the phone keyboard's search key submit a one-field form everywhere — with the placeholder as the label, side boxes start without a heading, and the results page shows a box only when no module does, so a page no longer reads "Search" a dozen times. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
-rw-r--r--AGENTS.md10
-rw-r--r--internal/web/handlers_layout.go4
-rw-r--r--internal/web/layout.go16
-rw-r--r--internal/web/static/blog.css15
-rw-r--r--internal/web/templates/blog/search.html2
-rw-r--r--internal/web/templates/layouts/blog.html12
6 files changed, 41 insertions, 18 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 5e94b3d..3959498 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -295,9 +295,13 @@ internal/web/ server.go (host router, middleware, render helpers)
and `blog/tag.html`); the two modules are `tags` (by use, `count`) and
`tagcloud` (alphabetical, `cloudSizes` → `tc-1…tc-5`).
- **Search** (`web/search.go`, `handleBlogSearch`, `GET /search?q=&p=`,
- `blog/search.html`): the `search` module is a plain GET form (header bar or
- side box, `searchform` in `layouts/blog.html`; the results page shows one
- too). `searchPattern` turns the query into the regex both Postgres and Go
+ `blog/search.html`): the `search` module is a plain GET form of one input
+ (`searchform` in `layouts/blog.html`; Enter submits, the placeholder is the
+ label, so "Search" is not repeated all over the page). In the header it is
+ rendered inside the menu module's row, floated right (`Layout.HeaderMenu/
+ HeaderSearch`), and gets its own thin bar only when the header has no menu;
+ in a column it is a box with an optional heading. The results page shows a
+ box of its own only when no module does (`Layout.HasSearch`). `searchPattern` turns the query into the regex both Postgres and Go
run: words `regexp.QuoteMeta`'d and joined with `.*`, so the reader's words
are literal, case does not matter (`~*` / `(?is)`) and spaces mean
"anything in between", in order. `SearchPublishedPosts` matches it against
diff --git a/internal/web/handlers_layout.go b/internal/web/handlers_layout.go
index d89cd18..db4f47c 100644
--- a/internal/web/handlers_layout.go
+++ b/internal/web/handlers_layout.go
@@ -126,10 +126,6 @@ func (s *Server) handleModuleAdd(w http.ResponseWriter, r *http.Request) {
m.Count = 10
case "tagcloud":
m.Title = i18n.T(blogLang, "Tag cloud")
- case "search":
- if area != "header" {
- m.Title = i18n.T(blogLang, "Search")
- }
}
m, err := blogStore(r).CreateModule(r.Context(), m)
if err != nil {
diff --git a/internal/web/layout.go b/internal/web/layout.go
index 0d51068..6f5c84f 100644
--- a/internal/web/layout.go
+++ b/internal/web/layout.go
@@ -124,6 +124,9 @@ type Layout struct {
// A column is laid out when it has something to show — or always, when
// the theme keeps the columns' space.
HasLeft, HasRight bool
+ // A header search box sits in the menu row when there is one, so the two
+ // header modules need to know about each other.
+ HeaderMenu, HeaderSearch bool
}
// buildLayout groups modules by area, dropping areas the theme has switched off.
@@ -136,6 +139,8 @@ func buildLayout(t Theme, mods []store.Module) Layout {
switch m.Area {
case "header":
l.Header = append(l.Header, m)
+ l.HeaderMenu = l.HeaderMenu || m.Kind == "menu"
+ l.HeaderSearch = l.HeaderSearch || m.Kind == "search"
case "left":
l.Left = append(l.Left, m)
case "right":
@@ -181,6 +186,17 @@ func (l Layout) NeedsTags() bool {
return false
}
+// HasSearch says whether a visible module offers a search box (the search
+// page shows its own only when none does).
+func (l Layout) HasSearch() bool {
+ for _, m := range l.all() {
+ if m.Kind == "search" {
+ return true
+ }
+ }
+ return false
+}
+
// MaxRecent is the longest "recent posts" list any visible module wants (0 = none).
func (l Layout) MaxRecent() int {
n := 0
diff --git a/internal/web/static/blog.css b/internal/web/static/blog.css
index e2bf588..4fd7b76 100644
--- a/internal/web/static/blog.css
+++ b/internal/web/static/blog.css
@@ -80,12 +80,15 @@ img { max-width: 100%; height: auto; }
.tc-3 { font-size: 1.2em; }
.tc-4 { font-size: 1.45em; }
.tc-5 { font-size: 1.75em; }
-.site-search { border-bottom: 1px solid rgba(0,0,0,0.1); }
-.search-inner { padding: 0.5em 1em; }
+/* search box: in the header it floats at the right end of the menu row (its
+ own thin bar only when there is no menu); in a column it fills the box */
.search-form { margin: 0; }
-.search-form input { padding: 0.35em 0.5em; font: inherit; max-width: 100%; }
-.search-form button { padding: 0.35em 0.8em; font: inherit; }
-.mod-search input { width: 100%; margin-bottom: 0.4em; }
+.search-form input { padding: 0.3em 0.5em; font: inherit; font-size: 0.9em; max-width: 100%; border: 1px solid rgba(0,0,0,0.25); border-radius: 3px; background: rgba(255,255,255,0.85); color: #222; }
+.site-nav .search-form { float: right; margin: 0.35em 0 0.35em 1em; }
+.site-nav .nav-inner { overflow: hidden; }
+.site-search { border-bottom: 1px solid rgba(0,0,0,0.1); }
+.search-inner { padding: 0.4em 1em; }
+.mod-search input { width: 100%; }
.search-page { margin-bottom: 1.5em; }
.search-results { list-style: none; margin: 0; padding: 0; }
.search-results li { margin: 0 0 1.2em; }
@@ -104,4 +107,6 @@ img { max-width: 100%; height: auto; }
.col-left, .col-right { padding: 0; margin-top: 1.5em; }
.col-left { order: 0; }
.col-empty { display: none; } /* a reserved-but-empty column has nothing to stack */
+ .site-nav .search-form { float: none; margin: 0.35em 0.8em; }
+ .site-nav .search-form input { width: 100%; }
}
diff --git a/internal/web/templates/blog/search.html b/internal/web/templates/blog/search.html
index b146f5e..7ee05fc 100644
--- a/internal/web/templates/blog/search.html
+++ b/internal/web/templates/blog/search.html
@@ -1,7 +1,7 @@
{{define "title"}}{{if .Data.q}}{{tf "Results for “%s”" .Data.q}}{{else}}{{t "Search"}}{{end}} · {{.Blog.Title}}{{end}}
{{define "content"}}
<h1 class="page-title">{{if .Data.q}}{{tf "Results for “%s”" .Data.q}}{{else}}{{t "Search"}}{{end}}</h1>
-<div class="search-page">{{template "searchform" .}}</div>
+{{if not .Data.layout.HasSearch}}<div class="search-page">{{template "searchform" .}}</div>{{end}}
{{if not .Data.q}}<p class="muted">{{t "Type a word or two to find posts."}}</p>
{{else if not .Data.hits}}<p class="muted">{{tf "Nothing matched “%s”." .Data.q}}</p>
{{else}}
diff --git a/internal/web/templates/layouts/blog.html b/internal/web/templates/layouts/blog.html
index 3e854f6..6e8b819 100644
--- a/internal/web/templates/layouts/blog.html
+++ b/internal/web/templates/layouts/blog.html
@@ -58,7 +58,7 @@
</div>
{{- else if eq $m.Kind "logo"}}{{if $t.Logo}}{{if eq $m.Area "header"}}<div class="wrap header-logo"><a href="/"><img class="logo" src="/media/{{$t.Logo}}" alt="{{$r.Blog.Title}}"></a></div>
{{else}}<div class="mod mod-logo"><a href="/"><img class="logo" src="/media/{{$t.Logo}}" alt="{{$r.Blog.Title}}"></a></div>{{end}}{{end}}
-{{- else if eq $m.Kind "menu"}}{{if eq $m.Area "header"}}<div class="site-nav"><div class="wrap nav-inner">{{template "menulinks" $r}}</div></div>
+{{- else if eq $m.Kind "menu"}}{{if eq $m.Area "header"}}<div class="site-nav"><div class="wrap nav-inner">{{if $r.Data.layout.HeaderSearch}}{{template "searchform" $r}}{{end}}{{template "menulinks" $r}}</div></div>
{{else}}<div class="mod site-nav mod-menu">{{template "modtitle" $m}}<div class="nav-inner">{{template "menulinks" $r}}</div></div>{{end}}
{{- else if eq $m.Kind "recent"}}<div class="mod mod-recent">{{template "modtitle" $m}}
<ul>{{range $i, $p := $r.Data.recent}}{{if lt $i $m.Count}}<li><a href="/{{$p.PageSlug}}/{{$p.Slug}}">{{$p.Title}}</a></li>{{end}}{{end}}</ul>
@@ -76,7 +76,7 @@
{{- else if eq $m.Kind "tagcloud"}}<div class="mod mod-tagcloud">{{template "modtitle" $m}}
<p class="tagcloud">{{range $r.Data.tagcloud}}<a class="tc-{{.Size}}" href="/tag/{{.Slug}}" title="{{.Count}}">{{.Name}}</a> {{else}}<span class="muted">{{t "No tags yet."}}</span>{{end}}</p>
</div>
-{{- else if eq $m.Kind "search"}}{{if eq $m.Area "header"}}<div class="site-search"><div class="wrap search-inner">{{template "searchform" $r}}</div></div>
+{{- else if eq $m.Kind "search"}}{{if eq $m.Area "header"}}{{if not $r.Data.layout.HeaderMenu}}<div class="site-search"><div class="wrap search-inner">{{template "searchform" $r}}</div></div>{{end}}
{{else}}<div class="mod mod-search">{{template "modtitle" $m}}{{template "searchform" $r}}</div>{{end}}
{{- else if eq $m.Kind "html"}}{{if not $m.Body}}{{else if or (eq $m.Area "above") (eq $m.Area "below")}}<div class="custom-html custom-html-{{$m.Area}}">{{html $m.Body}}</div>
{{else}}<div class="mod mod-html">{{template "modtitle" $m}}{{html $m.Body}}</div>{{end}}
@@ -87,9 +87,11 @@
</div>
{{- end}}{{end}}
-{{/* The search box: a plain GET form, so it works without JavaScript. .Data.q
- only exists on the search page, hence the with. */}}
-{{define "searchform"}}<form method="get" action="/search" class="search-form"><input type="search" name="q"{{with .Data.q}} value="{{.}}"{{end}} placeholder="{{t "Search…"}}" size="18"> <button type="submit">{{t "Search"}}</button></form>{{end}}
+{{/* The search box: a plain GET form, so it works without JavaScript. Just the
+ input — Enter (or the phone keyboard's search key) submits a one-field
+ form everywhere, and the placeholder is the label, so the page does not
+ say "Search" over and over. .Data.q only exists on the search page. */}}
+{{define "searchform"}}<form method="get" action="/search" class="search-form"><input type="search" name="q"{{with .Data.q}} value="{{.}}"{{end}} placeholder="{{t "Search…"}}" aria-label="{{t "Search…"}}" size="16"></form>{{end}}
{{define "modtitle"}}{{if .Title}}<h3 class="mod-title">{{.Title}}</h3>{{end}}{{end}}