Files
cv-site/doc/26-GO-ROUTES-API.md
juanatsap 2c7f8de242 refactor: centralize constants and reorganize documentation
- Create internal/constants package with all hardcoded values
  (environment, cookies, themes, headers, routes, cache)
- Create internal/httputil package for HTTP helper functions
- Update all handlers and middleware to use centralized constants
- Reorganize documentation with numbered prefixes (00-26)
- Remove duplicate docs from validation folder and docs/
- Delete handlers/constants.go (moved to internal/constants)
2025-12-06 16:27:12 +00:00

28 KiB

Go Routes and API Documentation

Overview

The CV site uses Go's standard net/http package with a custom routing setup in internal/routes/routes.go. The routing system applies a comprehensive middleware chain for security, logging, caching, and preferences management.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Middleware Chain                         │
│                                                              │
│  Request → Recovery → Logger → SecurityHeaders              │
│              ↓          ↓           ↓                        │
│         DynamicCache → Preferences → Router → Handler       │
│                                       ↓                      │
│                              ┌───────────────┐              │
│                              │  Mux Routes   │              │
│                              │  - Public     │              │
│                              │  - HTMX       │              │
│                              │  - API        │              │
│                              │  - Protected  │              │
│                              └───────────────┘              │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                  Protected Endpoints                        │
│                                                              │
│  /api/contact:                                              │
│    BrowserOnly → RateLimiter(5/hour) → Handler             │
│                                                              │
│  /export/pdf:                                               │
│    OriginChecker → RateLimiter(3/min) → Handler            │
│                                                              │
│  /static/*:                                                 │
│    CacheControl → FileServer                                │
└─────────────────────────────────────────────────────────────┘

Route Table

Public Routes

Route Method Handler Description Response Type
/ GET cvHandler.Home Main CV page with full layout HTML
/cv GET cvHandler.CVContent CV content partial (HTMX) HTML (partial)
/text GET cvHandler.PlainText Plain text CV for curl/AI text/plain
/health GET healthHandler.Check Health check endpoint JSON

/ - Home Page

Purpose: Serves the main CV page with full HTML layout

Handler: cvHandler.Home

Middleware:

  • Recovery
  • Logger
  • SecurityHeaders
  • DynamicCacheControl
  • PreferencesMiddleware

Response:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Juan's CV</title>
    <!-- CSS, meta tags, etc. -->
</head>
<body>
    <!-- Full page layout with header, CV content, footer -->
</body>
</html>

Status Codes:

  • 200 OK - Success
  • 500 Internal Server Error - Template error

/cv - CV Content Partial

Purpose: Returns CV content fragment for HTMX swaps

Handler: cvHandler.CVContent

Query Parameters:

  • lang - Language (en/es)
  • length - CV length (short/full)
  • icons - Show icons (true/false)

Response:

<section id="cv-content">
    <!-- CV sections without layout -->
    <div class="experience">...</div>
    <div class="education">...</div>
</section>

HTMX Usage:

<button hx-get="/cv?lang=es" hx-target="#cv-content">Spanish</button>

/text - Plain Text CV

Purpose: curl-friendly and AI-readable plain text CV

Handler: cvHandler.PlainText

Response:

JUAN ANDRÉS MORENO RUBIO
========================

Software Engineer | Go, HTMX, Cloud Architecture

EXPERIENCE
----------
Senior Software Engineer @ Company (2020-Present)
- Achievement 1
- Achievement 2

...

Usage:

curl https://juan.andres.morenorub.io/text

Status Codes:

  • 200 OK - Success
  • 500 Internal Server Error - Generation error

/health - Health Check

Purpose: Service health monitoring for load balancers

Handler: healthHandler.Check

Response:

{
  "status": "healthy",
  "timestamp": "2025-12-06T10:30:00Z",
  "version": "1.0.0"
}

Status Codes:

  • 200 OK - Service healthy
  • 503 Service Unavailable - Service degraded

HTMX Interactive Endpoints

Route Method Handler Description Response Type
/switch-language GET cvHandler.SwitchLanguage Toggle EN/ES language HTML (partial)
/toggle/length GET cvHandler.ToggleLength Toggle short/full CV HTML (partial)
/toggle/icons GET cvHandler.ToggleIcons Toggle icon display HTML (partial)
/toggle/theme GET cvHandler.ToggleTheme Toggle light/dark theme HTML (partial)

/switch-language - Language Toggle

Purpose: Switch between English and Spanish

Handler: cvHandler.SwitchLanguage

Mechanism:

  1. Reads current language from preferences cookie
  2. Toggles enes
  3. Sets new preference cookie
  4. Returns updated CV content

Response:

<section id="cv-content" hx-swap-oob="true">
    <!-- CV content in new language -->
</section>
<button id="lang-toggle" hx-swap-oob="true">
    ES <!-- Shows opposite language -->
</button>

HTMX Trigger:

<button hx-get="/switch-language" hx-target="body" hx-swap="outerHTML">
    {{ if eq .Language "en" }}ES{{ else }}EN{{ end }}
</button>

/toggle/length - CV Length Toggle

Purpose: Switch between short (1-page) and full (detailed) CV

Handler: cvHandler.ToggleLength

States:

  • short - Essential experience only (1 page)
  • full - Complete experience history

Response:

<section id="cv-content" hx-swap-oob="true">
    <!-- CV with different level of detail -->
</section>

Usage:

<button hx-get="/toggle/length" hx-target="#cv-content">
    {{ if eq .Length "short" }}Full CV{{ else }}Short CV{{ end }}
</button>

/toggle/icons - Icon Display Toggle

Purpose: Show/hide skill and technology icons

Handler: cvHandler.ToggleIcons

States:

  • true - Show icons (visual)
  • false - Hide icons (text only, PDF-friendly)

Response:

<section id="cv-content" hx-swap-oob="true">
    <!-- CV with/without icon sprites -->
</section>

/toggle/theme - Theme Toggle

Purpose: Switch between light and dark mode

Handler: cvHandler.ToggleTheme

States:

  • light - Light theme
  • dark - Dark theme

Response:

<html class="dark" hx-swap-oob="true">
    <!-- Class changed on root element -->
</html>

HTMX Trigger:

<button hx-get="/toggle/theme" hx-target="html" hx-swap="outerHTML">
    {{ if eq .Theme "dark" }}☀️{{ else }}🌙{{ end }}
</button>

API Endpoints

Route Method Handler Description Security
/api/cmd-k GET cvHandler.CmdKData Command palette data Standard chain
/api/contact POST cvHandler.HandleContact Contact form submission BrowserOnly + RateLimit(5/hour)

/api/cmd-k - Command Palette Data

Purpose: Provides search data for CMD+K command palette

Handler: cvHandler.CmdKData

Response:

{
  "commands": [
    {
      "id": "view-experience",
      "title": "View Experience",
      "description": "Jump to experience section",
      "action": "#experience"
    },
    {
      "id": "download-pdf",
      "title": "Download PDF",
      "description": "Export CV as PDF",
      "action": "/export/pdf"
    },
    {
      "id": "switch-language",
      "title": "Switch to Spanish",
      "description": "Change language to Spanish",
      "action": "/switch-language"
    }
  ]
}

Status Codes:

  • 200 OK - Success

/api/contact - Contact Form Submission

Purpose: Handle contact form submissions with comprehensive security

Handler: cvHandler.HandleContact

Method: POST

Security Chain:

BrowserOnly → RateLimiter(5/hour) → Handler

Request Body:

{
  "name": "Juan José",
  "email": "juan@example.com",
  "company": "ACME Corp",
  "subject": "Job Opportunity",
  "message": "I'd like to discuss...",
  "website": "",
  "timestamp": 1701867000
}

Validation Rules:

  • name: Required, max 100 chars, letters/spaces/hyphens only
  • email: Required, max 254 chars, valid RFC 5322 email
  • company: Optional, max 100 chars
  • subject: Required, max 200 chars
  • message: Required, max 5000 chars
  • website: Honeypot (must be empty)
  • timestamp: Must be 2s-24h old

Success Response (200 OK):

{
  "success": true,
  "message": "Message sent successfully"
}

Error Response (400 Bad Request):

{
  "success": false,
  "errors": [
    {
      "field": "email",
      "message": "Invalid email address format"
    }
  ]
}

Error Response (429 Too Many Requests):

<div class="alert alert-error">
    <h3>Too Many Requests</h3>
    <p>You've submitted too many contact forms. Please wait an hour before trying again.</p>
</div>

Status Codes:

  • 200 OK - Success
  • 400 Bad Request - Validation error
  • 403 Forbidden - BrowserOnly check failed
  • 429 Too Many Requests - Rate limit exceeded
  • 500 Internal Server Error - Email send failure

Security Features:

  1. BrowserOnly Middleware:

    • Blocks curl, Postman, wget
    • Requires User-Agent, Referer/Origin
    • Requires HTMX or XMLHttpRequest headers
  2. Rate Limiting:

    • 5 submissions per hour per IP
    • 1-hour window
    • Automatic cleanup
  3. Validation:

    • Email header injection prevention
    • Honeypot bot detection
    • Timing-based bot detection
    • HTML sanitization

Protected PDF Endpoint

Route Method Handler Description Security
/export/pdf GET cvHandler.ExportPDF Generate and download PDF OriginChecker + RateLimit(3/min)

/export/pdf - PDF Export

Purpose: Generate and serve CV as PDF

Handler: cvHandler.ExportPDF

Security Chain:

OriginChecker → RateLimiter(3/min) → Handler

Query Parameters:

  • lang - Language (en/es) - default: current preference
  • length - CV length (short/full) - default: current preference
  • icons - Show icons (true/false) - default: true

Response Headers:

Content-Type: application/pdf
Content-Disposition: attachment; filename="cv-jamr-2025-en.pdf"
Cache-Control: no-cache, no-store, must-revalidate

Status Codes:

  • 200 OK - PDF generated successfully
  • 403 Forbidden - Origin check failed or direct access blocked
  • 429 Too Many Requests - Rate limit exceeded (3/min)
  • 500 Internal Server Error - PDF generation failed

OriginChecker:

  • Checks Origin or Referer header
  • Allows: juan.andres.morenorub.io, localhost, 127.0.0.1
  • Blocks external site hotlinking
  • In production: Blocks direct access (requires referer)

RateLimiter:

  • Limit: 3 requests per minute per IP
  • Window: 60 seconds
  • Automatic entry cleanup

Usage:

<a href="/export/pdf?lang=en&length=short" target="_blank">
    Download Short CV (English)
</a>

PDF Shortcut Routes

Route Method Handler Description
/cv-jamr-* GET cvHandler.DefaultCVShortcut Year-aware PDF shortcuts

/cv-jamr-* - Default CV Shortcuts

Purpose: Friendly URLs for direct PDF access

Handler: cvHandler.DefaultCVShortcut

Pattern: /cv-jamr-{year}-{lang}.pdf

Examples:

  • /cv-jamr-2025-en.pdf - English CV for 2025
  • /cv-jamr-2025-es.pdf - Spanish CV for 2025
  • /cv-jamr-2024-en.pdf - English CV for 2024

Behavior:

  1. Parse year and language from URL
  2. Redirect to /export/pdf?lang={lang}&length=full
  3. Set appropriate filename in response

Status Codes:

  • 200 OK - PDF served successfully
  • 302 Found - Redirect to export endpoint
  • 400 Bad Request - Invalid URL format

Static Files

Route Method Handler Description Middleware
/static/* GET FileServer CSS, JS, images, fonts CacheControl

/static/* - Static Assets

Purpose: Serve static files (CSS, JavaScript, images, fonts)

Handler: http.FileServer(http.Dir("static"))

Directory Structure:

static/
├── css/
│   ├── main.css
│   └── themes/
│       ├── light.css
│       └── dark.css
├── js/
│   ├── app.js
│   └── htmx.min.js
├── images/
│   ├── logo.png
│   └── avatar.jpg
├── fonts/
│   └── inter.woff2
└── icons/
    └── sprites.svg

Cache Headers:

# Development
Cache-Control: public, max-age=3600  # 1 hour

# Production
Cache-Control: public, max-age=86400  # 1 day

Middleware: CacheControl

Examples:

/static/css/main.css
/static/js/htmx.min.js
/static/images/avatar.jpg
/static/fonts/inter.woff2
/static/icons/sprites.svg

Middleware Stack

Global Middleware Chain

Applied to all routes in this order:

handler := middleware.Recovery(
    middleware.Logger(
        middleware.SecurityHeaders(
            middleware.DynamicCacheControl(
                middleware.PreferencesMiddleware(mux),
            ),
        ),
    ),
)

1. Recovery

Purpose: Panic recovery and graceful error handling

File: internal/middleware/recovery.go

Behavior:

  • Catches panics in handlers
  • Logs stack trace
  • Returns 500 Internal Server Error
  • Prevents server crash

Error Response:

500 Internal Server Error
Internal server error

2. Logger

Purpose: Request logging for monitoring and debugging

File: internal/middleware/logger.go

Logged Information:

  • HTTP method
  • Request path
  • Response status code
  • Response time
  • Client IP

Log Format:

2025-12-06 10:30:15 | 200 | 15.234ms | 192.168.1.100 | GET /cv
2025-12-06 10:30:16 | 429 | 0.523ms  | 192.168.1.101 | POST /api/contact

3. SecurityHeaders

Purpose: Comprehensive security headers

File: internal/middleware/security.go

Headers Applied:

Header Value Purpose
X-Frame-Options SAMEORIGIN Prevent clickjacking
X-Content-Type-Options nosniff Prevent MIME sniffing
X-XSS-Protection 1; mode=block XSS protection (legacy)
Referrer-Policy strict-origin-when-cross-origin Privacy
Permissions-Policy geolocation=(), camera=(), ... Disable unnecessary features
Content-Security-Policy See CSP section below XSS/injection prevention
Strict-Transport-Security max-age=31536000 (prod only) Force HTTPS

Content Security Policy (CSP):

default-src 'self';
script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.jsdelivr.net https://esm.sh https://matomo.morenorub.io;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.iconify.design https://matomo.morenorub.io;
frame-ancestors 'self';
base-uri 'self';
form-action 'self'

HSTS (Production Only):

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

4. DynamicCacheControl

Purpose: Appropriate caching for dynamic HTML pages

File: internal/middleware/security.go

Cache Headers:

# Production
Cache-Control: public, max-age=300, must-revalidate  # 5 minutes

# Development
Cache-Control: no-cache, no-store, must-revalidate

Benefits:

  • Production: 5-minute cache reduces server load
  • Development: No cache for easy testing
  • must-revalidate: Ensures fresh content after expiry

5. PreferencesMiddleware

Purpose: Parse and inject user preferences from cookies

File: internal/middleware/preferences.go

Preferences:

  • Language (en/es)
  • Theme (light/dark)
  • CV length (short/full)
  • Icons display (true/false)

Behavior:

  1. Read preferences cookies
  2. Parse values
  3. Inject into request context
  4. Available to handlers via context

Cookie Names:

cv_language=en
cv_theme=dark
cv_length=full
cv_icons=true

Route-Specific Middleware

BrowserOnly

Applied To: /api/contact

Purpose: Block non-browser HTTP clients

File: internal/middleware/browser_only.go

Checks:

  1. User-Agent Validation:

    • Must be present
    • Must not be curl, wget, Postman, etc.
  2. Referer/Origin Validation:

    • At least one must be present
    • Prevents direct API calls
  3. Custom Header Validation:

    • HX-Request: true (HTMX), OR
    • X-Requested-With: XMLHttpRequest, OR
    • X-Browser-Request: true

Blocked User-Agents:

curl, wget, postman, insomnia, httpie, python-requests,
python-urllib, java, okhttp, go-http-client, axios,
node-fetch, apache-httpclient, libwww-perl, php, ruby,
scrapy, bot, crawler, spider

Error Response (403 Forbidden):

Forbidden: Browser access only

Security Benefit: Prevents automated bot submissions and API abuse


OriginChecker

Applied To: /export/pdf

Purpose: Prevent external site hotlinking

File: internal/middleware/security.go

Allowed Origins:

  • juan.andres.morenorub.io (production domain)
  • localhost (development)
  • 127.0.0.1 (development)
  • Custom domains from ALLOWED_ORIGINS env var

Validation:

  1. Check Origin header (CORS requests)
  2. Check Referer header (direct requests)
  3. In production: Require at least referer for PDF endpoint

Error Response (403 Forbidden):

Forbidden: External access not allowed
Forbidden: Direct access not allowed (production only)

Environment Configuration:

ALLOWED_ORIGINS="yourdomain.com,www.yourdomain.com"

RateLimiter

Applied To:

  • /api/contact - 5 requests/hour
  • /export/pdf - 3 requests/minute

Purpose: Prevent abuse and excessive resource usage

File: internal/middleware/security.go

Implementation:

// Contact form rate limiter
contactRateLimiter := middleware.NewRateLimiter(5, 1*time.Hour)

// PDF export rate limiter
pdfRateLimiter := middleware.NewRateLimiter(3, 1*time.Minute)

Features:

  • Per-IP tracking
  • Configurable limit and window
  • Automatic cleanup of expired entries
  • Thread-safe (sync.RWMutex)

Rate Limit Algorithm:

1. Get client IP (X-Forwarded-For → X-Real-IP → RemoteAddr)
2. Check if IP has entry
3. If no entry or expired:
   - Create new entry with count=1
   - Set resetTime = now + window
   - Allow request
4. If entry exists and not expired:
   - If count >= limit: Deny
   - Else: Increment count, Allow

Error Response (429 Too Many Requests):

HTTP/1.1 429 Too Many Requests
Retry-After: 60

Rate limit exceeded. Please try again later.

HTMX Error Response:

<div class="alert alert-error">
    <h3>Too Many Requests</h3>
    <p>You've submitted too many contact forms. Please wait an hour before trying again.</p>
</div>

Cleanup:

  • Runs every 1 minute (general) or 10 minutes (contact)
  • Removes expired IP entries
  • Prevents memory leak

CacheControl

Applied To: /static/*

Purpose: Aggressive caching for static assets

File: internal/middleware/security.go

Cache Headers:

# Development
Cache-Control: public, max-age=3600  # 1 hour

# Production
Cache-Control: public, max-age=86400  # 1 day

Benefits:

  • Reduces bandwidth
  • Improves page load speed
  • Offloads server processing

Cache Busting:

<!-- Version in filename for cache busting -->
<link rel="stylesheet" href="/static/css/main.css?v=1.2.3">

Security Features

1. HTTPS Enforcement (Production)

if os.Getenv("GO_ENV") == "production" {
    w.Header().Set("Strict-Transport-Security",
        "max-age=31536000; includeSubDomains; preload")
}

Effect: Forces HTTPS for 1 year, includes subdomains

2. Content Security Policy

Prevents XSS and injection attacks by whitelisting allowed sources:

script-src 'self' https://unpkg.com https://cdn.jsdelivr.net
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com
img-src 'self' data: https:

3. Multi-Layer Bot Protection

Contact Form:

  1. BrowserOnly middleware
  2. Honeypot field (website must be empty)
  3. Timing validation (2s-24h)
  4. Rate limiting (5/hour)

PDF Export:

  1. OriginChecker (prevents hotlinking)
  2. Rate limiting (3/minute)

4. Email Header Injection Prevention

// Validation checks for newlines and email headers
if ContainsEmailInjection(req.Subject) {
    return ValidationError{Field: "subject", Message: "Invalid characters"}
}

Blocked Patterns:

  • \r, \n characters
  • bcc:, cc:, to:, from:
  • content-type:, mime-version:

5. XSS Prevention

Template Auto-Escaping:

<!-- User input: <script>alert('XSS')</script> -->
<p>{{.UserInput}}</p>
<!-- Output: &lt;script&gt;alert('XSS')&lt;/script&gt; -->

Validation Sanitization:

Message string `validate:"required,trim,max=5000,sanitize"`
// Result: HTML-escaped, newlines removed

Error Responses

Standard Error Format

{
  "success": false,
  "error": "Error message",
  "errors": [
    {
      "field": "email",
      "tag": "email",
      "message": "Invalid email address format"
    }
  ]
}

HTTP Status Codes

Code Meaning Usage
200 OK Successful request
302 Found PDF shortcut redirect
400 Bad Request Validation error
403 Forbidden BrowserOnly or OriginChecker failed
404 Not Found Route not found
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server error, template error
503 Service Unavailable Health check failed

Configuration

Environment Variables

# Application environment
GO_ENV=production  # or "development"

# Allowed origins for PDF export
ALLOWED_ORIGINS="juan.andres.morenorub.io,www.juan.andres.morenorub.io"

# Template hot reload (development)
TEMPLATE_HOT_RELOAD=true

# Server configuration
PORT=8080
HOST=0.0.0.0

Route Priority

Routes are registered in order of specificity to avoid conflicts:

// 1. Specific patterns first
mux.HandleFunc("/cv-jamr-", cvHandler.DefaultCVShortcut)
mux.HandleFunc("/api/cmd-k", cvHandler.CmdKData)

// 2. Protected endpoints
mux.Handle("/api/contact", protectedContactHandler)
mux.Handle("/export/pdf", protectedPDFHandler)

// 3. General routes
mux.HandleFunc("/", cvHandler.Home)
mux.HandleFunc("/cv", cvHandler.CVContent)

// 4. Static files (catch-all)
mux.Handle("/static/", middleware.CacheControl(staticHandler))

Performance Considerations

1. Middleware Order Optimization

// Fast-fail first (Recovery catches panics immediately)
Recovery  Logger  SecurityHeaders  DynamicCache  Preferences  Mux

2. Rate Limiter Efficiency

// RWMutex for concurrent reads
type RateLimiter struct {
    mu      sync.RWMutex  // Read-heavy workload
    clients map[string]*rateLimitEntry
}

Performance:

  • Allow check: ~100-200 ns
  • Memory per IP: ~48 bytes
  • Cleanup overhead: Negligible (1/min)

3. Template Caching

Production mode (HotReload=false):

  • Templates loaded once at startup
  • Zero reload overhead
  • Thread-safe concurrent rendering

4. Static File Serving

// Native Go file server with proper cache headers
http.FileServer(http.Dir("static"))

Benefits:

  • Efficient sendfile() syscall
  • Range request support
  • ETag generation
  • Gzip compression (if configured)

Monitoring and Observability

Request Logging

2025-12-06 10:30:15 | 200 | 15.234ms | 192.168.1.100 | GET /cv
2025-12-06 10:30:16 | 429 | 0.523ms  | 192.168.1.101 | POST /api/contact
2025-12-06 10:30:17 | 200 | 125.678ms| 192.168.1.102 | GET /export/pdf

Logged Fields:

  • Timestamp
  • Status code
  • Response time
  • Client IP
  • Method + Path

Health Check Endpoint

curl https://juan.andres.morenorub.io/health

Response:

{
  "status": "healthy",
  "timestamp": "2025-12-06T10:30:00Z",
  "version": "1.0.0"
}

Use Cases:

  • Load balancer health checks
  • Uptime monitoring
  • Deployment verification

Rate Limiter Statistics

// Available for monitoring dashboards
func (rl *RateLimiter) GetStats() map[string]interface{} {
    return map[string]interface{}{
        "total_clients": len(rl.clients),
        "limit":         rl.limit,
        "window":        rl.window.String(),
    }
}

Testing Routes

Manual Testing

# Home page
curl https://juan.andres.morenorub.io/

# Plain text CV
curl https://juan.andres.morenorub.io/text

# Health check
curl https://juan.andres.morenorub.io/health

# PDF export (will be blocked - needs browser)
curl https://juan.andres.morenorub.io/export/pdf

# Contact form (will be blocked - needs browser)
curl -X POST https://juan.andres.morenorub.io/api/contact \
  -H "Content-Type: application/json" \
  -d '{"name":"Test","email":"test@example.com",...}'

Automated Testing

// Test route handlers
func TestHomeHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/", nil)
    w := httptest.NewRecorder()

    handler.ServeHTTP(w, req)

    if w.Code != http.StatusOK {
        t.Errorf("Expected 200, got %d", w.Code)
    }
}

// Test middleware
func TestBrowserOnly(t *testing.T) {
    req := httptest.NewRequest("POST", "/api/contact", nil)
    req.Header.Set("User-Agent", "curl/7.68.0")

    w := httptest.NewRecorder()
    middleware.BrowserOnly(mockHandler).ServeHTTP(w, req)

    if w.Code != http.StatusForbidden {
        t.Errorf("Expected 403, got %d", w.Code)
    }
}

Quick Reference

Route Overview

Public:
  /                    → Home page
  /cv                  → CV content partial
  /text                → Plain text CV
  /health              → Health check

HTMX:
  /switch-language     → Toggle EN/ES
  /toggle/length       → Toggle short/full
  /toggle/icons        → Toggle icons
  /toggle/theme        → Toggle light/dark

API:
  /api/cmd-k           → Command palette data
  /api/contact         → Contact form (protected)

Protected:
  /export/pdf          → PDF generation (rate limited)
  /cv-jamr-*           → PDF shortcuts

Static:
  /static/*            → CSS, JS, images, fonts

Middleware Chains

Global (all routes):
  Recovery → Logger → SecurityHeaders → DynamicCache → Preferences

Contact Form:
  + BrowserOnly → RateLimiter(5/hour)

PDF Export:
  + OriginChecker → RateLimiter(3/min)

Static Files:
  + CacheControl

Security Headers

X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: (comprehensive policy)
Strict-Transport-Security: max-age=31536000 (production only)
  • internal/routes/routes.go - Route setup and middleware chain
  • internal/middleware/security.go - Security middleware
  • internal/middleware/browser_only.go - BrowserOnly middleware
  • internal/middleware/contact_rate_limit.go - Contact rate limiting
  • internal/middleware/logger.go - Request logging
  • internal/middleware/recovery.go - Panic recovery
  • internal/middleware/preferences.go - User preferences
  • internal/handlers/cv.go - CV handlers
  • internal/handlers/health.go - Health check handler
  • internal/handlers/cv_contact.go - Contact form handler
  • internal/handlers/cv_pdf.go - PDF export handler

See Also