# 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
