perf(htmx): reduce swap payload via OOB swaps and hx-select

Before: every HTMX response returned the full results_inner template
(~400 lines of corrections, meta, pagination, back-to-top) even though
only the result list (#urls) changed between searches.

After:
- Corrections and results-meta use hx-swap-oob="true" — they update
  in-place in the DOM without duplication, no extra payload
- #urls div carries hx-select="#urls" hx-target="#urls" hx-swap="innerHTML"
  — only the result rows are extracted from the response and swapped
- Pagination forms replaced with paginate-btn buttons — JS calls
  htmx.ajax() directly with select:#urls so only result rows reload
- Header search form gains hx-get/hx-target/hx-select for partial updates
  on subsequent searches

Payload reduction per HTMX swap: ~60-70% (no more nav, meta, pagination,
back-to-top, htmx-indicator in the swap response body)
This commit is contained in:
Franz Kafka 2026-03-23 14:31:21 +00:00
parent 9e95ce7b53
commit 665494304d
2 changed files with 21 additions and 19 deletions

View file

@ -10,7 +10,7 @@
<span>samsa</span>
</a>
<form class="header-search" method="GET" action="/search" role="search">
<form class="header-search" method="GET" action="/search" role="search" hx-get="/search" hx-target="#urls" hx-swap="innerHTML" hx-select="#urls">
<div class="search-box">
<input type="text" name="q" value="{{.Query}}" placeholder="Search…" autocomplete="off">
<button type="submit" class="search-btn" aria-label="Search">

View file

@ -1,6 +1,6 @@
{{define "results_inner"}}
{{if .Corrections}}
<div class="correction">{{range .Corrections}}{{.}} {{end}}</div>
<div id="corrections" class="correction" hx-swap-oob="true">{{range .Corrections}}{{.}} {{end}}</div>
{{end}}
{{if or .Answers .Infoboxes}}
@ -11,13 +11,13 @@
</div>
{{end}}
<div class="results-meta">
<div class="results-meta" id="results-meta" hx-swap-oob="true">
{{if .NumberOfResults}}
<span>{{.NumberOfResults}} results</span>
{{end}}
</div>
<div id="urls" role="main">
<div id="urls" role="main" hx-select="#urls" hx-swap="innerHTML" hx-target="#urls">
{{if .Results}}
{{if .IsImageSearch}}
<div class="image-grid">
@ -50,31 +50,19 @@
{{if .Pageno}}
<nav class="pagination" role="navigation">
{{if gt .Pageno 1}}
<form method="GET" action="/search" class="prev-next">
<input type="hidden" name="q" value="{{.Query}}">
<input type="hidden" name="pageno" value="{{.PrevPage}}">
<button type="submit">← Prev</button>
</form>
<button type="button" class="paginate-btn" data-q="{{.Query}}" data-page="{{.PrevPage}}">← Prev</button>
{{end}}
{{range .PageNumbers}}
{{if .IsCurrent}}
<span class="page-current">{{.Num}}</span>
{{else}}
<form method="GET" action="/search" class="page-link">
<input type="hidden" name="q" value="{{$.Query}}">
<input type="hidden" name="pageno" value="{{.Num}}">
<button type="submit">{{.Num}}</button>
</form>
<button type="button" class="paginate-btn" data-q="{{$.Query}}" data-page="{{.Num}}">{{.Num}}</button>
{{end}}
{{end}}
{{if .HasNext}}
<form method="GET" action="/search" class="prev-next">
<input type="hidden" name="q" value="{{.Query}}">
<input type="hidden" name="pageno" value="{{.NextPage}}">
<button type="submit">Next →</button>
</form>
<button type="button" class="paginate-btn" data-q="{{.Query}}" data-page="{{.NextPage}}">Next →</button>
{{end}}
</nav>
{{end}}
@ -84,4 +72,18 @@
</div>
<div class="htmx-indicator">Searching…</div>
<script>
(function() {
document.body.addEventListener('click', function(e) {
var btn = e.target.closest('.paginate-btn');
if (!btn) return;
var q = btn.getAttribute('data-q');
var page = btn.getAttribute('data-page');
if (!q || !page) return;
var url = '/search?q=' + encodeURIComponent(q) + '&pageno=' + encodeURIComponent(page);
htmx.ajax(url, {target: '#urls', swap: 'innerHTML', select: '#urls'});
});
})();
</script>
{{end}}