From: Danilo M. Date: Mon, 20 Apr 2026 11:14:07 +0000 (+0200) Subject: docs: add search functionality implementation plan X-Git-Tag: release_22042026-1342~75 X-Git-Url: https://git.danix.xyz/?a=commitdiff_plain;h=c7d64a6fa58da9337ce6acc16ab8427798235341;p=danix.xyz-2.git docs: add search functionality implementation plan 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 --- 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 " +``` + +--- + +## 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 `