1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
|
# Search Functionality 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:** Implement unified search across desktop (modal), mobile (hamburger menu), and 404 page with lazy-loaded index and WCAG 2.1 AA compliance.
**Architecture:** Create a shared search module (`search.js`) with lazy-loading of `/search-index.json`, Alpine.js components for desktop modal and mobile integration, and refactor the 404 page to use the unified index. All styling uses Tailwind utilities; i18n keys added for localization.
**Tech Stack:** Hugo (JSON template), Alpine.js, Tailwind CSS, Feather Icons
---
## File Structure
### Create:
1. **`themes/danix-xyz-hacker/layouts/_default/search-index.json`** — Hugo template generating `/search-index.json` with all articles
2. **`themes/danix-xyz-hacker/assets/js/search.js`** — Shared search module with lazy-loading, filtering logic, and Alpine components
3. **`themes/danix-xyz-hacker/layouts/partials/search-modal.html`** — Desktop modal partial (hidden on mobile)
### Modify:
1. **`themes/danix-xyz-hacker/layouts/partials/header.html`** — Add search icon button (md-only)
2. **`themes/danix-xyz-hacker/layouts/partials/hamburger-menu.html`** — Insert search bar between nav and language toggle
3. **`themes/danix-xyz-hacker/layouts/_default/baseof.html`** — Include search-modal partial and search.js script
4. **`themes/danix-xyz-hacker/assets/js/not-found-page.js`** — Refactor to use shared index and filtering
5. **`i18n/en.yaml`** — Add search-related i18n keys
6. **`i18n/it.yaml`** — Add Italian translations for search keys
---
## Task 1: Generate Search Index JSON
**Files:**
- Create: `themes/danix-xyz-hacker/layouts/_default/search-index.json`
**Context:** Hugo will output this at `/search-index.json` during build. The template iterates over all articles and extracts title, URL, date, and summary (first 160 chars).
- [ ] **Step 1: Create the Hugo template file**
Create `themes/danix-xyz-hacker/layouts/_default/search-index.json` with the following content:
```golang
{{ $articles := where .Site.RegularPages "Section" "articles" }}
[
{{- range $index, $article := $articles -}}
{
"title": {{ $article.Title | jsonify }},
"url": {{ $article.Permalink | jsonify }},
"date": {{ $article.Date.Format "Jan 02, 2006" | jsonify }},
"summary": {{ substr ($article.Summary | plainify) 0 160 | jsonify }}
}
{{- if ne (add $index 1) (len $articles) }},{{ end }}
{{- end }}
]
```
**Explanation:**
- `jsonify` escapes strings for JSON safety
- `plainify` removes HTML tags from summary
- `substr` limits summary to first 160 characters
- Comma placement handles the last item (no trailing comma)
- [ ] **Step 2: Verify the template syntax is valid**
Run a quick check:
```bash
hugo list all | head -1
```
Expected: Output should show articles are detected. The JSON file will be generated on next build.
- [ ] **Step 3: Configure Hugo to output JSON correctly**
In `hugo.toml` or project config, ensure this output format is recognized. Check if there's an `[outputs]` section in `hugo.toml`:
```bash
grep -A 5 "\[outputs\]" hugo.toml || echo "No outputs section found"
```
If no outputs section exists, add one to ensure JSON files are published:
```toml
[outputs]
home = ["HTML", "JSON"]
section = ["HTML"]
page = ["HTML"]
```
Add this to `hugo.toml` if needed.
- [ ] **Step 4: Commit**
```bash
git add themes/danix-xyz-hacker/layouts/_default/search-index.json hugo.toml
git commit -m "feat: add search index JSON generation template
Generates /search-index.json at build time containing title, url, date, and summary for all articles. Template uses Hugo's jsonify and plainify filters for safe JSON output.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
```
---
## Task 2: Create Shared Search Module
**Files:**
- Create: `themes/danix-xyz-hacker/assets/js/search.js`
**Context:** This module exports utility functions and Alpine.js components used by desktop modal, mobile menu, and 404 page. It handles lazy-loading the index and filtering logic.
- [ ] **Step 1: Create the shared search module**
Create `themes/danix-xyz-hacker/assets/js/search.js`:
```javascript
// Lazy-load search index
async function loadSearchIndex() {
if (window.searchIndex) {
return window.searchIndex;
}
try {
const response = await fetch('/search-index.json');
if (!response.ok) throw new Error('Failed to load search index');
window.searchIndex = await response.json();
return window.searchIndex;
} catch (error) {
console.error('Error loading search index:', error);
return [];
}
}
// Filter articles by query (case-insensitive)
function filterArticles(query, articles) {
if (!query.trim()) {
return [];
}
const lowerQuery = query.toLowerCase();
return articles
.filter(article =>
article.title.toLowerCase().includes(lowerQuery) ||
article.summary.toLowerCase().includes(lowerQuery)
)
.slice(0, 5);
}
// Register Alpine.js components
document.addEventListener('alpine:init', () => {
// Desktop search modal component
Alpine.data('searchOverlay', () => ({
isOpen: false,
searchQuery: '',
filteredArticles: [],
allArticles: [],
indexLoaded: false,
async open() {
this.isOpen = true;
await this.ensureIndexLoaded();
this.$nextTick(() => {
const input = this.$el.querySelector('#search-input-desktop');
if (input) input.focus();
});
},
close() {
this.isOpen = false;
this.searchQuery = '';
this.filteredArticles = [];
},
async ensureIndexLoaded() {
if (!this.indexLoaded) {
this.allArticles = await loadSearchIndex();
this.indexLoaded = true;
}
},
filterArticles(query) {
this.searchQuery = query;
this.filteredArticles = filterArticles(query, this.allArticles);
},
handleEscape(event) {
if (event.key === 'Escape') {
this.close();
}
}
}));
// Mobile search component (integrated into hamburger menu)
Alpine.data('mobileSearch', () => ({
searchQuery: '',
filteredArticles: [],
allArticles: [],
indexLoaded: false,
async ensureIndexLoaded() {
if (!this.indexLoaded) {
this.allArticles = await loadSearchIndex();
this.indexLoaded = true;
}
},
filterArticles(query) {
this.searchQuery = query;
this.filteredArticles = filterArticles(query, this.allArticles);
}
}));
// Refactored 404 page component
Alpine.data('notFoundPage', () => ({
showEasterEgg: false,
searchQuery: '',
filteredArticles: [],
allArticles: [],
indexLoaded: false,
async init() {
await this.ensureIndexLoaded();
},
async ensureIndexLoaded() {
if (!this.indexLoaded) {
this.allArticles = await loadSearchIndex();
this.indexLoaded = true;
}
},
filterArticles(query) {
this.searchQuery = query;
this.filteredArticles = filterArticles(query, this.allArticles);
},
toggleEasterEgg() {
this.showEasterEgg = !this.showEasterEgg;
},
goToRandomArticle() {
if (this.allArticles.length > 0) {
const randomArticle = this.allArticles[Math.floor(Math.random() * this.allArticles.length)];
window.location.href = randomArticle.url;
}
}
}));
});
```
**Explanation:**
- `loadSearchIndex()` fetches and caches the JSON; called on first interaction
- `filterArticles()` performs case-insensitive matching on title/summary, returns max 5
- Three Alpine components share the same logic but manage their own state
- `ensureIndexLoaded()` lazy-loads on first use
- [ ] **Step 2: Run a syntax check**
```bash
node --check themes/danix-xyz-hacker/assets/js/search.js
```
Expected: No errors. If there's a syntax issue, it will be reported.
- [ ] **Step 3: Commit**
```bash
git add themes/danix-xyz-hacker/assets/js/search.js
git commit -m "feat: create shared search module with lazy-loading
Exports loadSearchIndex() and filterArticles() utilities plus Alpine.js components for desktop modal (searchOverlay), mobile menu (mobileSearch), and 404 page (notFoundPage). Implements lazy-loading of /search-index.json on first interaction and case-insensitive filtering (max 5 results).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
```
---
## Task 3: Create Desktop Search Modal Partial
**Files:**
- Create: `themes/danix-xyz-hacker/layouts/partials/search-modal.html`
**Context:** This partial renders the full-screen modal overlay, triggered by a search icon in the header. Only visible and used on desktop (≥768px).
- [ ] **Step 1: Create the modal partial**
Create `themes/danix-xyz-hacker/layouts/partials/search-modal.html`:
```html
<!-- Desktop Search Modal (hidden on mobile, shown via Alpine) -->
<div
x-cloak
x-data="searchOverlay()"
@keydown.escape.window="handleEscape($event)"
class="fixed inset-0 z-50"
:class="{ 'flex items-center justify-center': isOpen, 'hidden': !isOpen }"
x-show="isOpen"
>
<!-- Overlay backdrop -->
<div
class="absolute inset-0 bg-black/50"
@click="close()"
aria-hidden="true"
></div>
<!-- Modal content -->
<div
class="relative bg-bg border-2 border-accent rounded-lg shadow-xl max-w-2xl mx-4 w-full"
role="dialog"
aria-labelledby="search-modal-title"
aria-modal="true"
>
<!-- Header with close button -->
<div class="flex items-center justify-between p-6 border-b border-border">
<h2 id="search-modal-title" class="text-xl font-bold text-accent">
{{ i18n "searchArticles" }}
</h2>
<button
@click="close()"
aria-label="Close search"
class="p-2 rounded hover:bg-surface transition-colors focus:outline-none focus:ring-2 focus:ring-accent"
>
<i data-feather="x" class="w-5 h-5" aria-hidden="true"></i>
</button>
</div>
<!-- Search input -->
<div class="p-6 border-b border-border">
<label for="search-input-desktop" class="sr-only">
{{ i18n "searchPlaceholder" }}
</label>
<input
id="search-input-desktop"
type="text"
:value="searchQuery"
@input="filterArticles($el.value)"
placeholder="{{ i18n "searchPlaceholder" }}"
class="w-full px-4 py-3 border-2 border-border rounded focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent bg-bg text-text"
aria-describedby="search-results"
/>
</div>
<!-- Results container -->
<div id="search-results" class="max-h-96 overflow-y-auto p-6">
<!-- Results list -->
<div x-show="filteredArticles.length > 0" class="space-y-3" role="region" aria-live="polite">
<template x-for="article in filteredArticles" :key="article.url">
<div class="p-4 border-l-4 border-accent bg-bg/50 hover:bg-bg/70 transition-colors rounded">
<a :href="article.url" class="block focus:outline-none focus:ring-2 focus:ring-accent rounded px-2 py-1">
<h3 class="font-bold text-accent hover:underline" x-text="article.title"></h3>
<p class="text-sm text-text-dim mt-1" x-text="article.date"></p>
</a>
</div>
</template>
</div>
<!-- Empty state -->
<div
x-show="searchQuery && filteredArticles.length === 0"
class="text-center py-8 text-text-dim"
role="status"
>
{{ i18n "noSearchResults" }}
</div>
<!-- No query state -->
<div
x-show="!searchQuery"
class="text-center py-8 text-text-dim"
>
{{ i18n "searchPlaceholder" }}
</div>
</div>
</div>
</div>
```
**Explanation:**
- Modal hidden by default, shown when `isOpen` is true
- Backdrop click closes modal
- Esc key handled via `handleEscape()` in Alpine
- Focus trap: input auto-focused on open
- Results with max-height and scroll
- Semantic HTML: `role="dialog"`, `aria-modal="true"`, `aria-live="polite"` on results
- [ ] **Step 2: Verify indentation and structure**
```bash
grep -c "x-data" themes/danix-xyz-hacker/layouts/partials/search-modal.html
```
Expected: 1 (only the outer div has x-data)
- [ ] **Step 3: Commit**
```bash
git add themes/danix-xyz-hacker/layouts/partials/search-modal.html
git commit -m "feat: add desktop search modal partial
Creates full-screen overlay modal with search input and results list (max 5). Includes Esc key close, backdrop click, focus management, and WCAG 2.1 AA attributes (role=dialog, aria-labelledby, aria-live=polite).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
```
---
## Task 4: Add Search Icon to Header
**Files:**
- Modify: `themes/danix-xyz-hacker/layouts/partials/header.html` (lines 22-72)
**Context:** Add a search icon button in the right control area, next to the theme toggle. Hidden on mobile (md:flex), triggers the search modal.
- [ ] **Step 1: Read the current header**
Check lines 22-72 (right side controls section):
```bash
sed -n '22,72p' themes/danix-xyz-hacker/layouts/partials/header.html
```
Expected output shows the language switcher, theme toggle, and hamburger menu.
- [ ] **Step 2: Add search icon button**
Replace the comment `<!-- Right side controls: Language, Theme, Menu -->` section (lines 22-72) with:
```html
<!-- Right side controls: Search, Language, Theme, Menu -->
<div class="flex items-center gap-4 md:gap-6">
<!-- Search button (desktop only) -->
<button
@click="$dispatch('open-search')"
aria-label="{{ i18n "searchArticles" }}"
class="hidden md:flex p-2 rounded hover:bg-surface transition-colors focus:outline-none focus:ring-2 focus:ring-accent"
>
<i data-feather="search" class="w-5 h-5" aria-hidden="true"></i>
</button>
<!-- Language switcher (desktop) -->
<div class="hidden md:flex gap-2">
{{ $currentLang := .Page.Language }}
{{ $currentPath := .RelPermalink }}
{{ range .Site.Languages }}
{{ $langCode := .Lang }}
{{ $langName := .LanguageName }}
{{ $current := eq $langCode $currentLang }}
<!-- Build the translated URL by replacing language prefix -->
{{ $url := $currentPath }}
{{ if eq $langCode "en" }}
{{ if hasPrefix $currentPath "/it/" }}
{{ $url = strings.TrimPrefix "/it" $currentPath }}
{{ end }}
{{ else }}
{{ if not (hasPrefix $currentPath "/it/") }}
{{ $url = printf "/it%s" $currentPath }}
{{ end }}
{{ end }}
<a
href="{{ $url }}"
class="px-2 py-1 text-sm rounded transition-colors {{ if $current }}bg-accent text-white{{ else }}hover:bg-surface{{ end }}"
>
{{ $langName }}
</a>
{{ end }}
</div>
<!-- Theme toggle button -->
<button
id="theme-toggle"
aria-label="{{ i18n "toggleTheme" }}"
class="p-2 rounded hover:bg-surface transition-colors focus:outline-none focus:ring-2 focus:ring-accent"
>
<i id="theme-icon-sun" data-feather="sun" class="w-5 h-5" aria-hidden="true"></i>
<i id="theme-icon-moon" data-feather="moon" class="w-5 h-5" aria-hidden="true"></i>
</button>
<!-- Hamburger menu button (mobile only) -->
<button
x-data
@click="$dispatch('toggle-menu')"
aria-label="{{ i18n "toggleMenu" }}"
aria-controls="hamburger-menu"
class="md:hidden p-2 rounded hover:bg-surface transition-colors focus:outline-none focus:ring-2 focus:ring-accent"
>
<i data-feather="menu" class="w-5 h-5" aria-hidden="true"></i>
</button>
</div>
```
**Key changes:**
- Added search button with `hidden md:flex` (visible only on desktop)
- Dispatches `open-search` event for Alpine to listen
- Added `focus:ring-2 focus:ring-accent` to theme toggle and hamburger for consistency
- Added `aria-hidden="true"` to icons
- [ ] **Step 3: Verify the header structure**
```bash
grep -c "open-search" themes/danix-xyz-hacker/layouts/partials/header.html
```
Expected: 1
- [ ] **Step 4: Commit**
```bash
git add themes/danix-xyz-hacker/layouts/partials/header.html
git commit -m "feat: add search icon button to header
Adds magnifying glass icon button in desktop header (hidden on mobile). Dispatches 'open-search' event to trigger modal. Includes focus ring styling for accessibility.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
```
---
## Task 5: Listen for Search Event in Modal Partial
**Files:**
- Modify: `themes/danix-xyz-hacker/layouts/partials/search-modal.html` (line 6)
**Context:** Add event listener so the search icon click opens the modal.
- [ ] **Step 1: Add event listener to modal**
Update the modal's x-data opening div (line 6) to listen for the `open-search` event:
Old:
```html
<div
x-cloak
x-data="searchOverlay()"
@keydown.escape.window="handleEscape($event)"
```
New:
```html
<div
x-cloak
x-data="searchOverlay()"
@keydown.escape.window="handleEscape($event)"
@open-search.window="open()"
```
- [ ] **Step 2: Verify the change**
```bash
grep "@open-search" themes/danix-xyz-hacker/layouts/partials/search-modal.html
```
Expected: 1 match
- [ ] **Step 3: Commit**
```bash
git add themes/danix-xyz-hacker/layouts/partials/search-modal.html
git commit -m "feat: add open-search event listener to modal
Modal now listens for 'open-search' event dispatched by header search icon button.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
```
---
## Task 6: Integrate Search into Hamburger Menu
**Files:**
- Modify: `themes/danix-xyz-hacker/layouts/partials/hamburger-menu.html` (insert after nav, before language switcher)
**Context:** Add a search bar inside the hamburger menu overlay, positioned between navigation links and language toggle. Visible when menu is open.
- [ ] **Step 1: Read the current hamburger menu structure**
```bash
sed -n '1,45p' themes/danix-xyz-hacker/layouts/partials/hamburger-menu.html
```
Expected: Shows menu header, nav items, then language switcher starts around line 41.
- [ ] **Step 2: Insert search bar between nav and language switcher**
Insert the following HTML after the closing `</nav>` tag (around line 39) and before the `<!-- Language switcher -->` comment:
```html
<!-- Mobile search bar -->
<div class="p-6 border-b border-border" x-data="mobileSearch()" @open-mobile-search.window="ensureIndexLoaded()">
<label for="search-input-mobile" class="sr-only">
{{ i18n "searchPlaceholder" }}
</label>
<input
id="search-input-mobile"
type="text"
:value="searchQuery"
@input="filterArticles($el.value); ensureIndexLoaded()"
placeholder="{{ i18n "searchPlaceholder" }}"
class="w-full px-4 py-2 border-2 border-border rounded focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent bg-bg text-text text-sm"
aria-describedby="mobile-search-results"
/>
<!-- Mobile search results -->
<div id="mobile-search-results" class="mt-3 space-y-2" x-show="filteredArticles.length > 0" role="region" aria-live="polite">
<template x-for="article in filteredArticles" :key="article.url">
<div class="p-3 border-l-4 border-accent bg-bg/50 hover:bg-bg/70 transition-colors rounded text-sm">
<a :href="article.url" @click="menuOpen = false" class="block focus:outline-none focus:ring-2 focus:ring-accent rounded px-1 py-1">
<h4 class="font-bold text-accent" x-text="article.title"></h4>
<p class="text-xs text-text-dim mt-0.5" x-text="article.date"></p>
</a>
</div>
</template>
</div>
<!-- Empty state -->
<div
x-show="searchQuery && filteredArticles.length === 0"
class="mt-3 text-sm text-text-dim"
role="status"
>
{{ i18n "noSearchResults" }}
</div>
</div>
```
**Explanation:**
- Uses `mobileSearch` Alpine component (defined in search.js)
- Positioned between nav and language switcher
- Input triggers both `filterArticles()` and `ensureIndexLoaded()` (lazy-load on first type)
- Results styled similarly to desktop but smaller (text-sm)
- Click on result closes menu (via `menuOpen = false`)
- [ ] **Step 3: Verify structure**
```bash
grep -c "mobile-search-results" themes/danix-xyz-hacker/layouts/partials/hamburger-menu.html
```
Expected: 1
- [ ] **Step 4: Commit**
```bash
git add themes/danix-xyz-hacker/layouts/partials/hamburger-menu.html
git commit -m "feat: integrate search bar into mobile hamburger menu
Adds search input between nav links and language toggle. Uses mobileSearch Alpine component with lazy-loaded index. Clicking a result closes the menu. Styled consistently with desktop modal.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
```
---
## Task 7: Include Search Modal and Script in Base Template
**Files:**
- Modify: `themes/danix-xyz-hacker/layouts/_default/baseof.html` (before closing body tag)
**Context:** Include the search modal partial and the search.js script in the base template so they're available on all pages.
- [ ] **Step 1: Add search modal partial after footer**
Add the following after line 69 (the footer partial):
```html
<!-- Search modal (desktop and mobile) -->
{{ partial "search-modal.html" . }}
```
- [ ] **Step 2: Add search.js script before closing body tag**
Add the following before line 111 (before `</body>`) and after the matrix-rain script:
```html
<!-- Search functionality script -->
{{ $searchScript := resources.Get "js/search.js" | minify }}
<script src="{{ $searchScript.RelPermalink }}"></script>
```
- [ ] **Step 3: Verify the changes**
```bash
grep -c "search-modal.html" themes/danix-xyz-hacker/layouts/_default/baseof.html && \
grep -c "search.js" themes/danix-xyz-hacker/layouts/_default/baseof.html
```
Expected: 1 and 1
- [ ] **Step 4: Commit**
```bash
git add themes/danix-xyz-hacker/layouts/_default/baseof.html
git commit -m "feat: include search modal and script in base template
Adds search-modal.html partial and search.js script to all pages. Search modal available globally; script initializes Alpine components on page load.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
```
---
## Task 8: Add i18n Keys for Search
**Files:**
- Modify: `i18n/en.yaml`
- Modify: `i18n/it.yaml`
**Context:** Add search-related translation keys for both languages.
- [ ] **Step 1: Add English keys**
Add the following to `i18n/en.yaml` (after the existing entries, in the "Navigation & UI" section):
```yaml
searchArticles: "Search Articles"
searchPlaceholder: "Search by title or content..."
noSearchResults: "No articles found matching your search."
```
- [ ] **Step 2: Verify English file**
```bash
grep -c "searchArticles" i18n/en.yaml
```
Expected: 1
- [ ] **Step 3: Add Italian keys**
Add the following to `i18n/it.yaml` (after the existing entries, in the "Navigation & UI" section):
```yaml
searchArticles: "Cerca Articoli"
searchPlaceholder: "Cerca per titolo o contenuto..."
noSearchResults: "Nessun articolo trovato per la tua ricerca."
```
- [ ] **Step 4: Verify Italian file**
```bash
grep -c "searchArticles" i18n/it.yaml
```
Expected: 1
- [ ] **Step 5: Commit**
```bash
git add i18n/en.yaml i18n/it.yaml
git commit -m "feat: add search-related i18n keys for EN and IT
Adds searchArticles, searchPlaceholder, and noSearchResults keys for both English and Italian translations.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
```
---
## Task 9: Refactor 404 Page to Use Shared Search
**Files:**
- Modify: `themes/danix-xyz-hacker/assets/js/not-found-page.js`
- Modify: `themes/danix-xyz-hacker/layouts/404.en.html` (remove inline articles data)
- Modify: `themes/danix-xyz-hacker/layouts/404.it.html` (remove inline articles data)
**Context:** Replace the inline `window.articlesData` with the shared search logic, removing code duplication. The `notFoundPage` Alpine component in search.js now handles index loading.
- [ ] **Step 1: Update not-found-page.js to remove duplicate logic**
Replace the entire contents of `themes/danix-xyz-hacker/assets/js/not-found-page.js` with:
```javascript
// 404 page: initialize shared notFoundPage Alpine component
document.addEventListener('alpine:init', () => {
// Ensure search index is preloaded on 404 page
const notFoundElement = document.querySelector('[x-data*="notFoundPage"]');
if (notFoundElement && notFoundElement.__x) {
notFoundElement.__x.$data.init();
}
console.log('404 page initialized with shared search functionality');
});
```
**Explanation:**
- Removed duplicate `filterArticles()` function (now in search.js)
- Removed duplicate Alpine component (now in search.js)
- Calls `init()` on the Alpine component to preload the search index
- [ ] **Step 2: Remove inline articles data from 404.en.html**
In `themes/danix-xyz-hacker/layouts/404.en.html`, find and remove lines 3-15 (the `<script>` block with `window.articlesData`):
```bash
sed -i '4,14d' themes/danix-xyz-hacker/layouts/404.en.html
```
Expected: The script block is removed.
- [ ] **Step 3: Remove inline articles data from 404.it.html**
In `themes/danix-xyz-hacker/layouts/404.it.html`, find and remove lines 3-15 (the same script block):
```bash
sed -i '4,14d' themes/danix-xyz-hacker/layouts/404.it.html
```
- [ ] **Step 4: Update 404.en.html Alpine component call**
The 404 page must now call the shared Alpine component. Change line 18 (or thereabouts, after removing inline data):
Old:
```html
<div class="mx-auto px-4 py-12 max-w-4xl border border-border glow-accent rounded-lg bg-bg p-8" x-data="notFoundPage()">
```
New (no change needed—the component is already called the same way):
```html
<div class="mx-auto px-4 py-12 max-w-4xl border border-border glow-accent rounded-lg bg-bg p-8" x-data="notFoundPage()">
```
Verify the Alpine component is still there and correct.
- [ ] **Step 5: Update 404.it.html Alpine component call**
Same as above—verify the component call is unchanged.
- [ ] **Step 6: Verify the 404 pages still work**
Check that both 404 pages have the search input and results structure intact:
```bash
grep -c "@input=\"filterArticles" themes/danix-xyz-hacker/layouts/404.en.html
```
Expected: 1
- [ ] **Step 7: Commit**
```bash
git add themes/danix-xyz-hacker/assets/js/not-found-page.js themes/danix-xyz-hacker/layouts/404.en.html themes/danix-xyz-hacker/layouts/404.it.html
git commit -m "refactor: unify 404 page with shared search functionality
Removes inline window.articlesData from 404 pages. not-found-page.js now uses shared notFoundPage Alpine component from search.js. Eliminates code duplication; 404 page now benefits from lazy-loaded search index.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
```
---
## Task 10: Build and Test
**Files:**
- No new files; testing existing implementation
**Context:** Build the site and perform manual testing to ensure search works on desktop, mobile, and 404 page.
- [ ] **Step 1: Clean and build the site**
```bash
rm -rf public && hugo
```
Expected: Hugo builds without errors. `/search-index.json` should be generated in the public directory.
- [ ] **Step 2: Verify search index was generated**
```bash
ls -lh public/search-index.json && \
head -c 200 public/search-index.json
```
Expected: File exists and contains JSON array of articles.
- [ ] **Step 3: Start the dev server**
```bash
hugo server -D
```
Expected: Server runs at `http://localhost:1313/`.
- [ ] **Step 4: Test desktop search (≥768px viewport)**
Manual test in browser at `http://localhost:1313/`:
- Open browser DevTools (F12)
- Resize to desktop width (≥768px)
- Click the search icon (magnifying glass) in the header
- Verify modal opens, input is focused
- Type a query (e.g., "article" or a known article title)
- Verify results appear in real-time, max 5 shown
- Click a result → should navigate to article
- Press Esc → modal should close
- Click outside modal → modal should close
**Expected outcome:** All interactions work smoothly.
- [ ] **Step 5: Test mobile search (<768px viewport)**
Manual test on mobile or DevTools mobile view:
- Resize to mobile width (<768px)
- Verify search icon is NOT visible in header
- Open hamburger menu (click menu icon)
- Verify search bar is visible between nav links and language toggle
- Type a query
- Verify results appear below input
- Click a result → navigate to article and menu closes
- Type and verify no results state shows message
**Expected outcome:** Search bar is visible in menu, filtering works.
- [ ] **Step 6: Test 404 page search**
Manual test on 404 page:
- Navigate to a non-existent page (e.g., `http://localhost:1313/nonexistent`)
- Verify search bar is visible
- Type a query
- Verify results appear
- Click a result → navigate to article
- Verify no inline `articlesData` in DevTools console (no errors about missing data)
**Expected outcome:** 404 page search works; no console errors.
- [ ] **Step 7: Test lazy-loading**
Manual test in DevTools Network tab:
- Open Network tab (F12)
- Reload page
- Verify `/search-index.json` is NOT loaded on page load
- Click search icon (or open menu on mobile)
- Verify `/search-index.json` appears in Network tab as fetched
- Click search icon again (or type in search again)
- Verify `/search-index.json` is NOT fetched again (cached)
**Expected outcome:** Index is lazy-loaded only once per session.
- [ ] **Step 8: Test keyboard accessibility**
Manual test in browser:
- Tab through header → search icon should be focusable, show focus ring
- Press Enter on search icon → modal should open
- Inside modal, Tab should cycle through input and close button
- Press Esc → modal should close
- Open menu, Tab through search bar → should be focusable
**Expected outcome:** All interactive elements are keyboard accessible.
- [ ] **Step 9: Test language switching**
Manual test:
- Perform search on English page, note results
- Click language toggle to Italian
- Open search on Italian page
- Verify results use Italian article URLs (e.g., `/it/articles/...`)
- Search results should be the same (index includes all languages)
**Expected outcome:** Language switching works; search index is language-agnostic.
- [ ] **Step 10: Verify CSS compilation**
```bash
npm run build
```
Expected: Tailwind CSS builds without errors. Check that new Tailwind classes (focus:ring-2, focus:ring-accent, etc.) are included in compiled CSS.
- [ ] **Step 11: Stop dev server and commit test results**
```bash
# Stop hugo server (Ctrl+C)
git add -A
git commit -m "test: verify search functionality across desktop, mobile, and 404
Tested:
- Desktop modal: open, filter, navigate, Esc/backdrop close
- Mobile menu: search bar visible, filtering, results
- 404 page: search works, no console errors
- Lazy-loading: index fetched once per session
- Keyboard: all elements focusable, focus rings visible
- Language: results respect current language links
- CSS: Tailwind classes compiled successfully
All manual tests passed.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
```
---
## Task 11: Create Branch and Final Commit
**Files:**
- Branch: Create `week-8-search` (if following weekly branching from CLAUDE.md)
**Context:** Finalize the implementation by creating a feature branch and merging to master (as per the weekly workflow in CLAUDE.md).
- [ ] **Step 1: Verify all changes are committed**
```bash
git status
```
Expected: Nothing to commit, working tree clean.
- [ ] **Step 2: Create week-8 branch (if not already done)**
If you haven't created a branch yet:
```bash
git checkout -b week-8-search
```
If already on the branch, skip this step.
- [ ] **Step 3: View commit log**
```bash
git log --oneline | head -15
```
Expected: Shows all commits from Tasks 1-10.
- [ ] **Step 4: Run final build to ensure no errors**
```bash
hugo --cleanDestinationDir
```
Expected: Clean build with no errors or warnings.
- [ ] **Step 5: Merge to master (after code review)**
Once code review is complete:
```bash
git checkout master && \
git merge week-8-search --no-ff -m "merge: week 8 search functionality
Complete implementation of unified search across desktop header (modal), mobile hamburger menu, and 404 page. Features:
- Lazy-loaded /search-index.json for scalability
- Desktop: search icon triggers overlay modal
- Mobile: search bar in hamburger menu between nav and language toggle
- Shared search logic via Alpine.js components
- Full i18n support (EN/IT)
- WCAG 2.1 AA compliant (keyboard nav, focus management, screen reader support)
- Real-time filtering, max 5 results displayed
Commits: 11 (index generation, shared module, modal, header, menu, base template, i18n, 404 refactor, testing)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"
```
- [ ] **Step 6: Verify merge**
```bash
git log --oneline | head -1
```
Expected: Shows the merge commit with message.
- [ ] **Step 7: Push to remote (if applicable)**
```bash
git push origin master
```
(Adjust remote name if needed. Check MEMORY.md for your git workflow.)
- [ ] **Step 8: Cleanup**
Optional: Delete local feature branch if no longer needed:
```bash
git branch -d week-8-search
```
- [ ] **Step 9: Final summary commit (optional)**
If desired, create a final summary in HANDOFF.md documenting the completion:
```bash
echo "
## Week 8: Search Functionality (Completed)
Implemented unified search across the site:
- Desktop header search icon → full-screen modal overlay
- Mobile hamburger menu search bar (between nav and language toggle)
- Lazy-loaded /search-index.json for scalability
- Refactored 404 page to use shared search logic
- Full i18n (EN/IT), WCAG 2.1 AA compliant
- Real-time filtering, max 5 results
Merged to master on 2026-04-20.
" >> HANDOFF.md
```
Then commit:
```bash
git add HANDOFF.md && \
git commit -m "docs: update HANDOFF.md with week 8 completion"
```
---
## Success Checklist
- ✅ Search index generated at `/search-index.json`
- ✅ Desktop header search icon visible on desktop, hidden on mobile
- ✅ Desktop modal opens on icon click, closes on Esc/backdrop click
- ✅ Mobile search bar visible in hamburger menu
- ✅ Real-time filtering on both desktop and mobile
- ✅ Max 5 results displayed
- ✅ 404 page uses shared search logic, no duplicate code
- ✅ All i18n keys added (EN/IT)
- ✅ Lazy-loading verified (index fetched once per session)
- ✅ Keyboard accessibility (focus visible, Esc, Tab)
- ✅ WCAG 2.1 AA compliance (roles, aria-labels, aria-live)
- ✅ Tailwind utilities only, no new CSS file
- ✅ All commits follow git workflow
- ✅ Code builds without errors
---
## Implementation Notes
**Alpine.js Event Flow:**
1. Header search icon click → dispatches `open-search` event
2. Modal listens on `@open-search.window` → calls `open()`
3. Mobile menu search input → `@input` calls `filterArticles()`
4. Results update reactively via Alpine
**Lazy-Loading Strategy:**
- `loadSearchIndex()` called on first search interaction
- Cached in `window.searchIndex` to prevent refetching
- All three search contexts (desktop, mobile, 404) use same cache
**Code Reuse:**
- `filterArticles()` utility function used by all three contexts
- Alpine components share the same filtering logic
- Reduces maintenance burden and ensures consistent behavior
**Testing Coverage:**
- Manual tests cover desktop, mobile, 404, and keyboard accessibility
- Lazy-loading verified via Network tab
- Language switching verified
- Tailwind CSS compilation verified
|