# CMD+K Command Palette API Documentation ## Overview The CV application provides a command palette (CMD+K / Ctrl+K) powered by [ninja-keys](https://github.com/nickadam/ninja-keys) web component. Dynamic entries (experiences, projects, courses) are loaded from a backend API endpoint, allowing automatic updates when CV data changes without modifying JavaScript code. ## Architecture ### Design Decision **API-First Approach**: Rather than hardcoding entries in JavaScript or reading from DOM elements, the command palette fetches its dynamic data from a dedicated API endpoint. This provides: 1. **Automatic Updates**: New CV entries appear in CMD+K without code changes 2. **Language Support**: API returns localized data based on language parameter 3. **Cache Efficiency**: 1-hour cache headers reduce redundant requests 4. **Separation of Concerns**: Frontend only handles rendering; backend owns data ### Data Flow ``` User opens CMD+K (Ctrl+K / Cmd+K) ↓ ninja-keys-init.js initializes ↓ fetch('/api/cmd-k?lang={en|es}') ↓ Backend loads CV data from JSON files ↓ Maps experiences, projects, courses to actions ↓ Returns JSON with action arrays ↓ Frontend combines with static actions ↓ ninja-keys displays searchable command palette ``` ## API Endpoint ### GET /api/cmd-k Returns dynamic entries for the ninja-keys command palette. **URL**: `/api/cmd-k` **Method**: `GET` **Authentication**: None (public endpoint) #### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `lang` | string | `en` | Language code (`en` or `es`) | #### Response **Content-Type**: `application/json` **Cache-Control**: `public, max-age=3600` (1 hour) ```json { "experiences": [ { "id": "exp-olympic-broadcasting", "title": "Olympic Broadcasting Services", "section": "Experience", "keywords": "Olympic Broadcasting Services Senior SAP Technical Consultant" } ], "projects": [ { "id": "proj-somos-una-ola", "title": "Somos Una Ola", "section": "Projects", "keywords": "Somos Una Ola Volunteer project promoting beach cleaning..." } ], "courses": [ { "id": "course-codecademy-certifications", "title": "Codecademy Certifications", "section": "Courses", "keywords": "Codecademy Certifications Codecademy" } ] } ``` #### Response Fields | Field | Type | Description | |-------|------|-------------| | `experiences` | array | Work experience entries | | `projects` | array | Personal/professional project entries | | `courses` | array | Course and certification entries | Each entry contains: | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier (e.g., `exp-{companyId}`, `proj-{projectId}`) | | `title` | string | Display title for the command palette | | `section` | string | Section label (`Experience`, `Projects`, `Courses`) | | `keywords` | string | Searchable keywords for filtering | #### Example Requests ```bash # English (default) curl http://localhost:1999/api/cmd-k # Spanish curl http://localhost:1999/api/cmd-k?lang=es # With jq formatting curl -s http://localhost:1999/api/cmd-k | jq '.' # Check response headers curl -I http://localhost:1999/api/cmd-k ``` #### Error Responses | Status | Description | |--------|-------------| | 500 | Failed to load CV data | ## Frontend Integration ### ninja-keys-init.js The frontend JavaScript fetches from the API and combines with static actions: ```javascript // Fetch dynamic entries from API async function fetchDynamicEntries() { try { const response = await fetch(`/api/cmd-k?lang=${lang}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); } catch (error) { console.error('Failed to fetch CMD+K data:', error); return { experiences: [], projects: [], courses: [] }; } } // Combine with static actions const dynamicData = await fetchDynamicEntries(); const actions = [ ...staticActions, ...mapExperienceActions(dynamicData.experiences || []), ...mapProjectActions(dynamicData.projects || []), ...mapCourseActions(dynamicData.courses || []) ]; ninjaKeys.data = actions; ``` ### Action Mapping Dynamic entries are converted to ninja-keys actions with handlers: ```javascript function mapExperienceActions(experiences) { return experiences.map(exp => ({ id: exp.id, title: exp.title, section: exp.section, keywords: `${exp.keywords} work job career`.toLowerCase(), icon: '', handler: () => scrollToSection(exp.id) })); } ``` ## Backend Implementation ### Handler: cv_cmdk.go ```go // CmdKData returns JSON data for the ninja-keys command palette func (h *CVHandler) CmdKData(w http.ResponseWriter, r *http.Request) { lang := r.URL.Query().Get("lang") if lang == "" { lang = "en" } if lang != "en" && lang != "es" { lang = "en" } cv, err := models.LoadCV(lang) if err != nil { http.Error(w, "Failed to load CV data", http.StatusInternalServerError) return } response := CmdKResponse{ Experiences: mapExperiences(cv.Experience), Projects: mapProjects(cv.Projects), Courses: mapCourses(cv.Courses), } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "public, max-age=3600") json.NewEncoder(w).Encode(response) } ``` ### Route Registration ```go // routes/routes.go // API routes (must be before "/" to avoid catch-all) mux.HandleFunc("/api/cmd-k", cvHandler.CmdKData) ``` ## ID Convention IDs follow a consistent pattern matching DOM element IDs for scroll targeting: | Type | Pattern | Example | |------|---------|---------| | Experience | `exp-{companyId}` | `exp-olympic-broadcasting` | | Project | `proj-{projectId}` | `proj-somos-una-ola` | | Course | `course-{courseId}` | `course-codecademy-certifications` | These IDs correspond to HTML element IDs in the page: ```html
...
...
...
``` ## Static Actions In addition to dynamic entries, the command palette includes static actions: ### Navigation - Jump to Top, Experience, Education, Skills, Projects, Courses, Languages, Awards, Other Info ### Shortcuts - Toggle CV Length (L key) - Toggle Icons (I key) - Toggle Theme (V key) - Show Shortcuts Help (? key) - Print CV (Cmd+P) ### Downloads - Download PDF (Default, Short, Extended versions) - View/Download Text CV ### Actions - Open Contact Form - Show Site Info - Toggle Zoom Controls - Switch Language (EN/ES) - Change Color Theme ### Social Links - LinkedIn, GitHub, Domestika, Personal Website ## Testing ### Unit Tests (Go) Located at `internal/handlers/cv_cmdk_test.go`: ```go func TestCmdKData(t *testing.T) { // Tests: Default language, English, Spanish, Invalid language fallback // Validates: Status code, Content-Type, response structure, counts } func TestCmdKDataCaching(t *testing.T) { // Validates Cache-Control header } ``` Run with: ```bash go test ./internal/handlers/ -run TestCmdK -v ``` ### E2E Tests (Playwright/Bun) Located at `tests/mjs/71-cmd-k-api-scroll.test.mjs`: Tests: 1. API returns valid JSON with expected structure 2. Experience scroll navigation works 3. Project scroll navigation works 4. Course scroll navigation works 5. Section scroll navigation works 6. Multiple sequential searches work correctly Run with: ```bash HEADLESS=true bun run tests/mjs/71-cmd-k-api-scroll.test.mjs ``` ## Performance - **Cache Duration**: 1 hour (reduces API calls on page refresh) - **Response Size**: ~2-3 KB (compact JSON) - **Load Time**: API fetched during page initialization - **Fallback**: Empty arrays returned on error (graceful degradation) ## Files | File | Purpose | |------|---------| | `internal/handlers/cv_cmdk.go` | API handler | | `internal/handlers/cv_cmdk_test.go` | Unit tests | | `internal/routes/routes.go` | Route registration | | `static/js/ninja-keys-init.js` | Frontend integration | | `tests/mjs/71-cmd-k-api-scroll.test.mjs` | E2E tests |