Skip to content

Commit 6a99050

Browse files
dvdksnclaude
andcommitted
guides: add text filter to the landing page
- Instant client-side filter (Alpine.js) matching title, summary, and tags across the already-rendered guide list; multi-word queries are AND-matched - Refined search input with inline clear button and Esc-to-clear - While filtering: hide the jump-to rail and go single-column full-width, flatten the grouped sections into one list, and show a "N of M guides" meta bar with a Clear action - Container fills the viewport (min-height) so a single result no longer collapses the page - Whole-row link cards with hover affordance (arrow), plus a polished empty-results state Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ef6ce39 commit 6a99050

1 file changed

Lines changed: 118 additions & 30 deletions

File tree

layouts/guides/landing.html

Lines changed: 118 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,30 @@
55
{{ define "main" }}
66
{{- $tagOrder := slice "languages" "ai" "testing" "cicd" "security" "databases" "deployment" "admin" "labs" }}
77

8-
<div class="not-prose">
8+
<div
9+
class="not-prose mx-auto flex min-h-[calc(100dvh-7rem)] max-w-7xl flex-col"
10+
x-data="{
11+
activeTag: 'languages',
12+
query: '',
13+
words() { return this.query.trim().toLowerCase().split(/\s+/).filter(Boolean) },
14+
filtering() { return this.query.trim().length > 0 },
15+
matchText(text) { return this.words().every(w => text.includes(w)) },
16+
rowVisible(el) { return !this.filtering() || this.matchText(el.dataset.search) },
17+
sectionVisible(el) { return !this.filtering() || Array.from(el.querySelectorAll('[data-guide]')).some(r => this.matchText(r.dataset.search)) },
18+
resultCount() { return Array.from(document.querySelectorAll('[data-guide]')).filter(r => this.matchText(r.dataset.search)).length },
19+
init() {
20+
const obs = new IntersectionObserver(
21+
entries => entries.forEach(e => { if (e.isIntersecting) this.activeTag = e.target.id }),
22+
{ rootMargin: '-20% 0px -70% 0px', threshold: 0 }
23+
);
24+
document.querySelectorAll('section[data-tag]').forEach(s => obs.observe(s));
25+
}
26+
}"
27+
>
928
{{- partial "breadcrumbs.html" . }}
1029

11-
<div class="mb-12 mt-4">
12-
<p class="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500">
30+
<div class="mt-4">
31+
<p class="text-xs font-semibold tracking-widest text-gray-400 uppercase dark:text-gray-500">
1332
{{- len .Pages }} guides
1433
</p>
1534
<h1
@@ -23,22 +42,39 @@
2342
{{- end }}
2443
</div>
2544

26-
<div
27-
class="flex gap-12"
28-
x-data="{
29-
activeTag: 'languages',
30-
init() {
31-
const obs = new IntersectionObserver(
32-
entries => entries.forEach(e => { if (e.isIntersecting) this.activeTag = e.target.id }),
33-
{ rootMargin: '-20% 0px -70% 0px', threshold: 0 }
34-
);
35-
document.querySelectorAll('section[data-tag]').forEach(s => obs.observe(s));
36-
}
37-
}"
38-
>
39-
<!-- Sticky jump nav -->
40-
<nav class="hidden xl:flex w-52 flex-none flex-col self-start sticky top-20">
41-
<p class="mb-3 text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500">
45+
<!-- Text filter -->
46+
<div class="relative mt-8 mb-10 max-w-xl">
47+
<span
48+
class="icon-svg icon-sm pointer-events-none absolute top-1/2 left-3.5 -translate-y-1/2 text-gray-400 dark:text-gray-500"
49+
>
50+
{{ partialCached "icon" "magnifying-glass" "magnifying-glass" }}
51+
</span>
52+
<input
53+
type="search"
54+
x-model="query"
55+
@keydown.escape="query = ''"
56+
aria-label="Filter guides"
57+
placeholder="Filter guides by name, topic, or tag…"
58+
class="w-full rounded-xl border border-gray-300 bg-white py-2.5 pr-10 pl-11 text-sm text-gray-900 shadow-sm transition placeholder:text-gray-400 focus:border-blue-400 focus:ring-2 focus:ring-blue-400/40 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder:text-gray-500"
59+
/>
60+
<button
61+
x-show="query"
62+
x-cloak
63+
@click="query = ''"
64+
aria-label="Clear filter"
65+
class="icon-svg icon-sm absolute top-1/2 right-2.5 -translate-y-1/2 rounded-md p-1 text-gray-400 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-gray-200"
66+
>
67+
{{ partialCached "icon" "x-mark" "x-mark" }}
68+
</button>
69+
</div>
70+
71+
<div class="flex flex-1 gap-16 xl:gap-24">
72+
<!-- Sticky jump nav (hidden while filtering) -->
73+
<nav
74+
x-show="!filtering()"
75+
class="sticky top-20 hidden w-52 flex-none flex-col self-start xl:flex"
76+
>
77+
<p class="mb-3 text-xs font-semibold tracking-widest text-gray-400 uppercase dark:text-gray-500">
4278
Jump to
4379
</p>
4480
{{- range $i, $tag := $tagOrder }}
@@ -63,19 +99,39 @@
6399
{{- end }}
64100
</nav>
65101

66-
<!-- Sections -->
102+
<!-- Guide list -->
67103
<div class="min-w-0 flex-1">
104+
<!-- Filtering meta bar -->
105+
<div
106+
x-show="filtering()"
107+
x-cloak
108+
class="mb-8 flex items-center justify-between border-b border-gray-200 pb-4 dark:border-gray-800"
109+
>
110+
<p class="text-sm text-gray-500 dark:text-gray-400">
111+
<span class="font-semibold text-gray-900 dark:text-gray-100" x-text="resultCount()"></span>
112+
of {{ len .Pages }} guides
113+
</p>
114+
<button
115+
@click="query = ''"
116+
class="text-sm font-medium text-blue-600 transition hover:underline dark:text-blue-400"
117+
>
118+
Clear filter
119+
</button>
120+
</div>
121+
68122
{{- range $i, $tag := $tagOrder }}
69123
{{- $tagData := index hugo.Data.tags $tag }}
70124
{{- $pages := where $.Pages "Params.tags" "intersect" (slice $tag) }}
71125
{{- if $pages }}
72126
<section
73127
id="{{ $tag }}"
74128
data-tag="{{ $tag }}"
75-
class="scroll-mt-20 border-t border-gray-200 py-12 first:border-t-0 first:pt-0 dark:border-gray-800"
129+
x-show="sectionVisible($el)"
130+
class="scroll-mt-16"
131+
:class="filtering() ? '' : 'border-t border-gray-200 py-12 first:border-t-0 first:pt-0 dark:border-gray-800'"
76132
>
77-
<div class="mb-6">
78-
<p class="mb-1 text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500">
133+
<div class="mb-6" x-show="!filtering()">
134+
<p class="mb-1 text-xs font-semibold tracking-widest text-gray-400 uppercase dark:text-gray-500">
79135
{{- printf "%02d" (add $i 1) -}}
80136
</p>
81137
<h2 class="text-2xl font-bold">{{ $tagData.title }}</h2>
@@ -85,18 +141,50 @@ <h2 class="text-2xl font-bold">{{ $tagData.title }}</h2>
85141
</div>
86142
<div class="divide-y divide-gray-200 dark:divide-gray-800">
87143
{{- range $pages }}
88-
<div class="-mx-3 grid grid-cols-[5fr_6fr] gap-6 rounded px-3 py-4 hover:bg-gray-50 dark:hover:bg-gray-900">
89-
<a
90-
href="{{ .Permalink }}"
91-
class="font-medium leading-snug hover:underline"
92-
>{{ .Title }}</a>
93-
<p class="text-sm leading-relaxed text-gray-500 dark:text-gray-400">{{ .Summary }}</p>
94-
</div>
144+
{{- $tagTitles := slice }}
145+
{{- range .Params.tags }}{{ $tagTitles = $tagTitles | append (index hugo.Data.tags .).title }}{{ end }}
146+
{{- $search := lower (printf "%s %s %s %s" .Title .Summary (delimit .Params.tags " ") (delimit $tagTitles " ")) }}
147+
{{- $search = $search | replaceRE "\\s+" " " }}
148+
<a
149+
href="{{ .Permalink }}"
150+
data-guide
151+
data-search="{{ $search }}"
152+
x-show="rowVisible($el)"
153+
class="group -mx-3 grid grid-cols-[5fr_6fr_auto] items-start gap-6 rounded-lg px-3 py-4 transition-colors hover:bg-gray-50 dark:hover:bg-gray-900"
154+
>
155+
<span class="font-medium leading-snug text-gray-900 transition-colors group-hover:text-blue-600 dark:text-gray-100 dark:group-hover:text-blue-400">{{ .Title }}</span>
156+
<span class="text-sm leading-relaxed text-gray-500 dark:text-gray-400">{{ .Summary }}</span>
157+
<span
158+
class="icon-svg icon-sm mt-0.5 self-center text-gray-300 opacity-0 transition-all duration-200 group-hover:translate-x-0.5 group-hover:text-blue-600 group-hover:opacity-100 dark:text-gray-600 dark:group-hover:text-blue-400"
159+
>
160+
{{ partialCached "icon" "arrow-right" "arrow-right" }}
161+
</span>
162+
</a>
95163
{{- end }}
96164
</div>
97165
</section>
98166
{{- end }}
99167
{{- end }}
168+
169+
<!-- No matches -->
170+
<div
171+
x-show="filtering() && resultCount() === 0"
172+
x-cloak
173+
class="flex flex-col items-center justify-center gap-3 py-24 text-center"
174+
>
175+
<span class="icon-svg icon-lg text-gray-300 dark:text-gray-600">
176+
{{ partialCached "icon" "magnifying-glass" "magnifying-glass" }}
177+
</span>
178+
<p class="text-gray-500 dark:text-gray-400">
179+
No guides match “<span class="font-medium text-gray-700 dark:text-gray-200" x-text="query"></span>”.
180+
</p>
181+
<button
182+
@click="query = ''"
183+
class="text-sm font-medium text-blue-600 transition hover:underline dark:text-blue-400"
184+
>
185+
Clear filter
186+
</button>
187+
</div>
100188
</div>
101189
</div>
102190
</div>

0 commit comments

Comments
 (0)