# 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:**
```html
Juan's CV
```
**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:**
```html
```
**HTMX Usage:**
```html
```
---
#### `/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:**
```bash
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:**
```json
{
"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 `en` ↔ `es`
3. Sets new preference cookie
4. Returns updated CV content
**Response:**
```html
```
**HTMX Trigger:**
```html
```
---
#### `/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:**
```html
```
**Usage:**
```html
```
---
#### `/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:**
```html
```
---
#### `/toggle/theme` - Theme Toggle
**Purpose:** Switch between light and dark mode
**Handler:** `cvHandler.ToggleTheme`
**States:**
- `light` - Light theme
- `dark` - Dark theme
**Response:**
```html
```
**HTMX Trigger:**
```html
```
### 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:**
```json
{
"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:**
```json
{
"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):**
```json
{
"success": true,
"message": "Message sent successfully"
}
```
**Error Response (400 Bad Request):**
```json
{
"success": false,
"errors": [
{
"field": "email",
"message": "Invalid email address format"
}
]
}
```
**Error Response (429 Too Many Requests):**
```html
Too Many Requests
You've submitted too many contact forms. Please wait an hour before trying again.
```
**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:**
```html
Download Short CV (English)
```
### 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:
```go
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:**
```bash
# 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:**
```go
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:**
```bash
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:**
```go
// 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:**
```html
Too Many Requests
You've submitted too many contact forms. Please wait an hour before trying again.
```
**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:**
```bash
# 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:**
```html
```
## Security Features
### 1. HTTPS Enforcement (Production)
```go
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
```go
// 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:**
```html
{{.UserInput}}
```
**Validation Sanitization:**
```go
Message string `validate:"required,trim,max=5000,sanitize"`
// Result: HTML-escaped, newlines removed
```
## Error Responses
### Standard Error Format
```json
{
"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
```bash
# 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:
```go
// 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
```go
// Fast-fail first (Recovery catches panics immediately)
Recovery → Logger → SecurityHeaders → DynamicCache → Preferences → Mux
```
### 2. Rate Limiter Efficiency
```go
// 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
```go
// 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
```bash
curl https://juan.andres.morenorub.io/health
```
**Response:**
```json
{
"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
```go
// 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
```bash
# 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
```go
// 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)
```
## Related Files
- `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
- [Validation System Documentation](go-validation-system.md)
- [Template System Documentation](go-template-system.md)
- [Go net/http Package](https://pkg.go.dev/net/http)