# 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 " ``` --- ## 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 " ``` --- ## 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
``` **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 " ``` --- ## 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 `` section (lines 22-72) with: ```html
``` **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 " ``` --- ## 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
" ``` --- ## 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 `` tag (around line 39) and before the `` comment: ```html
{{ i18n "noSearchResults" }}
``` **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 " ``` --- ## 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 {{ partial "search-modal.html" . }} ``` - [ ] **Step 2: Add search.js script before closing body tag** Add the following before line 111 (before ``) and after the matrix-rain script: ```html {{ $searchScript := resources.Get "js/search.js" | minify }} ``` - [ ] **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 " ``` --- ## 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 " ``` --- ## 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 `