Implement a command palette accessible via CMD+K/Ctrl+K using the ninja-keys web component. Features include: - New /api/cmd-k endpoint serving dynamic CV entries (experiences, projects, courses) - Language-aware responses with 1-hour cache headers - Scroll-to-section functionality for quick navigation - Enhanced keyboard shortcuts modal with CMD+K documentation - Comprehensive test coverage for API and UI interactions Also includes cleanup of deprecated debug test files and various UI polish improvements to contact form, themes, and action bar components.
8.2 KiB
CMD+K Command Palette API Documentation
Overview
The CV application provides a command palette (CMD+K / Ctrl+K) powered by 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:
- Automatic Updates: New CV entries appear in CMD+K without code changes
- Language Support: API returns localized data based on language parameter
- Cache Efficiency: 1-hour cache headers reduce redundant requests
- 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)
{
"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
# 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:
// 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:
function mapExperienceActions(experiences) {
return experiences.map(exp => ({
id: exp.id,
title: exp.title,
section: exp.section,
keywords: `${exp.keywords} work job career`.toLowerCase(),
icon: '<iconify-icon icon="mdi:office-building" width="20"></iconify-icon>',
handler: () => scrollToSection(exp.id)
}));
}
Backend Implementation
Handler: cv_cmdk.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
// 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:
<div class="experience-item" id="exp-olympic-broadcasting">...</div>
<div class="project-item" id="proj-somos-una-ola">...</div>
<div class="course-item" id="course-codecademy-certifications">...</div>
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:
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:
go test ./internal/handlers/ -run TestCmdK -v
E2E Tests (Playwright/Bun)
Located at tests/mjs/71-cmd-k-api-scroll.test.mjs:
Tests:
- API returns valid JSON with expected structure
- Experience scroll navigation works
- Project scroll navigation works
- Course scroll navigation works
- Section scroll navigation works
- Multiple sequential searches work correctly
Run with:
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 |