diff options
Diffstat (limited to 'docs/superpowers')
| -rw-r--r-- | docs/superpowers/plans/2026-04-20-search-functionality.md | 1142 |
1 files changed, 1142 insertions, 0 deletions
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 index 0000000..1aaa1ee --- /dev/null +++ b/docs/superpowers/plans/2026-04-20-search-functionality.md @@ -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 |
