]> danix's work - danix.xyz-2.git/commitdiff
docs: add search functionality implementation plan
authorDanilo M. <redacted>
Mon, 20 Apr 2026 11:14:07 +0000 (13:14 +0200)
committerDanilo M. <redacted>
Mon, 20 Apr 2026 11:14:07 +0000 (13:14 +0200)
Detailed 11-task plan for implementing unified search:
1. Generate search index JSON template
2. Create shared search module (lazy-loading, filtering, Alpine components)
3. Create desktop search modal partial
4. Add search icon to header
5. Wire search event listener
6. Integrate search into mobile hamburger menu
7. Include modal and script in base template
8. Add i18n keys (EN/IT)
9. Refactor 404 page to use shared search
10. Build and manual test
11. Create feature branch and merge

Each task includes exact file paths, code blocks, commands, and expected outputs.

Co-Authored-By: Claude Haiku 4.5 <redacted>
docs/superpowers/plans/2026-04-20-search-functionality.md [new file with mode: 0644]

diff --git a/docs/superpowers/plans/2026-04-20-search-functionality.md b/docs/superpowers/plans/2026-04-20-search-functionality.md
new file mode 100644 (file)
index 0000000..1aaa1ee
--- /dev/null
@@ -0,0 +1,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