Files
cv-site/doc/16-CMD-K-API.md
juanatsap 9a848e8c53 feat: Add CMD+K command palette with ninja-keys integration
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.
2025-12-01 13:03:06 +00:00

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:

  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)

{
  "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
  • 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:

  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:

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