--- /dev/null
+# 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