- 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)
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- Success500 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- Success500 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 healthy503 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:
- Reads current language from preferences cookie
- Toggles
en↔es - Sets new preference cookie
- 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 themedark- 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 onlyemail: Required, max 254 chars, valid RFC 5322 emailcompany: Optional, max 100 charssubject: Required, max 200 charsmessage: Required, max 5000 charswebsite: 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- Success400 Bad Request- Validation error403 Forbidden- BrowserOnly check failed429 Too Many Requests- Rate limit exceeded500 Internal Server Error- Email send failure
Security Features:
-
BrowserOnly Middleware:
- Blocks curl, Postman, wget
- Requires User-Agent, Referer/Origin
- Requires HTMX or XMLHttpRequest headers
-
Rate Limiting:
- 5 submissions per hour per IP
- 1-hour window
- Automatic cleanup
-
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 preferencelength- CV length (short/full) - default: current preferenceicons- 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 successfully403 Forbidden- Origin check failed or direct access blocked429 Too Many Requests- Rate limit exceeded (3/min)500 Internal Server Error- PDF generation failed
OriginChecker:
- Checks
OriginorRefererheader - 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:
- Parse year and language from URL
- Redirect to
/export/pdf?lang={lang}&length=full - Set appropriate filename in response
Status Codes:
200 OK- PDF served successfully302 Found- Redirect to export endpoint400 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:
- Read preferences cookies
- Parse values
- Inject into request context
- 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:
-
User-Agent Validation:
- Must be present
- Must not be curl, wget, Postman, etc.
-
Referer/Origin Validation:
- At least one must be present
- Prevents direct API calls
-
Custom Header Validation:
HX-Request: true(HTMX), ORX-Requested-With: XMLHttpRequest, ORX-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_ORIGINSenv var
Validation:
- Check
Originheader (CORS requests) - Check
Refererheader (direct requests) - 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:
- BrowserOnly middleware
- Honeypot field (
websitemust be empty) - Timing validation (2s-24h)
- Rate limiting (5/hour)
PDF Export:
- OriginChecker (prevents hotlinking)
- 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,\ncharactersbcc:,cc:,to:,from:content-type:,mime-version:
5. XSS Prevention
Template Auto-Escaping:
<!-- User input: <script>alert('XSS')</script> -->
<p>{{.UserInput}}</p>
<!-- Output: <script>alert('XSS')</script> -->
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)
Related Files
internal/routes/routes.go- Route setup and middleware chaininternal/middleware/security.go- Security middlewareinternal/middleware/browser_only.go- BrowserOnly middlewareinternal/middleware/contact_rate_limit.go- Contact rate limitinginternal/middleware/logger.go- Request logginginternal/middleware/recovery.go- Panic recoveryinternal/middleware/preferences.go- User preferencesinternal/handlers/cv.go- CV handlersinternal/handlers/health.go- Health check handlerinternal/handlers/cv_contact.go- Contact form handlerinternal/handlers/cv_pdf.go- PDF export handler