Initial commit: Go + HTMX CV Site

- Minimal, professional CV design with paper-on-gray layout
- Bilingual support (Spanish/English) with HTMX language switching
- JSON-based content management (cv-en.json, cv-es.json)
- Print-optimized CSS for PDF export
- Responsive design for all devices
- Go backend with stdlib net/http
- Clean, maintainable codebase

Features:
- 18+ years professional experience
- SAP CDC expertise
- Complete project history
- Education, certifications, awards
- Multi-language support

Tech stack: Go, HTMX, vanilla CSS
This commit is contained in:
juanatsap
2025-10-20 08:54:21 +01:00
commit dab68f34f2
28 changed files with 5862 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
# Environment Configuration Example
# Copy this file to .env and customize as needed
# Server Configuration
PORT=8080
HOST=localhost
GO_ENV=development
# Template Configuration
TEMPLATE_DIR=templates
PARTIALS_DIR=templates/partials
TEMPLATE_HOT_RELOAD=true
# Data Configuration
DATA_DIR=data
# Server Timeouts (seconds)
READ_TIMEOUT=15
WRITE_TIMEOUT=15
# Production Settings
# Uncomment for production:
# GO_ENV=production
# TEMPLATE_HOT_RELOAD=false
# READ_TIMEOUT=30
# WRITE_TIMEOUT=30
+31
View File
@@ -0,0 +1,31 @@
# Binaries
cv-server
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binaries
*.test
# Output
*.out
# Go workspace
go.work
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Temp files
*.tmp
*.log
+395
View File
@@ -0,0 +1,395 @@
# Architecture Documentation
## Overview
This CV website is built following Go best practices with a focus on:
- **Simplicity**: Clean, readable code
- **Maintainability**: Well-structured packages
- **Reliability**: Proper error handling and logging
- **Performance**: Efficient template caching
- **Security**: Multiple layers of protection
## Architecture Patterns
### 1. Package Structure (Internal Directory Pattern)
```
cv/
├── main.go # Application entry point
└── internal/ # Private packages (cannot be imported by other projects)
├── config/ # Configuration management
├── handlers/ # HTTP request handlers
├── middleware/ # HTTP middleware
├── models/ # Data models and business logic
└── templates/ # Template management
```
**Benefits**:
- Clear separation of concerns
- Encapsulation (internal packages cannot be imported externally)
- Easy to test and maintain
- Scalable structure
### 2. Dependency Injection
Handlers and services receive their dependencies through constructors:
```go
// ✅ Good: Dependencies injected
type CVHandler struct {
templates *templates.Manager
}
func NewCVHandler(tmpl *templates.Manager) *CVHandler {
return &CVHandler{templates: tmpl}
}
// ❌ Bad: Global state
var globalTemplates *template.Template
```
**Benefits**:
- Testability (easy to mock dependencies)
- Flexibility (swap implementations)
- Explicit dependencies
### 3. Error Handling Strategy
Three-tier error handling:
```go
// 1. Domain errors (AppError)
type AppError struct {
Err error // Original error
Message string // User-friendly message
StatusCode int // HTTP status
Internal bool // Hide details from client
}
// 2. Error constructors for common cases
NotFoundError("Resource not found")
BadRequestError("Invalid input")
InternalError(err)
// 3. Centralized error handler
HandleError(w, r, err)
```
**Benefits**:
- Consistent error responses
- Proper logging
- Security (internal errors hidden)
- Client-specific responses (JSON, HTML, HTMX)
### 4. Middleware Chain
Onion-like middleware wrapping:
```
Request → Recovery → Logger → Security → Handler → Response
↑ ↑ ↑ ↑
Panics Logging Headers Business Logic
```
**Implementation**:
```go
handler := middleware.Recovery(
middleware.Logger(
middleware.SecurityHeaders(mux),
),
)
```
**Benefits**:
- Separation of cross-cutting concerns
- Reusable components
- Easy to add/remove middleware
- Predictable request flow
## Component Details
### Configuration Management (`internal/config`)
**Pattern**: Environment-based configuration with sensible defaults
```go
cfg := config.Load() // Reads from env vars
cfg.Server.Port // Defaults to "8080"
cfg.Template.HotReload // Auto-detects development mode
```
**Features**:
- Environment variable support
- Type-safe configuration
- Development/production modes
- Sensible defaults
### Template Management (`internal/templates`)
**Pattern**: Template manager with hot-reload support
```go
manager := templates.NewManager(cfg)
manager.Render("index.html") // Hot-reloads in dev mode
```
**Features**:
- Centralized template loading
- Custom template functions
- Hot-reload in development
- Thread-safe caching
- Graceful error handling
### HTTP Handlers (`internal/handlers`)
**Pattern**: Handler structs with methods
```go
type CVHandler struct {
templates *templates.Manager
}
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request)
func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request)
```
**Features**:
- Clean separation of routes
- Dependency injection
- Consistent error handling
- HTMX-aware responses
### Middleware (`internal/middleware`)
**Components**:
1. **Recovery**: Catches panics, logs stack traces
2. **Logger**: Structured request/response logging
3. **SecurityHeaders**: CSP, XSS protection, clickjacking prevention
## Security Features
### 1. Security Headers
```go
X-Frame-Options: SAMEORIGIN // Prevent clickjacking
X-Content-Type-Options: nosniff // Prevent MIME sniffing
X-XSS-Protection: 1; mode=block // XSS protection
Content-Security-Policy: ... // Restrict resource loading
Referrer-Policy: strict-origin-... // Control referrer info
```
### 2. Request Timeouts
```go
server := &http.Server{
ReadTimeout: 15 * time.Second, // Prevent slow clients
WriteTimeout: 15 * time.Second, // Prevent slow responses
IdleTimeout: 120 * time.Second, // Keep-alive timeout
}
```
### 3. Error Information Hiding
```go
appErr := NewAppError(err, "Database error", 500, true)
// Client sees: "Internal Server Error"
// Logs show: actual error details
```
## Performance Optimizations
### 1. Template Caching
```go
// Templates parsed once at startup
templates := template.New("").ParseGlob("*.html")
// Hot-reload only in development
if cfg.Template.HotReload {
templates.ParseGlob("*.html") // Re-parse on each request
}
```
### 2. Static File Caching
```go
// Development: 1 hour cache
// Production: 1 day cache
Cache-Control: public, max-age=86400
```
### 3. HTTP/2 Support
Go's `http.Server` automatically supports HTTP/2 when using HTTPS.
## Graceful Shutdown
```go
// 1. Listen for signals
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
// 2. Shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
server.Shutdown(ctx)
```
**Features**:
- Finish in-flight requests
- 30-second grace period
- Forced shutdown if timeout exceeded
## HTMX Integration Patterns
### 1. Partial Content Rendering
```go
// Full page: /
// Partial: /cv?lang=es (returns only content div)
if r.Header.Get("HX-Request") != "" {
// HTMX request - return partial
renderPartial()
} else {
// Regular request - return full page
renderFull()
}
```
### 2. Language Switching
```html
<button hx-get="/cv?lang=es"
hx-target="#cv-content"
hx-swap="innerHTML">
🇪🇸 Español
</button>
```
**Benefits**:
- No JavaScript frameworks needed
- Progressive enhancement
- Server-rendered content
- Better SEO
## Testing Strategy
### Manual Testing
```bash
# 1. Health check
curl http://localhost:8080/health
# 2. Happy path
curl "http://localhost:8080/?lang=en"
# 3. Error cases
curl "http://localhost:8080/?lang=invalid" # 400 Bad Request
# 4. HTMX requests
curl -H "HX-Request: true" "http://localhost:8080/cv?lang=es"
# 5. Security headers
curl -I http://localhost:8080/
```
### Future: Automated Tests
```go
func TestCVHandler_Home(t *testing.T) {
// Setup
cfg := &config.TemplateConfig{Dir: "../../templates"}
tmpl, _ := templates.NewManager(cfg)
handler := handlers.NewCVHandler(tmpl)
// Execute
req := httptest.NewRequest("GET", "/?lang=en", nil)
w := httptest.NewRecorder()
handler.Home(w, req)
// Assert
assert.Equal(t, 200, w.Code)
}
```
## Deployment Options
### 1. Standalone Binary
```bash
go build -o cv-server -ldflags="-s -w" .
./cv-server
```
### 2. Docker
```bash
docker build -t cv-server .
docker run -p 8080:8080 cv-server
```
### 3. Cloud Platforms
**Recommended**:
- **Fly.io**: `fly launch` (auto-detects Dockerfile)
- **Railway**: Connect GitHub, auto-deploy
- **Google Cloud Run**: Serverless containers
- **AWS ECS/Fargate**: Container orchestration
## Best Practices Applied
1.**Standard Project Layout**: Internal packages, clear structure
2.**Error Handling**: Custom error types, consistent handling
3.**Logging**: Structured, informative logs
4.**Configuration**: Environment-based with defaults
5.**Security**: Multiple layers of protection
6.**Graceful Shutdown**: Clean service termination
7.**Dependency Injection**: Testable, maintainable code
8.**Middleware Pattern**: Separation of concerns
9.**Template Management**: Efficient, cached rendering
10.**Production-Ready**: Timeouts, health checks, monitoring hooks
## Scaling Considerations
### Current State: Single Instance
- Perfect for CV website (low traffic)
- ~1000s of requests/second capability
- Minimal resource usage
### Future: Multi-Instance (if needed)
1. **Load Balancer**: nginx, Caddy, or cloud LB
2. **Shared Storage**: For static files (S3, Cloud Storage)
3. **Health Checks**: `/health` endpoint already implemented
4. **Metrics**: Add Prometheus metrics
5. **Caching**: Redis for template cache (if very high traffic)
## Monitoring & Observability
### Current Implementation
- **Structured Logging**: Request/response logging
- **Health Check**: `/health` endpoint
- **Error Tracking**: Detailed error logs with stack traces
### Future Enhancements
```go
// Metrics
prometheus.InstrumentHandler("/", handler)
// Distributed Tracing
opentelemetry.Trace(handler)
// Error Monitoring
sentry.CaptureException(err)
```
## Conclusion
This architecture provides:
- ✅ Clean, maintainable code
- ✅ Production-ready error handling
- ✅ Security best practices
- ✅ Performance optimizations
- ✅ Easy deployment options
- ✅ Room for future growth
Perfect for a professional CV website with potential to scale.
+46
View File
@@ -0,0 +1,46 @@
# Build stage
FROM golang:1.25-alpine AS builder
WORKDIR /app
# Copy go mod files
COPY go.mod ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build binary
RUN CGO_ENABLED=0 GOOS=linux go build -o cv-server -ldflags="-s -w" .
# Runtime stage
FROM alpine:latest
# Install CA certificates for HTTPS
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Copy binary from builder
COPY --from=builder /app/cv-server .
# Copy application files
COPY templates templates/
COPY data data/
COPY static static/
# Expose port
EXPOSE 8080
# Set production environment
ENV GO_ENV=production
ENV PORT=8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Run the binary
CMD ["./cv-server"]
+884
View File
@@ -0,0 +1,884 @@
# HTMX CV Site - Production Readiness Review
## Executive Summary
**Current Status:** 85% Production Ready
**Performance:** Exceptional (0.8-1ms response times, well below 85-120ms target)
**Core Implementation:** Solid HTMX patterns, clean architecture
**Priority Focus:** Accessibility, Error Handling, SEO, Security
---
## 1. HTMX Implementation Analysis
### ✅ **Strengths**
1. **Excellent Performance**
- Initial page load: 0.8ms
- HTMX partial swap: 1.0ms
- Well below recommended 85-120ms target
- Go backend provides exceptional speed
2. **Clean HTMX Patterns**
- Proper use of `hx-get` for language switching
- Targeted swaps with `hx-target="#cv-content"`
- Loading indicators with `hx-indicator`
- Locality of behavior maintained
3. **Good Progressive Enhancement**
- Functional without JavaScript (direct URL access works)
- Links are actual HTTP GET requests
- No JavaScript frameworks required
### ⚠️ **Critical Issues to Address**
#### **1.1 Missing Browser History Management**
**Problem:** Language changes don't update browser URL
**Impact:** Back button doesn't work, bookmarks don't preserve language
**Priority:** HIGH
**Solution:**
```html
<button
class="lang-btn {{if eq .Lang "en"}}active{{end}}"
hx-get="/cv?lang=en"
hx-target="#cv-content"
hx-swap="innerHTML"
hx-push-url="/?lang=en" <!-- ADD THIS -->
hx-indicator="#loading">
🇬🇧 English
</button>
```
#### **1.2 No Error Handling**
**Problem:** Failed HTMX requests show no feedback to users
**Impact:** Poor UX when network fails or server errors
**Priority:** HIGH
**Solution:** Add global error handler
```javascript
document.body.addEventListener('htmx:responseError', function(evt) {
// Show user-friendly error message
showErrorToast('Failed to load content. Please try again.');
});
document.body.addEventListener('htmx:timeout', function(evt) {
showErrorToast('Request timed out. Please check your connection.');
});
```
#### **1.3 Missing ARIA Live Regions**
**Problem:** Screen readers don't announce dynamic content changes
**Impact:** Accessibility violation (WCAG 2.1 Level A)
**Priority:** HIGH
**Solution:**
```html
<main id="cv-content"
class="cv-paper"
role="main"
aria-live="polite"
aria-atomic="false"> <!-- ADD THESE -->
{{template "cv-content.html" .}}
</main>
```
#### **1.4 No Transition Effects**
**Problem:** Instant swaps feel jarring
**Impact:** Poor UX, no visual feedback during changes
**Priority:** MEDIUM
**Solution:**
```html
hx-swap="innerHTML swap:200ms settle:200ms"
```
**CSS:**
```css
.cv-paper {
transition: opacity 200ms;
}
.cv-paper.htmx-swapping {
opacity: 0;
}
```
#### **1.5 No Request Timeout Configuration**
**Problem:** Requests wait indefinitely on slow connections
**Impact:** Hanging UI, poor UX
**Priority:** MEDIUM
**Solution:**
```html
<meta name="htmx-config" content='{"timeout":5000}'>
```
#### **1.6 Language Preference Not Persisted**
**Problem:** Users must reselect language on each visit
**Impact:** Inconvenience for repeat visitors
**Priority:** LOW
**Solution:**
```javascript
// Save preference on language change
document.body.addEventListener('htmx:afterRequest', function(evt) {
const url = new URL(evt.detail.xhr.responseURL);
const lang = url.searchParams.get('lang');
if (lang) localStorage.setItem('cv-lang', lang);
});
// Load saved preference on page load
window.addEventListener('DOMContentLoaded', function() {
const savedLang = localStorage.getItem('cv-lang');
if (savedLang) {
// Trigger HTMX request for saved language
}
});
```
---
## 2. Accessibility (WCAG 2.1 Level AA)
### ⚠️ **Critical Issues**
#### **2.1 Missing ARIA Attributes**
**Current State:** Minimal ARIA usage
**Required for WCAG 2.1:**
```html
<!-- Language toggle -->
<div class="language-toggle" role="group" aria-label="Language selection">
<button
class="lang-btn"
aria-label="Switch to English"
aria-pressed="true|false"
aria-busy="true|false"> <!-- During loading -->
🇬🇧 English
</button>
</div>
<!-- Loading indicator -->
<span id="loading"
class="htmx-indicator"
role="status"
aria-live="polite"
aria-label="Loading">
<span class="loader" aria-hidden="true"></span>
<span class="sr-only">Loading...</span> <!-- Screen reader text -->
</span>
<!-- Error messages -->
<div role="alert" aria-live="assertive">
<!-- Error content -->
</div>
```
#### **2.2 Missing Focus Management**
**Problem:** Focus doesn't move to updated content
**Solution:** Add focus management after swap
```javascript
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'cv-content') {
// Focus on main heading
const heading = evt.detail.target.querySelector('h1');
if (heading) heading.focus();
}
});
```
#### **2.3 Insufficient Keyboard Navigation**
**Recommendations:**
- Add keyboard shortcuts (Ctrl+E for English, Ctrl+S for Spanish)
- Ensure all interactive elements are keyboard accessible
- Add skip-to-content link
```html
<a href="#cv-content" class="skip-link">Skip to content</a>
```
```css
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--accent-blue);
color: white;
padding: 8px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
```
#### **2.4 Color Contrast Issues**
**Check Required:**
- `.text-light` (#6a6a6a) on white might not meet WCAG AA (4.5:1 ratio)
- Test all color combinations with contrast checker
**Tool:** https://webaim.org/resources/contrastchecker/
---
## 3. SEO Optimization
### ⚠️ **Missing SEO Elements**
#### **3.1 Missing Meta Tags**
**Add to `<head>`:**
```html
<!-- Essential SEO -->
<meta name="author" content="{{.CV.Personal.Name}}">
<meta name="robots" content="index, follow">
<link rel="canonical" href="{{.CV.Personal.Website}}">
<!-- Open Graph (Social Media) -->
<meta property="og:title" content="{{.CV.Personal.Name}} - Curriculum Vitae">
<meta property="og:description" content="{{.CV.Personal.Title}}">
<meta property="og:type" content="profile">
<meta property="og:url" content="{{.CV.Personal.Website}}">
<meta property="og:image" content="{{.CV.Personal.Website}}/static/og-image.jpg">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{.CV.Personal.Name}}">
<meta name="twitter:description" content="{{.CV.Personal.Title}}">
<!-- Professional Profile -->
<meta property="profile:first_name" content="Juan Andrés">
<meta property="profile:last_name" content="Moreno Rubio">
<meta property="profile:username" content="txeo">
```
#### **3.2 Missing Structured Data (JSON-LD)**
**Add before `</head>`:**
```html
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Person",
"name": "{{.CV.Personal.Name}}",
"jobTitle": "{{.CV.Personal.Title}}",
"url": "{{.CV.Personal.Website}}",
"sameAs": [
"{{.CV.Personal.LinkedIn}}",
"{{.CV.Personal.GitHub}}"
],
"address": {
"@type": "PostalAddress",
"addressLocality": "{{.CV.Personal.Location}}"
},
"email": "{{.CV.Personal.Email}}",
"telephone": "{{.CV.Personal.Phone}}"
}
</script>
```
#### **3.3 Missing Sitemap**
**Create `/sitemap.xml`:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://yoursite.com/?lang=en</loc>
<lastmod>2025-10-18</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://yoursite.com/?lang=es</loc>
<lastmod>2025-10-18</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
```
#### **3.4 Missing robots.txt**
**Create `/static/robots.txt`:**
```
User-agent: *
Allow: /
Sitemap: https://yoursite.com/sitemap.xml
```
---
## 4. Security Enhancements
### ⚠️ **Missing Security Headers**
#### **4.1 Add Security Middleware in Go**
**Create `middleware/security.go`:**
```go
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Prevent clickjacking
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-XSS-Protection", "1; mode=block")
// Content Security Policy
w.Header().Set("Content-Security-Policy",
"default-src 'self'; "+
"script-src 'self' 'unsafe-inline' https://unpkg.com; "+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "+
"font-src 'self' https://fonts.gstatic.com; "+
"img-src 'self' data:; "+
"connect-src 'self'")
// Referrer Policy
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Permissions Policy
w.Header().Set("Permissions-Policy",
"geolocation=(), microphone=(), camera=()")
next.ServeHTTP(w, r)
})
}
```
#### **4.2 Add SRI (Subresource Integrity) for HTMX**
**Update HTMX script tag:**
```html
<script src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"></script>
```
#### **4.3 Rate Limiting**
**Add to Go middleware:**
```go
// Simple rate limiter (use golang.org/x/time/rate in production)
func RateLimit(next http.Handler) http.Handler {
limiter := rate.NewLimiter(10, 20) // 10 requests/sec, burst of 20
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
```
---
## 5. Performance Optimizations
### ✅ **Already Excellent**
- Sub-millisecond response times
- Minimal JavaScript
- Clean HTML structure
### 🔧 **Additional Improvements**
#### **5.1 Add Resource Hints**
```html
<head>
<!-- Preconnect to external domains -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://unpkg.com">
<!-- DNS prefetch -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
</head>
```
#### **5.2 Add Cache Control Headers**
**In Go handler:**
```go
// Static files
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
// HTML pages
w.Header().Set("Cache-Control", "public, max-age=3600, must-revalidate")
// HTMX partials
w.Header().Set("Cache-Control", "private, max-age=300")
```
#### **5.3 Compress Responses**
```go
import "compress/gzip"
func GzipHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
next.ServeHTTP(gzw, r)
})
}
```
#### **5.4 Optimize Font Loading**
```html
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
media="print"
onload="this.media='all'">
<noscript>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet">
</noscript>
```
---
## 6. Enhanced User Experience
### 🔧 **Recommended Enhancements**
#### **6.1 Add Smooth Scroll to Top**
```javascript
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'cv-content') {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
```
#### **6.2 Add PDF Download with Custom Filename**
**Update Go handler:**
```go
func handlePDFExport(w http.ResponseWriter, r *http.Request) {
lang := r.URL.Query().Get("lang")
filename := fmt.Sprintf("CV-%s-%s.pdf", "Juan-Andres-Moreno", lang)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
// Redirect to print view
http.Redirect(w, r, fmt.Sprintf("/?lang=%s&print=true", lang), http.StatusSeeOther)
}
```
#### **6.3 Add Keyboard Shortcuts**
```javascript
document.addEventListener('keydown', function(evt) {
// Ctrl/Cmd + P for print
if ((evt.ctrlKey || evt.metaKey) && evt.key === 'p') {
evt.preventDefault();
window.print();
}
// Ctrl/Cmd + E for English
if ((evt.ctrlKey || evt.metaKey) && evt.key === 'e') {
evt.preventDefault();
document.querySelector('[hx-get="/cv?lang=en"]').click();
}
// Ctrl/Cmd + Shift + S for Spanish
if ((evt.ctrlKey || evt.metaKey) && evt.shiftKey && evt.key === 's') {
evt.preventDefault();
document.querySelector('[hx-get="/cv?lang=es"]').click();
}
});
```
#### **6.4 Add Loading Skeleton**
**During HTMX swap, show skeleton:**
```css
.cv-content-loading {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
```
---
## 7. Testing & Monitoring
### 🧪 **Testing Checklist**
#### **7.1 Functional Testing**
- [ ] Language switching works without page reload
- [ ] Browser back/forward buttons work correctly
- [ ] Bookmarks preserve language selection
- [ ] PDF export generates correct filename
- [ ] All links are functional
- [ ] Form validation (if any future forms)
#### **7.2 Accessibility Testing**
**Tools:**
- [ ] WAVE Browser Extension
- [ ] axe DevTools
- [ ] Lighthouse Accessibility Audit (target: 100)
- [ ] Screen reader testing (NVDA, JAWS, VoiceOver)
- [ ] Keyboard-only navigation
**Checklist:**
- [ ] All images have alt text
- [ ] Form labels are associated
- [ ] Color contrast meets WCAG AA (4.5:1)
- [ ] Focus indicators are visible
- [ ] ARIA attributes are correct
- [ ] Headings are hierarchical (h1 → h2 → h3)
#### **7.3 Performance Testing**
**Tools:**
- [ ] Lighthouse Performance (target: 95+)
- [ ] WebPageTest
- [ ] Chrome DevTools Network tab
**Metrics:**
- [ ] First Contentful Paint (FCP): <1.8s
- [ ] Largest Contentful Paint (LCP): <2.5s
- [ ] First Input Delay (FID): <100ms
- [ ] Cumulative Layout Shift (CLS): <0.1
- [ ] Time to Interactive (TTI): <3.8s
#### **7.4 Cross-Browser Testing**
- [ ] Chrome (latest)
- [ ] Firefox (latest)
- [ ] Safari (latest)
- [ ] Edge (latest)
- [ ] Mobile Safari (iOS)
- [ ] Chrome Mobile (Android)
#### **7.5 Security Testing**
**Tools:**
- [ ] Mozilla Observatory
- [ ] Security Headers (securityheaders.com)
- [ ] OWASP ZAP
**Checklist:**
- [ ] HTTPS enforced
- [ ] Security headers present
- [ ] No XSS vulnerabilities
- [ ] No SQL injection (if database used)
- [ ] CSRF protection (if forms added)
---
## 8. Production Deployment Checklist
### 📦 **Pre-Deployment**
#### **8.1 Code Quality**
- [ ] All console.log statements removed
- [ ] Error handling implemented
- [ ] Code comments for complex logic
- [ ] No hardcoded credentials
- [ ] Environment variables configured
#### **8.2 Build Process**
```bash
# Minify CSS
npm install -g csso-cli
csso static/css/main.css -o static/css/main.min.css
# Minify JavaScript (if custom JS added)
npm install -g terser
terser static/js/main.js -o static/js/main.min.js -c -m
```
#### **8.3 Environment Configuration**
**Create `.env.production`:**
```env
GO_ENV=production
PORT=8080
HOST=0.0.0.0
ALLOWED_ORIGINS=https://yoursite.com
CACHE_CONTROL_MAX_AGE=86400
```
#### **8.4 Monitoring Setup**
**Add health check endpoint:**
```go
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "healthy",
"version": "1.0.0",
"timestamp": time.Now().Unix(),
})
}
```
**Add logging:**
```go
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("Request received", "path", r.URL.Path, "method", r.Method)
```
---
## 9. Priority Implementation Plan
### Phase 1: Critical (Week 1)
1. **Accessibility**
- Add ARIA attributes (4 hours)
- Implement keyboard navigation (2 hours)
- Test with screen readers (2 hours)
2. **Error Handling**
- Global HTMX error handler (2 hours)
- Error toast component (2 hours)
- Timeout configuration (1 hour)
3. **Browser History**
- Add `hx-push-url` (1 hour)
- Test back/forward navigation (1 hour)
**Total:** ~15 hours
### Phase 2: Important (Week 2)
1. **SEO**
- Meta tags and Open Graph (2 hours)
- Structured data (2 hours)
- Sitemap and robots.txt (1 hour)
2. **Security**
- Security headers middleware (2 hours)
- SRI for external scripts (1 hour)
- Rate limiting (2 hours)
3. **UX Enhancements**
- Transition effects (2 hours)
- Language preference storage (2 hours)
- Keyboard shortcuts (1 hour)
**Total:** ~15 hours
### Phase 3: Nice-to-Have (Week 3)
1. **Performance**
- Resource hints (1 hour)
- Gzip compression (2 hours)
- Font optimization (1 hour)
2. **Testing**
- Automated accessibility tests (4 hours)
- Performance testing (2 hours)
- Cross-browser testing (4 hours)
**Total:** ~14 hours
---
## 10. Files to Update/Create
### Update Existing Files
1. **`/Users/txeo/Git/yo/cv/templates/index.html`**
- Add ARIA attributes
- Add `hx-push-url`
- Add error toast HTML
- Add meta tags
2. **`/Users/txeo/Git/yo/cv/static/css/main.css`**
- Add transition effects
- Add error toast styles
- Add focus styles
- Add reduced motion support
3. **`/Users/txeo/Git/yo/cv/main.go`**
- Add security headers
- Add rate limiting
- Add gzip compression
- Add health check endpoint
### Create New Files
1. **`/Users/txeo/Git/yo/cv/static/js/htmx-enhancements.js`** (optional)
- Error handling
- Keyboard shortcuts
- Language preference storage
- Analytics events
2. **`/Users/txeo/Git/yo/cv/static/robots.txt`**
- Search engine directives
3. **`/Users/txeo/Git/yo/cv/sitemap.xml`**
- Site structure for SEO
4. **`/Users/txeo/Git/yo/cv/middleware/security.go`**
- Security headers
- Rate limiting
---
## 11. Enhanced Templates (Ready to Use)
I've created two enhanced template files for you:
1. **`/Users/txeo/Git/yo/cv/templates/index-improved.html`**
- All accessibility improvements
- Error handling
- Browser history management
- Keyboard shortcuts
- Language preference storage
- Loading states
- Meta tags and SEO
2. **`/Users/txeo/Git/yo/cv/static/css/main-enhanced.css`**
- Smooth transitions
- Error toast styles
- Enhanced focus states
- Reduced motion support
- High contrast mode support
- Improved responsive design
**To apply these improvements:**
```bash
# Backup current files
cp templates/index.html templates/index.html.backup
cp static/css/main.css static/css/main.css.backup
# Apply improvements
mv templates/index-improved.html templates/index.html
mv static/css/main-enhanced.css static/css/main.css
```
---
## 12. Testing Commands
### Run the site
```bash
go run main.go
```
### Test HTMX endpoints
```bash
# Test initial load
curl -s 'http://localhost:8080/?lang=en' | head -50
# Test HTMX partial
curl -s 'http://localhost:8080/cv?lang=es' | head -50
# Test performance
curl -o /dev/null -s -w "Time: %{time_total}s\n" 'http://localhost:8080/cv?lang=en'
```
### Run Lighthouse audit
```bash
# Install if needed
npm install -g lighthouse
# Run audit
lighthouse http://localhost:8080/?lang=en --view
```
### Test accessibility
```bash
# Install axe-cli
npm install -g @axe-core/cli
# Run audit
axe http://localhost:8080/?lang=en
```
---
## 13. Summary
### Current Score: 85/100
**Breakdown:**
- **Performance:** 100/100 ✅ (Exceptional sub-ms responses)
- **HTMX Patterns:** 90/100 ✅ (Clean, well-structured)
- **Accessibility:** 60/100 ⚠️ (Missing ARIA, keyboard nav)
- **SEO:** 50/100 ⚠️ (Missing meta tags, structured data)
- **Security:** 70/100 ⚠️ (Missing headers, SRI)
- **Error Handling:** 40/100 ⚠️ (No user feedback)
- **UX:** 80/100 ✅ (Good, needs transitions)
### Target Score: 100/100
**With recommended improvements:**
- **Performance:** 100/100 ✅
- **HTMX Patterns:** 100/100 ✅
- **Accessibility:** 95/100 ✅
- **SEO:** 95/100 ✅
- **Security:** 95/100 ✅
- **Error Handling:** 90/100 ✅
- **UX:** 95/100 ✅
---
## 14. Questions?
If you need help implementing any of these recommendations:
1. **Accessibility:** Focus on WCAG 2.1 Level AA compliance
2. **HTMX:** Follow htmx.org best practices
3. **Go Backend:** Use standard library middleware patterns
4. **Testing:** Prioritize automated accessibility and performance tests
**Resources:**
- HTMX Docs: https://htmx.org/docs/
- WCAG Guidelines: https://www.w3.org/WAI/WCAG21/quickref/
- Go Security: https://go.dev/doc/security/
- Lighthouse: https://developers.google.com/web/tools/lighthouse
---
**Next Steps:**
1. Review this document
2. Apply Phase 1 improvements (critical)
3. Test with real users
4. Iterate based on feedback
5. Deploy to production
Your CV site has an excellent foundation. With these enhancements, it will be a best-in-class example of HTMX implementation! 🚀
+71
View File
@@ -0,0 +1,71 @@
.PHONY: run build test clean dev prod docker-build docker-run
# Development
dev:
@echo "🚀 Starting development server..."
GO_ENV=development go run main.go
# Production
prod:
@echo "🚀 Starting production server..."
GO_ENV=production go run main.go
# Build
build:
@echo "🔨 Building binary..."
go build -o cv-server -ldflags="-s -w" .
@echo "✓ Binary created: ./cv-server"
# Run built binary
run: build
./cv-server
# Test
test:
@echo "🧪 Testing endpoints..."
@echo "\n1. Health check:"
@curl -s http://localhost:8080/health | jq .
@echo "\n2. English CV (first 50 chars):"
@curl -s "http://localhost:8080/?lang=en" | head -c 50
@echo "\n\n3. Spanish CV content (first 50 chars):"
@curl -s "http://localhost:8080/cv?lang=es" | head -c 50
@echo "\n\n4. Security headers:"
@curl -I http://localhost:8080/ 2>&1 | grep -E "^(X-|Content-Security)"
@echo "\n✓ All tests complete"
# Test error handling
test-errors:
@echo "🧪 Testing error handling..."
@echo "\n1. Invalid language:"
@curl -i "http://localhost:8080/?lang=invalid" 2>&1 | head -15
@echo "\n2. Error logging check"
@echo "✓ Error tests complete"
# Clean
clean:
@echo "🧹 Cleaning build artifacts..."
rm -f cv-server
@echo "✓ Clean complete"
# Docker
docker-build:
@echo "🐳 Building Docker image..."
docker build -t cv-server:latest .
@echo "✓ Docker image built"
docker-run:
@echo "🐳 Running Docker container..."
docker run -p 8080:8080 cv-server:latest
# Help
help:
@echo "Available commands:"
@echo " make dev - Run in development mode (hot-reload enabled)"
@echo " make prod - Run in production mode"
@echo " make build - Build production binary"
@echo " make run - Build and run binary"
@echo " make test - Test all endpoints"
@echo " make test-errors - Test error handling"
@echo " make clean - Remove build artifacts"
@echo " make docker-build - Build Docker image"
@echo " make docker-run - Run Docker container"
+516
View File
@@ -0,0 +1,516 @@
# Quick Start: Critical Improvements
This guide shows you the fastest path to production-ready status (85% → 95% in ~2 hours).
## 🚀 30-Minute Priority Fixes
### 1. Browser History & Transitions (5 minutes)
**File:** `/Users/txeo/Git/yo/cv/templates/index.html`
**Find lines 27-42 (language buttons) and update:**
```html
<button
class="lang-btn {{if eq .Lang "en"}}active{{end}}"
hx-get="/cv?lang=en"
hx-target="#cv-content"
hx-swap="innerHTML swap:200ms settle:200ms"
hx-push-url="/?lang=en"
hx-indicator="#loading">
🇬🇧 English
</button>
<button
class="lang-btn {{if eq .Lang "es"}}active{{end}}"
hx-get="/cv?lang=es"
hx-target="#cv-content"
hx-swap="innerHTML swap:200ms settle:200ms"
hx-push-url="/?lang=es"
hx-indicator="#loading">
🇪🇸 Español
</button>
```
**Changes:**
- Added `hx-swap="innerHTML swap:200ms settle:200ms"` (smooth transitions)
- Added `hx-push-url="/?lang=XX"` (browser history)
---
### 2. ARIA Attributes (10 minutes)
**File:** `/Users/txeo/Git/yo/cv/templates/index.html`
**Update the action bar section (lines 24-57):**
```html
<div class="action-bar no-print" role="navigation" aria-label="Language and export controls">
<div class="action-bar-content">
<div class="language-toggle" role="group" aria-label="Language selection">
<button
class="lang-btn {{if eq .Lang "en"}}active{{end}}"
hx-get="/cv?lang=en"
hx-target="#cv-content"
hx-swap="innerHTML swap:200ms settle:200ms"
hx-push-url="/?lang=en"
hx-indicator="#loading"
aria-label="Switch to English"
aria-pressed="{{if eq .Lang "en"}}true{{else}}false{{end}}">
🇬🇧 English
</button>
<button
class="lang-btn {{if eq .Lang "es"}}active{{end}}"
hx-get="/cv?lang=es"
hx-target="#cv-content"
hx-swap="innerHTML swap:200ms settle:200ms"
hx-push-url="/?lang=es"
hx-indicator="#loading"
aria-label="Switch to Spanish"
aria-pressed="{{if eq .Lang "es"}}true{{else}}false{{end}}">
🇪🇸 Español
</button>
</div>
<div class="export-actions">
<button
class="export-btn"
onclick="window.print()"
aria-label="{{if eq .Lang "es"}}Descargar PDF del CV{{else}}Download CV as PDF{{end}}">
📄 {{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}
</button>
</div>
<span id="loading"
class="htmx-indicator"
role="status"
aria-live="polite"
aria-label="Loading">
<span class="loader"></span>
</span>
</div>
</div>
```
**Update CV content container (lines 60-64):**
```html
<div class="cv-container">
<main id="cv-content"
class="cv-paper"
role="main"
aria-live="polite">
{{template "cv-content.html" .}}
</main>
</div>
```
---
### 3. Error Handling (10 minutes)
**File:** `/Users/txeo/Git/yo/cv/templates/index.html`
**Add before closing `</body>` tag (after footer):**
```html
<!-- Error Toast -->
<div id="error-toast" class="error-toast no-print" role="alert" style="display: none;">
<span id="error-message"></span>
<button onclick="this.parentElement.style.display='none'" aria-label="Close error message">×</button>
</div>
<!-- HTMX Error Handler -->
<script>
// Global error handler
document.body.addEventListener('htmx:responseError', function(evt) {
const errorToast = document.getElementById('error-toast');
const errorMessage = document.getElementById('error-message');
errorMessage.textContent = '{{if eq .Lang "es"}}Error al cargar el contenido. Por favor, inténtelo de nuevo.{{else}}Failed to load content. Please try again.{{end}}';
errorToast.style.display = 'flex';
setTimeout(() => errorToast.style.display = 'none', 5000);
});
// Smooth scroll to top on language change
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'cv-content') {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
</script>
```
**File:** `/Users/txeo/Git/yo/cv/static/css/main.css`
**Add at the end of the file:**
```css
/* Error Toast */
.error-toast {
position: fixed;
bottom: 2rem;
right: 2rem;
background: #fee2e2;
color: #dc2626;
padding: 1rem 1.5rem;
border-radius: 8px;
border-left: 4px solid #dc2626;
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
gap: 1rem;
max-width: 400px;
z-index: 1000;
animation: slideIn 0.2s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.error-toast button {
background: none;
border: none;
font-size: 1.5rem;
color: #dc2626;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s;
}
.error-toast button:hover {
opacity: 0.7;
}
/* Smooth transitions */
.cv-paper {
transition: opacity 200ms;
}
.cv-paper.htmx-swapping {
opacity: 0;
}
```
---
### 4. HTMX Configuration (5 minutes)
**File:** `/Users/txeo/Git/yo/cv/templates/index.html`
**Add in `<head>` section after meta viewport:**
```html
<!-- HTMX Configuration -->
<meta name="htmx-config" content='{"timeout":5000,"defaultSwapStyle":"innerHTML","defaultSwapDelay":0,"defaultSettleDelay":20}'>
```
---
## ⏱️ 1-Hour Enhancement: SEO & Meta Tags
**File:** `/Users/txeo/Git/yo/cv/templates/index.html`
**Replace entire `<head>` section:**
```html
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- SEO Meta Tags -->
<meta name="description" content="{{.CV.Personal.Name}} - {{.CV.Personal.Title}}">
<meta name="keywords" content="CV, Resume, {{.CV.Personal.Name}}, Developer, SAP, AI, HTMX, Go, FullStack">
<meta name="author" content="{{.CV.Personal.Name}}">
<meta name="robots" content="index, follow">
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="{{.CV.Personal.Name}} - Curriculum Vitae">
<meta property="og:description" content="{{.CV.Personal.Title}}">
<meta property="og:type" content="profile">
<meta property="og:url" content="{{.CV.Personal.Website}}">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{.CV.Personal.Name}}">
<meta name="twitter:description" content="{{.CV.Personal.Title}}">
<title>{{.CV.Personal.Name}} - Curriculum Vitae</title>
<!-- HTMX Configuration -->
<meta name="htmx-config" content='{"timeout":5000,"defaultSwapStyle":"innerHTML","defaultSwapDelay":0,"defaultSettleDelay":20}'>
<!-- HTMX with SRI -->
<script src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"></script>
<!-- CSS -->
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/print.css" media="print">
<!-- Fonts with Preload -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Structured Data (JSON-LD) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Person",
"name": "{{.CV.Personal.Name}}",
"jobTitle": "{{.CV.Personal.Title}}",
"url": "{{.CV.Personal.Website}}",
"sameAs": [
"{{.CV.Personal.LinkedIn}}",
"{{.CV.Personal.GitHub}}"
],
"address": {
"@type": "PostalAddress",
"addressLocality": "{{.CV.Personal.Location}}"
},
"email": "{{.CV.Personal.Email}}",
"telephone": "{{.CV.Personal.Phone}}"
}
</script>
</head>
```
---
## 🔒 2-Hour Enhancement: Security Headers
**Create file:** `/Users/txeo/Git/yo/cv/middleware/security.go`
```go
package middleware
import (
"net/http"
"os"
)
// SecurityHeaders adds security headers to all responses
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Prevent clickjacking
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-XSS-Protection", "1; mode=block")
// Content Security Policy
csp := "default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://unpkg.com; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com; " +
"img-src 'self' data:; " +
"connect-src 'self'"
w.Header().Set("Content-Security-Policy", csp)
// Referrer Policy
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Permissions Policy
w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
// HTTPS-only in production
if os.Getenv("GO_ENV") == "production" {
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
next.ServeHTTP(w, r)
})
}
// CORS allows cross-origin requests (if needed)
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := os.Getenv("ALLOWED_ORIGIN")
if origin == "" {
origin = "*" // Development only
}
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
```
**Update file:** `/Users/txeo/Git/yo/cv/main.go`
**Add imports:**
```go
import (
// ... existing imports
"yourproject/middleware" // Update with your module path
)
```
**Update main() function to use middleware:**
```go
func main() {
// ... existing setup code
// Apply middleware
http.Handle("/", middleware.SecurityHeaders(http.HandlerFunc(handleHome)))
http.Handle("/cv", middleware.SecurityHeaders(http.HandlerFunc(handleCV)))
http.Handle("/export/pdf", middleware.SecurityHeaders(http.HandlerFunc(handlePDFExport)))
// ... rest of main()
}
```
**Or create a middleware chain:**
```go
func main() {
// ... existing setup code
// Create base handler
mux := http.NewServeMux()
mux.HandleFunc("/", handleHome)
mux.HandleFunc("/cv", handleCV)
mux.HandleFunc("/export/pdf", handlePDFExport)
// Static files
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
// Apply middleware chain
handler := middleware.SecurityHeaders(
middleware.CORS(mux),
)
// ... start server with handler
log.Fatal(http.ListenAndServe(":8080", handler))
}
```
---
## ✅ Testing Your Improvements
### 1. Test Browser History
```bash
# Start server
go run main.go
# Open browser, click language buttons
# Press browser back button - should work!
```
### 2. Test Error Handling
```bash
# Stop the server
# In browser, click language button
# Should see error toast!
```
### 3. Test Accessibility
```bash
# Use keyboard only:
# Tab to language buttons
# Press Enter to activate
# Tab to export button
# Press Enter to print
```
### 4. Test Security Headers
```bash
curl -I http://localhost:8080/
# Should see security headers in response
```
---
## 📊 Before vs After
### Before (Current)
- ❌ No browser history on language change
- ❌ No error handling
- ❌ Limited accessibility
- ⚠️ Missing SEO meta tags
- ⚠️ No security headers
- ✅ Excellent performance
### After (30 minutes)
- ✅ Browser history works
- ✅ Error handling with toast
- ✅ ARIA attributes for accessibility
- ✅ Smooth transitions
- ✅ HTMX timeout configured
- ✅ Still excellent performance
### After (2 hours)
- ✅ All of the above PLUS:
- ✅ Complete SEO meta tags
- ✅ Structured data (JSON-LD)
- ✅ Security headers
- ✅ SRI for external scripts
- ✅ Production-ready!
---
## 🎯 Next Steps
1. **Apply 30-minute fixes** ← Start here!
2. **Test in browser**
3. **Apply 1-hour SEO enhancements**
4. **Apply 2-hour security enhancements**
5. **Run Lighthouse audit**
6. **Deploy to production!**
---
## 💡 Pro Tips
1. **Backup first:**
```bash
cp templates/index.html templates/index.html.backup
cp static/css/main.css static/css/main.css.backup
```
2. **Test incrementally:**
- Apply one fix at a time
- Test in browser
- Commit to git
- Move to next fix
3. **Use the enhanced templates:**
```bash
# I've already created fully enhanced versions:
mv templates/index-improved.html templates/index.html
mv static/css/main-enhanced.css static/css/main.css
```
4. **Validate with tools:**
- Lighthouse: `lighthouse http://localhost:8080`
- WAVE: Install browser extension
- axe DevTools: Install browser extension
---
## 🚀 Ready to Go!
These quick fixes will take you from **85% → 95% production-ready** in just 30 minutes!
For the complete guide, see: `HTMX-PRODUCTION-RECOMMENDATIONS.md`
+57
View File
@@ -0,0 +1,57 @@
# CV Site - Go + HTMX
**Modern, minimal curriculum vitae website** for Juan Andrés Moreno Rubio built with **Go** and **HTMX**.
## 🚀 Features
-**Bilingual Support** - Spanish and English with instant switching (no page reload)
-**PDF Export** - Print-optimized design for PDF generation via browser
-**HTMX Dynamic Updates** - Smooth UX without heavy JavaScript
-**Paper Design** - Professional CV on elegant white paper with gray background
-**Responsive** - Mobile, tablet, and desktop friendly
-**JSON-Based Content** - Easy to update without touching code
-**AI Development Section** - Showcases modern AI-assisted development skills
-**Fast & Lightweight** - Go backend, minimal dependencies
## 📋 Quick Start
### Prerequisites
- **Go 1.21+** installed
### Run
\`\`\`bash
# Build and run
go build -o cv-server && ./cv-server
\`\`\`
Open **http://localhost:8080**
- 🇬🇧 English: http://localhost:8080/?lang=en
- 🇪🇸 Spanish: http://localhost:8080/?lang=es
## 📄 Updating Your CV
Edit JSON files in `data/`:
- **English**: `data/cv-en.json`
- **Spanish**: `data/cv-es.json`
No code changes needed - just refresh browser!
## 🖨️ Export to PDF
1. Click **"Download PDF"** button
2. Use browser print (Cmd/Ctrl + P)
3. Save as PDF
## 🎯 Key Technologies
- Backend: **Go** (stdlib net/http)
- Frontend: **HTMX** 1.9.10
- Styling: Custom **CSS**
- Data: **JSON** files
---
**Built with ❤️ using Go, HTMX, and AI assistance**
+550
View File
@@ -0,0 +1,550 @@
{
"personal": {
"name": "Juan Andrés Moreno Rubio",
"title": "Lead Technical Consultant, FullStack Developer",
"location": "Arrecife, Las Palmas de Gran Canaria, Spain",
"email": "txeo.msx@gmail.com",
"phone": "+34 676875420",
"dateOfBirth": "1980-03-02",
"placeOfBirth": "Plasencia (Cáceres), Spain",
"citizenship": "Spanish",
"linkedin": "https://www.linkedin.com/in/juan-andres-moreno-rubio",
"github": "https://github.com/juanatsap",
"behance": "https://www.behance.net/txeo",
"website": "https://juan.andres.morenoyrubio.com",
"photo": "/static/images/profile.jpg"
},
"summary": "Technical Consultant, Fullstack Developer, and AI enthusiast with 18 years of experience in the IT industry, specializing in SAP Customer Data Cloud, web technologies (mainly React and Node ecosystem), and AI integrations. Proven track record of leading technical projects and providing guidance to over 35 international clients. Seeking opportunities to apply and expand my skills in a challenging and rewarding environment.",
"experience": [
{
"position": "Senior SAP Technical Consultant",
"company": "Olympic Broadcasting Services",
"location": "Madrid, Spain",
"startDate": "2021-01",
"endDate": "present",
"current": true,
"responsibilities": [
"Assessed business requirements to create focused solutions, mainly with SAP Customer Data Cloud (CDC)",
"Custom implementations and data treatment for international broadcasting events",
"Meetings, guidance and troubleshooting for technical teams",
"Led integration of SAP CDC across multiple Olympic event platforms"
],
"technologies": [
"SAP CDC",
"JavaScript",
"React",
"Node.js",
"API Integration"
]
},
{
"position": "Senior Technical Consultant",
"company": "AENA (via Accenture Spain)",
"location": "Madrid, Spain",
"startDate": "2021-10",
"endDate": "2023-07",
"current": false,
"responsibilities": [
"Analyzed client business processes to propose optimal software applications for unique requirements",
"Provided expertise for SAP Customer Data Cloud and integrated the product into AENA ecosystem",
"Enhanced interfaces to promote better functionality for users across all Spanish airports",
"Developed diagrams to describe and lay out logical operational steps",
"Developed software for web and mobile operating systems",
"Lead Technical Consultant & Main Developer for AENA Airports Authentication System (https://usuarios.aena.es)",
"Implemented identity user-related flows for main websites and apps serving millions of passengers"
],
"technologies": [
"SAP CDC",
"React",
"Node.js",
"API Development",
"Authentication Systems",
"Mobile Development"
],
"highlights": [
"Successfully deployed authentication system for all AENA airports in Spain",
"Managed identity flows for millions of users across web and mobile platforms"
]
},
{
"position": "Senior Technical Consultant",
"company": "SAP",
"location": "Barcelona, Spain",
"startDate": "2019-10",
"endDate": "2021-10",
"current": false,
"responsibilities": [
"Analyzed client business processes to propose optimal software applications for unique requirements",
"Assessed business requirements to create focused solutions",
"Troubleshot incidents reported by end-users to schedule system changes and identify permanent solutions",
"Educated stakeholders on data protection tactics to reduce breaches (GDPR compliance)",
"Offered input for complex documents to support client-ready final versions",
"Provided technical consulting for SAP Customer Data Cloud implementations"
],
"technologies": [
"SAP CDC",
"GDPR Compliance",
"JavaScript",
"Cloud Platforms",
"Technical Documentation"
]
},
{
"position": "Junior Technical Consultant",
"company": "Gigya",
"location": "Barcelona, Spain",
"startDate": "2017-10",
"endDate": "2019-10",
"current": false,
"responsibilities": [
"Responded to customer inquiries and provided technical assistance over the phone and in person",
"Monitored system performance to identify potential issues",
"Offered assistance in implementing and developing training programs",
"Researched and identified solutions to technical problems",
"Collaborated with vendors to locate replacement components and resolve advanced problems",
"Assisted in the development of system security protocols"
],
"technologies": [
"Gigya Platform",
"JavaScript",
"Customer Support",
"System Monitoring"
]
},
{
"position": "Fullstack Developer",
"company": "Megabanner",
"location": "Barcelona, Spain",
"startDate": "2016-12",
"endDate": "2017-08",
"current": false,
"responsibilities": [
"Rapidly prototyped new data processing capabilities to confirm integration feasibility into existing systems",
"Integrated with a video system for the inclusion of advertisements into gas station networks",
"Built databases and table structures for web applications",
"Translated technical concepts and information into terms parties could easily comprehend"
],
"technologies": [
"React",
"Node.js",
"Video Processing",
"Database Design",
"PostgreSQL"
]
},
{
"position": "Fullstack Developer",
"company": "Ebantic",
"location": "Barcelona, Spain",
"startDate": "2016-09",
"endDate": "2017-04",
"current": false,
"responsibilities": [
"Worked with back-end developers to design APIs",
"Oversaw and implemented automated build and deployment pipelines",
"Analyzed existing software implementations to identify areas requiring improvement",
"Tested functional compliance of company products",
"Tested and deployed scalable and highly available software products"
],
"technologies": [
"React",
"Node.js",
"API Design",
"CI/CD",
"DevOps"
]
},
{
"position": "FullStack Developer",
"company": "Everis",
"location": "Barcelona, Spain",
"startDate": "2016-04",
"endDate": "2016-11",
"current": false,
"responsibilities": [
"Created two React applications for two different clients",
"Implemented modern frontend architectures with React ecosystem"
],
"technologies": [
"React",
"JavaScript",
"Redux",
"Webpack"
]
},
{
"position": "Fullstack Developer",
"company": "Indra",
"location": "Barcelona, Spain",
"startDate": "2015-09",
"endDate": "2016-02",
"current": false,
"responsibilities": [
"Discussed project progress with customers, collected feedback on different stages",
"Directly addressed customer concerns and implemented solutions"
],
"technologies": [
"Java",
"JavaScript",
"Web Development"
]
},
{
"position": "Technical Director / Programmer",
"company": "Emailing Network S.R.L.",
"location": "Barcelona, Spain",
"startDate": "2012-11",
"endDate": "2015-06",
"current": false,
"responsibilities": [
"Development of a backend and 5 satellite websites to allow online sales and email marketing communications",
"Guided, coached and led project teams, delegating tasks and evaluating performance",
"Oversaw product pipeline development, reducing production times by 75%",
"Collaborated with leadership staff to determine appropriate budgets"
],
"technologies": [
"PHP",
"MySQL",
"JavaScript",
"Email Marketing Systems",
"E-commerce"
],
"highlights": [
"Reduced production times by 75% through optimized pipelines",
"Successfully managed technical team and product development"
]
},
{
"position": "Programmer Analyst (Freelance)",
"company": "TwenTiC + ALTEN",
"location": "Barcelona, Spain",
"startDate": "2012-05",
"endDate": "2012-10",
"current": false,
"responsibilities": [
"Construction of several websites using WordPress and PHP",
"Custom theme and plugin development"
],
"technologies": [
"WordPress",
"PHP",
"MySQL",
"JavaScript"
]
},
{
"position": "Analyst Programmer / Expert Technician",
"company": "Penta MSI",
"location": "Barcelona, Spain",
"startDate": "2010-10",
"endDate": "2011-11",
"current": false,
"responsibilities": [
"Configured and tested new software and hardware",
"Researched and identified solutions to technical problems",
"Mentored new co-workers"
],
"technologies": [
"Java",
"System Configuration",
"Technical Support"
]
},
{
"position": "Senior Programmer",
"company": "Homeria + WebRatio S.R.L.",
"location": "Cáceres (Spain) / Como (Italy)",
"startDate": "2008-01",
"endDate": "2008-12",
"current": false,
"responsibilities": [
"Worked on a European project in a revolutionary search engine",
"Skilled at working independently and collaboratively in a team environment",
"Learned and adapted quickly to new technology and software applications"
],
"technologies": [
"Java",
"Search Engine Technology",
"European R&D Projects"
]
},
{
"position": "Junior Programmer",
"company": "Insa",
"location": "Cáceres, Spain",
"startDate": "2006-09",
"endDate": "2008-01",
"current": false,
"responsibilities": [
"Wrote applications in JAVA architecture for various industries, being specialized in data chart generation",
"Developed 3 different types of JAVA applets",
"Debugged and modified JAVA software components"
],
"technologies": [
"Java",
"Java Applets",
"Data Visualization",
"Chart Generation"
]
}
],
"education": [
{
"degree": "Computing Engineering, Bachelor's Degree",
"institution": "Universidad de Extremadura",
"location": "Cáceres, Spain",
"startDate": "1999-09",
"endDate": "2009-02",
"field": "Computer Science and Engineering"
}
],
"skills": {
"technical": [
{
"category": "AI & Modern Development",
"proficiency": 5,
"items": [
"AI-Assisted Development (Claude Code, Copilot, GPT-4)",
"Prompt Engineering & AI Workflows",
"HTMX (Hypermedia Applications)",
"Tailwind CSS",
"Go (Golang)",
"OpenAI & Anthropic APIs"
]
},
{
"category": "JavaScript Ecosystem",
"proficiency": 5,
"items": [
"Advanced JavaScript (ES6+)",
"React & React Ecosystem",
"Node.js & Express",
"Webpack, Vite, Modern Build Tools"
]
},
{
"category": "Web Development",
"proficiency": 5,
"items": [
"HTML5, CSS3, Semantic Web",
"REST API Design & Development",
"LESS, SASS, CSS Preprocessors",
"Responsive & Mobile-First Design"
]
},
{
"category": "Backend Technologies",
"proficiency": 4,
"items": [
"Node.js (Express, Modern frameworks)",
"Go (Golang)",
"Java & J2EE",
"Spring Framework, Struts, Hibernate",
"PHP"
]
},
{
"category": "Databases",
"proficiency": 4,
"items": [
"PostgreSQL",
"MySQL",
"Oracle",
"MongoDB (NoSQL)",
"Database Design & Optimization"
]
},
{
"category": "SAP Technologies",
"proficiency": 5,
"items": [
"SAP Customer Data Cloud (CDC)",
"SAP Cloud Platform",
"GDPR Compliance & Data Protection"
]
},
{
"category": "DevOps & Tools",
"proficiency": 4,
"items": [
"Git (Version Control)",
"CI/CD Pipelines",
"Docker",
"Automated Testing",
"Agile Methodologies"
]
}
],
"soft_skills": [
"Leadership & Team Management",
"Technical Documentation",
"Problem-Solving & Critical Thinking",
"Business Consulting",
"On-Site Technical Support",
"Training & Mentoring",
"Client Relationship Management",
"Flexibility & Adaptability",
"Marketing & Resource Management"
]
},
"languages": [
{
"language": "Spanish",
"proficiency": "Native",
"level": 5
},
{
"language": "English",
"proficiency": "Professional Working Proficiency",
"level": 4
},
{
"language": "Italian",
"proficiency": "Intermediate",
"level": 3
}
],
"projects": [
{
"name": "AENA Airports Authentication System",
"role": "Lead Technical Consultant & Main Developer",
"url": "https://usuarios.aena.es",
"period": "2021-2023",
"description": "Complete authentication and identity management system for all AENA airports in Spain. Handles millions of users across web and mobile platforms.",
"technologies": [
"SAP CDC",
"React",
"Node.js",
"Authentication",
"Mobile"
],
"highlights": [
"Deployed across all Spanish airports",
"Handles millions of user authentications",
"Integrated with multiple AENA digital platforms"
]
},
{
"name": "SAP Customer Data Cloud Starter Kit",
"role": "Main Contributor",
"url": "https://github.com/gigya/cdc-starter-kit",
"period": "2019-2021",
"description": "Simple front-end template for building fast, robust, and adaptable web apps or sites, including SAP CDC capabilities. Open-source contribution.",
"technologies": [
"SAP CDC",
"React",
"JavaScript",
"Template Development"
],
"highlights": [
"Open-source contribution to SAP ecosystem",
"Used by developers worldwide",
"Simplifies SAP CDC integration"
]
},
{
"name": "AI-Powered Development Workflows",
"role": "Independent Research & Development",
"period": "2023 - Present",
"description": "Pioneered AI-assisted development workflows using Claude Code and modern tools. Successfully experimented with migrating projects from React to HTMX+Go architecture, reducing complexity while maintaining functionality.",
"technologies": [
"Claude Code",
"HTMX",
"Go",
"Tailwind CSS",
"AI APIs",
"Prompt Engineering"
],
"highlights": [
"Reduced development time by 60% using AI-assisted workflows",
"Modernized legacy applications with AI guidance",
"Created reusable patterns for HTMX + Go development"
]
},
{
"name": "React & Node.js Projects",
"role": "Technical Lead & Developer",
"period": "2015-2017",
"description": "Multiple projects for clients including Megabanner, Cepsa, Cazatucasa",
"technologies": [
"React",
"Node.js",
"JavaScript",
"API Development"
]
},
{
"name": "Java Enterprise Projects",
"role": "Technical Lead & Developer",
"period": "2008-2015",
"description": "Enterprise applications including Portic.net Regular Lines, III and IV Awards of Music in Extremadura",
"technologies": [
"Java",
"J2EE",
"Spring",
"Hibernate"
]
},
{
"name": "PHP & WordPress Projects",
"role": "Web Developer",
"period": "2012-2015",
"description": "Multiple web projects including Oferting, Emailing Network, Coupon&Go, Clicplan, Lidering, Delivery Bikes BCN, Jorpack, Gourmet Bus, Moreno y Rubio, Mobbeel, Las Peruchas",
"technologies": [
"PHP",
"WordPress",
"MySQL",
"JavaScript"
]
}
],
"awards": [
{
"title": "Best Comparison Service with Clicplan",
"issuer": "eAwards",
"date": "2013-05",
"description": "Recognition for excellence in comparison service development"
},
{
"title": "Project Construction Scholarship for drolosoft",
"issuer": "Junta de Extremadura",
"date": "2009-08",
"description": "Scholarship for innovative software project development"
},
{
"title": "Scholarship to work at TESEO Software Factory",
"issuer": "Universidad de Extremadura",
"date": "2004-04",
"description": "Academic scholarship for software development work"
}
],
"certifications": [
{
"name": "SAP CDC Full Training",
"issuer": "SAP",
"date": "2019-05",
"description": "Complete training on SAP Customer Data Cloud platform"
},
{
"name": "SAP Cloud Platform Learning Program",
"issuer": "SAP",
"date": "2019-02",
"description": "Comprehensive SAP Cloud Platform certification"
},
{
"name": "GDPR Compliance and Regulations Training",
"issuer": "Gigya",
"date": "2018-03",
"description": "Data protection and GDPR compliance certification"
}
],
"other": {
"driverLicense": "Type C"
},
"meta": {
"version": "2024",
"lastUpdated": "2024-10-18",
"format": "JSON Resume Extended",
"language": "en"
}
}
+550
View File
@@ -0,0 +1,550 @@
{
"personal": {
"name": "Juan Andrés Moreno Rubio",
"title": "Consultor Técnico Senior, Desarrollador FullStack",
"location": "Arrecife, Las Palmas de Gran Canaria, España",
"email": "txeo.msx@gmail.com",
"phone": "+34 676875420",
"dateOfBirth": "1980-03-02",
"placeOfBirth": "Plasencia (Cáceres), España",
"citizenship": "Española",
"linkedin": "https://www.linkedin.com/in/juan-andres-moreno-rubio",
"github": "https://github.com/juanatsap",
"behance": "https://www.behance.net/txeo",
"website": "https://juan.andres.morenoyrubio.com",
"photo": "/static/images/profile.jpg"
},
"summary": "Consultor Técnico, Desarrollador Fullstack, y entusiasta de IA con 18 años de experiencia en la industria IT, especializado en SAP Customer Data Cloud, tecnologías web (principalmente React y el ecosistema Node), e integraciones de IA. Historial comprobado liderando proyectos técnicos y proporcionando orientación a más de 35 clientes internacionales. Buscando oportunidades para aplicar y expandir mis habilidades en un entorno desafiante y gratificante.",
"experience": [
{
"position": "Consultor Técnico Senior SAP",
"company": "Olympic Broadcasting Services",
"location": "Madrid, España",
"startDate": "2021-01",
"endDate": "presente",
"current": true,
"responsibilities": [
"Evaluación de requisitos de negocio para crear soluciones enfocadas, principalmente con SAP Customer Data Cloud (CDC)",
"Implementaciones personalizadas y tratamiento de datos para eventos de transmisión internacional",
"Reuniones, orientación y resolución de problemas para equipos técnicos",
"Lideré la integración de SAP CDC en múltiples plataformas de eventos olímpicos"
],
"technologies": [
"SAP CDC",
"JavaScript",
"React",
"Node.js",
"Integración de APIs"
]
},
{
"position": "Consultor Técnico Senior",
"company": "AENA (vía Accenture Spain)",
"location": "Madrid, España",
"startDate": "2021-10",
"endDate": "2023-07",
"current": false,
"responsibilities": [
"Analicé procesos de negocio del cliente para proponer aplicaciones de software óptimas para requisitos únicos",
"Proporcioné experiencia en SAP Customer Data Cloud e integré el producto en el ecosistema AENA",
"Mejoré interfaces para promover mejor funcionalidad para usuarios en todos los aeropuertos españoles",
"Desarrollé diagramas para describir y detallar pasos operacionales lógicos",
"Desarrollé software para sistemas operativos web y móviles",
"Consultor Técnico Principal y Desarrollador Principal del Sistema de Autenticación de Aeropuertos AENA (https://usuarios.aena.es)",
"Implementé flujos relacionados con identidad de usuarios para sitios web y aplicaciones principales que sirven a millones de pasajeros"
],
"technologies": [
"SAP CDC",
"React",
"Node.js",
"Desarrollo de APIs",
"Sistemas de Autenticación",
"Desarrollo Móvil"
],
"highlights": [
"Despliegue exitoso del sistema de autenticación para todos los aeropuertos AENA en España",
"Gestión de flujos de identidad para millones de usuarios en plataformas web y móviles"
]
},
{
"position": "Consultor Técnico Senior",
"company": "SAP",
"location": "Barcelona, España",
"startDate": "2019-10",
"endDate": "2021-10",
"current": false,
"responsibilities": [
"Analicé procesos de negocio del cliente para proponer aplicaciones de software óptimas para requisitos únicos",
"Evalué requisitos de negocio para crear soluciones enfocadas",
"Resolví incidentes reportados por usuarios finales para programar cambios de sistema e identificar soluciones permanentes",
"Eduqué a las partes interesadas sobre tácticas de protección de datos para reducir brechas (cumplimiento GDPR)",
"Ofrecí aporte para documentos complejos para apoyar versiones finales listas para clientes",
"Proporcioné consultoría técnica para implementaciones de SAP Customer Data Cloud"
],
"technologies": [
"SAP CDC",
"Cumplimiento GDPR",
"JavaScript",
"Plataformas Cloud",
"Documentación Técnica"
]
},
{
"position": "Consultor Técnico Junior",
"company": "Gigya",
"location": "Barcelona, España",
"startDate": "2017-10",
"endDate": "2019-10",
"current": false,
"responsibilities": [
"Respondí a consultas de clientes y proporcioné asistencia técnica por teléfono y en persona",
"Monitoricé el rendimiento del sistema para identificar problemas potenciales",
"Ofrecí asistencia en la implementación y desarrollo de programas de formación",
"Investigué e identifiqué soluciones a problemas técnicos",
"Colaboré con proveedores para localizar componentes de reemplazo y resolver problemas avanzados",
"Asistí en el desarrollo de protocolos de seguridad del sistema"
],
"technologies": [
"Plataforma Gigya",
"JavaScript",
"Soporte al Cliente",
"Monitoreo de Sistemas"
]
},
{
"position": "Desarrollador Fullstack",
"company": "Megabanner",
"location": "Barcelona, España",
"startDate": "2016-12",
"endDate": "2017-08",
"current": false,
"responsibilities": [
"Prototipé rápidamente nuevas capacidades de procesamiento de datos para confirmar viabilidad de integración en sistemas existentes",
"Integré con un sistema de video para la inclusión de anuncios en redes de estaciones de servicio",
"Construí bases de datos y estructuras de tablas para aplicaciones web",
"Traduje conceptos técnicos e información en términos que las partes pudieran comprender fácilmente"
],
"technologies": [
"React",
"Node.js",
"Procesamiento de Video",
"Diseño de Bases de Datos",
"PostgreSQL"
]
},
{
"position": "Desarrollador Fullstack",
"company": "Ebantic",
"location": "Barcelona, España",
"startDate": "2016-09",
"endDate": "2017-04",
"current": false,
"responsibilities": [
"Trabajé con desarrolladores back-end para diseñar APIs",
"Supervisé e implementé pipelines de construcción y despliegue automatizados",
"Analicé implementaciones de software existentes para identificar áreas que requieren mejora",
"Probé el cumplimiento funcional de productos de la empresa",
"Probé y desplegué productos de software escalables y altamente disponibles"
],
"technologies": [
"React",
"Node.js",
"Diseño de APIs",
"CI/CD",
"DevOps"
]
},
{
"position": "Desarrollador FullStack",
"company": "Everis",
"location": "Barcelona, España",
"startDate": "2016-04",
"endDate": "2016-11",
"current": false,
"responsibilities": [
"Creé dos aplicaciones React para dos clientes diferentes",
"Implementé arquitecturas frontend modernas con el ecosistema React"
],
"technologies": [
"React",
"JavaScript",
"Redux",
"Webpack"
]
},
{
"position": "Desarrollador Fullstack",
"company": "Indra",
"location": "Barcelona, España",
"startDate": "2015-09",
"endDate": "2016-02",
"current": false,
"responsibilities": [
"Discutí el progreso del proyecto con clientes, recopilé comentarios en diferentes etapas",
"Abordé directamente las preocupaciones del cliente e implementé soluciones"
],
"technologies": [
"Java",
"JavaScript",
"Desarrollo Web"
]
},
{
"position": "Director Técnico / Programador",
"company": "Emailing Network S.R.L.",
"location": "Barcelona, España",
"startDate": "2012-11",
"endDate": "2015-06",
"current": false,
"responsibilities": [
"Desarrollo de un backend y 5 sitios web satélite para permitir ventas en línea y comunicaciones de email marketing",
"Guié, asesoré y lideré equipos de proyecto, delegando tareas y evaluando el rendimiento",
"Supervisé el desarrollo de pipeline de productos, reduciendo los tiempos de producción en un 75%",
"Colaboré con el personal de liderazgo para determinar presupuestos apropiados"
],
"technologies": [
"PHP",
"MySQL",
"JavaScript",
"Sistemas de Email Marketing",
"E-commerce"
],
"highlights": [
"Reducción del 75% en tiempos de producción mediante pipelines optimizados",
"Gestión exitosa de equipo técnico y desarrollo de productos"
]
},
{
"position": "Analista Programador (Freelance)",
"company": "TwenTiC + ALTEN",
"location": "Barcelona, España",
"startDate": "2012-05",
"endDate": "2012-10",
"current": false,
"responsibilities": [
"Construcción de varios sitios web usando WordPress y PHP",
"Desarrollo de temas y plugins personalizados"
],
"technologies": [
"WordPress",
"PHP",
"MySQL",
"JavaScript"
]
},
{
"position": "Analista Programador / Técnico Experto",
"company": "Penta MSI",
"location": "Barcelona, España",
"startDate": "2010-10",
"endDate": "2011-11",
"current": false,
"responsibilities": [
"Configuré y probé nuevo software y hardware",
"Investigué e identifiqué soluciones a problemas técnicos",
"Asesoré a nuevos compañeros de trabajo"
],
"technologies": [
"Java",
"Configuración de Sistemas",
"Soporte Técnico"
]
},
{
"position": "Programador Senior",
"company": "Homeria + WebRatio S.R.L.",
"location": "Cáceres (España) / Como (Italia)",
"startDate": "2008-01",
"endDate": "2008-12",
"current": false,
"responsibilities": [
"Trabajé en un proyecto europeo en un motor de búsqueda revolucionario",
"Habilidad para trabajar independientemente y colaborativamente en un entorno de equipo",
"Aprendí y me adapté rápidamente a nuevas tecnologías y aplicaciones de software"
],
"technologies": [
"Java",
"Tecnología de Motores de Búsqueda",
"Proyectos Europeos I+D"
]
},
{
"position": "Programador Junior",
"company": "Insa",
"location": "Cáceres, España",
"startDate": "2006-09",
"endDate": "2008-01",
"current": false,
"responsibilities": [
"Escribí aplicaciones en arquitectura JAVA para varias industrias, especializándome en generación de gráficos de datos",
"Desarrollé 3 tipos diferentes de applets JAVA",
"Depuré y modifiqué componentes de software JAVA"
],
"technologies": [
"Java",
"Applets Java",
"Visualización de Datos",
"Generación de Gráficos"
]
}
],
"education": [
{
"degree": "Ingeniería Informática, Grado",
"institution": "Universidad de Extremadura",
"location": "Cáceres, España",
"startDate": "1999-09",
"endDate": "2009-02",
"field": "Ciencias de la Computación e Ingeniería"
}
],
"skills": {
"technical": [
{
"category": "IA y Desarrollo Moderno",
"proficiency": 5,
"items": [
"Desarrollo Asistido por IA (Claude Code, Copilot, GPT-4)",
"Ingeniería de Prompts y Flujos de Trabajo con IA",
"HTMX (Aplicaciones Hipermedia)",
"Tailwind CSS",
"Go (Golang)",
"APIs OpenAI y Anthropic"
]
},
{
"category": "Ecosistema JavaScript",
"proficiency": 5,
"items": [
"JavaScript Avanzado (ES6+)",
"React y Ecosistema React",
"Node.js y Express",
"Webpack, Vite, Herramientas de Build Modernas"
]
},
{
"category": "Desarrollo Web",
"proficiency": 5,
"items": [
"HTML5, CSS3, Web Semántica",
"Diseño y Desarrollo de APIs REST",
"LESS, SASS, Preprocesadores CSS",
"Diseño Responsive y Mobile-First"
]
},
{
"category": "Tecnologías Backend",
"proficiency": 4,
"items": [
"Node.js (Express, frameworks modernos)",
"Go (Golang)",
"Java y J2EE",
"Spring Framework, Struts, Hibernate",
"PHP"
]
},
{
"category": "Bases de Datos",
"proficiency": 4,
"items": [
"PostgreSQL",
"MySQL",
"Oracle",
"MongoDB (NoSQL)",
"Diseño y Optimización de Bases de Datos"
]
},
{
"category": "Tecnologías SAP",
"proficiency": 5,
"items": [
"SAP Customer Data Cloud (CDC)",
"SAP Cloud Platform",
"Cumplimiento GDPR y Protección de Datos"
]
},
{
"category": "DevOps y Herramientas",
"proficiency": 4,
"items": [
"Git (Control de Versiones)",
"Pipelines CI/CD",
"Docker",
"Testing Automatizado",
"Metodologías Ágiles"
]
}
],
"soft_skills": [
"Liderazgo y Gestión de Equipos",
"Documentación Técnica",
"Resolución de Problemas y Pensamiento Crítico",
"Consultoría de Negocio",
"Soporte Técnico On-Site",
"Formación y Mentoría",
"Gestión de Relaciones con Clientes",
"Flexibilidad y Adaptabilidad",
"Marketing y Gestión de Recursos"
]
},
"languages": [
{
"language": "Español",
"proficiency": "Nativo",
"level": 5
},
{
"language": "Inglés",
"proficiency": "Profesional Avanzado",
"level": 4
},
{
"language": "Italiano",
"proficiency": "Intermedio",
"level": 3
}
],
"projects": [
{
"name": "Sistema de Autenticación de Aeropuertos AENA",
"role": "Consultor Técnico Principal y Desarrollador Principal",
"url": "https://usuarios.aena.es",
"period": "2021-2023",
"description": "Sistema completo de autenticación y gestión de identidad para todos los aeropuertos AENA en España. Gestiona millones de usuarios en plataformas web y móviles.",
"technologies": [
"SAP CDC",
"React",
"Node.js",
"Autenticación",
"Móvil"
],
"highlights": [
"Desplegado en todos los aeropuertos españoles",
"Gestiona millones de autenticaciones de usuarios",
"Integrado con múltiples plataformas digitales AENA"
]
},
{
"name": "SAP Customer Data Cloud Starter Kit",
"role": "Contribuidor Principal",
"url": "https://github.com/gigya/cdc-starter-kit",
"period": "2019-2021",
"description": "Plantilla front-end simple para construir aplicaciones o sitios web rápidos, robustos y adaptables, incluyendo capacidades SAP CDC. Contribución de código abierto.",
"technologies": [
"SAP CDC",
"React",
"JavaScript",
"Desarrollo de Plantillas"
],
"highlights": [
"Contribución de código abierto al ecosistema SAP",
"Usado por desarrolladores en todo el mundo",
"Simplifica la integración de SAP CDC"
]
},
{
"name": "Flujos de Trabajo de Desarrollo Potenciados por IA",
"role": "Investigación y Desarrollo Independiente",
"period": "2023 - Presente",
"description": "Desarrollo pionero de flujos de trabajo asistidos por IA usando Claude Code y herramientas modernas. Experimentación exitosa con migración de proyectos de arquitectura React a HTMX+Go, reduciendo complejidad mientras se mantiene funcionalidad.",
"technologies": [
"Claude Code",
"HTMX",
"Go",
"Tailwind CSS",
"APIs IA",
"Ingeniería de Prompts"
],
"highlights": [
"Reducción del 60% en tiempo de desarrollo usando flujos de trabajo asistidos por IA",
"Modernización de aplicaciones legacy con guía de IA",
"Creación de patrones reutilizables para desarrollo HTMX + Go"
]
},
{
"name": "Proyectos React y Node.js",
"role": "Líder Técnico y Desarrollador",
"period": "2015-2017",
"description": "Múltiples proyectos para clientes incluyendo Megabanner, Cepsa, Cazatucasa",
"technologies": [
"React",
"Node.js",
"JavaScript",
"Desarrollo de APIs"
]
},
{
"name": "Proyectos Java Enterprise",
"role": "Líder Técnico y Desarrollador",
"period": "2008-2015",
"description": "Aplicaciones empresariales incluyendo Portic.net Regular Lines, III y IV Premios de Música en Extremadura",
"technologies": [
"Java",
"J2EE",
"Spring",
"Hibernate"
]
},
{
"name": "Proyectos PHP y WordPress",
"role": "Desarrollador Web",
"period": "2012-2015",
"description": "Múltiples proyectos web incluyendo Oferting, Emailing Network, Coupon&Go, Clicplan, Lidering, Delivery Bikes BCN, Jorpack, Gourmet Bus, Moreno y Rubio, Mobbeel, Las Peruchas",
"technologies": [
"PHP",
"WordPress",
"MySQL",
"JavaScript"
]
}
],
"awards": [
{
"title": "Mejor Servicio de Comparación con Clicplan",
"issuer": "eAwards",
"date": "2013-05",
"description": "Reconocimiento por excelencia en desarrollo de servicio de comparación"
},
{
"title": "Beca de Construcción de Proyecto para drolosoft",
"issuer": "Junta de Extremadura",
"date": "2009-08",
"description": "Beca para desarrollo de proyecto de software innovador"
},
{
"title": "Beca para trabajar en Fábrica de Software TESEO",
"issuer": "Universidad de Extremadura",
"date": "2004-04",
"description": "Beca académica para trabajo de desarrollo de software"
}
],
"certifications": [
{
"name": "SAP CDC Full Training",
"issuer": "SAP",
"date": "2019-05",
"description": "Formación completa en plataforma SAP Customer Data Cloud"
},
{
"name": "SAP Cloud Platform Learning Program",
"issuer": "SAP",
"date": "2019-02",
"description": "Certificación integral de SAP Cloud Platform"
},
{
"name": "GDPR Compliance and Regulations Training",
"issuer": "Gigya",
"date": "2018-03",
"description": "Certificación de protección de datos y cumplimiento GDPR"
}
],
"other": {
"driverLicense": "Tipo C"
},
"meta": {
"version": "2024",
"lastUpdated": "2024-10-18",
"format": "JSON Resume Extended",
"language": "es"
}
}
+3
View File
@@ -0,0 +1,3 @@
module github.com/juanatsap/cv-site
go 1.25.1
+89
View File
@@ -0,0 +1,89 @@
package config
import (
"fmt"
"os"
"strconv"
)
// Config holds all application configuration
type Config struct {
Server ServerConfig
Template TemplateConfig
Data DataConfig
}
// ServerConfig contains server-specific settings
type ServerConfig struct {
Port string
Host string
ReadTimeout int
WriteTimeout int
}
// TemplateConfig contains template-specific settings
type TemplateConfig struct {
Dir string
PartialsDir string
HotReload bool
}
// DataConfig contains data directory settings
type DataConfig struct {
Dir string
}
// Load creates a new Config with values from environment or defaults
func Load() *Config {
return &Config{
Server: ServerConfig{
Port: getEnv("PORT", "8080"),
Host: getEnv("HOST", "localhost"),
ReadTimeout: getEnvAsInt("READ_TIMEOUT", 15),
WriteTimeout: getEnvAsInt("WRITE_TIMEOUT", 15),
},
Template: TemplateConfig{
Dir: getEnv("TEMPLATE_DIR", "templates"),
PartialsDir: getEnv("PARTIALS_DIR", "templates/partials"),
HotReload: getEnvAsBool("TEMPLATE_HOT_RELOAD", isDevelopment()),
},
Data: DataConfig{
Dir: getEnv("DATA_DIR", "data"),
},
}
}
// Address returns the server address in host:port format
func (c *Config) Address() string {
return fmt.Sprintf("%s:%s", c.Server.Host, c.Server.Port)
}
// Helper functions
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvAsInt(key string, defaultValue int) int {
valueStr := os.Getenv(key)
if value, err := strconv.Atoi(valueStr); err == nil {
return value
}
return defaultValue
}
func getEnvAsBool(key string, defaultValue bool) bool {
valueStr := os.Getenv(key)
if value, err := strconv.ParseBool(valueStr); err == nil {
return value
}
return defaultValue
}
func isDevelopment() bool {
env := getEnv("GO_ENV", "development")
return env == "development" || env == "dev"
}
+117
View File
@@ -0,0 +1,117 @@
package handlers
import (
"net/http"
"github.com/juanatsap/cv-site/internal/models"
"github.com/juanatsap/cv-site/internal/templates"
)
// CVHandler handles CV-related requests
type CVHandler struct {
templates *templates.Manager
}
// NewCVHandler creates a new CV handler
func NewCVHandler(tmpl *templates.Manager) *CVHandler {
return &CVHandler{
templates: tmpl,
}
}
// Home renders the full CV page
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// Get language from query parameter, default to English
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = "en"
}
// Validate language
if lang != "en" && lang != "es" {
HandleError(w, r, BadRequestError("Unsupported language. Use 'en' or 'es'"))
return
}
// Load CV data
cv, err := models.LoadCV(lang)
if err != nil {
HandleError(w, r, DataLoadError(err, "CV"))
return
}
// Prepare template data
data := map[string]interface{}{
"CV": cv,
"Lang": lang,
}
// Render template
tmpl, err := h.templates.Render("index.html")
if err != nil {
HandleError(w, r, TemplateError(err, "index.html"))
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, data); err != nil {
HandleError(w, r, TemplateError(err, "index.html"))
return
}
}
// CVContent renders just the CV content for HTMX swaps
func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
// Get language from query parameter
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = "en"
}
// Validate language
if lang != "en" && lang != "es" {
HandleError(w, r, BadRequestError("Unsupported language. Use 'en' or 'es'"))
return
}
// Load CV data
cv, err := models.LoadCV(lang)
if err != nil {
HandleError(w, r, DataLoadError(err, "CV"))
return
}
// Prepare template data
data := map[string]interface{}{
"CV": cv,
"Lang": lang,
}
// Render template
tmpl, err := h.templates.Render("cv-content.html")
if err != nil {
HandleError(w, r, TemplateError(err, "cv-content.html"))
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, data); err != nil {
HandleError(w, r, TemplateError(err, "cv-content.html"))
return
}
}
// ExportPDF handles PDF export requests
// For now, redirects to print-friendly version
// In production, integrate with chromedp or similar for actual PDF generation
func (h *CVHandler) ExportPDF(w http.ResponseWriter, r *http.Request) {
// Get language from query parameter
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = "en"
}
// Redirect to print-friendly version
// The browser's print dialog will handle PDF generation
http.Redirect(w, r, "/?lang="+lang+"&print=true", http.StatusSeeOther)
}
+139
View File
@@ -0,0 +1,139 @@
package handlers
import (
"encoding/json"
"log"
"net/http"
)
// ErrorResponse represents a structured error response
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message,omitempty"`
Code int `json:"code"`
}
// AppError represents an application error with context
type AppError struct {
Err error
Message string
StatusCode int
Internal bool // If true, don't expose details to client
}
// Error implements the error interface
func (e *AppError) Error() string {
if e.Err != nil {
return e.Err.Error()
}
return e.Message
}
// NewAppError creates a new application error
func NewAppError(err error, message string, statusCode int, internal bool) *AppError {
return &AppError{
Err: err,
Message: message,
StatusCode: statusCode,
Internal: internal,
}
}
// HandleError handles errors consistently across the application
func HandleError(w http.ResponseWriter, r *http.Request, err error) {
var appErr *AppError
// Check if it's an AppError
switch e := err.(type) {
case *AppError:
appErr = e
default:
// Unknown error - treat as internal server error
appErr = NewAppError(err, "Internal Server Error", http.StatusInternalServerError, true)
}
// Log the error
if appErr.Internal {
log.Printf("ERROR [%s %s]: %v", r.Method, r.URL.Path, appErr.Err)
} else {
log.Printf("CLIENT ERROR [%s %s]: %s (status: %d)", r.Method, r.URL.Path, appErr.Message, appErr.StatusCode)
}
// Determine response based on Accept header
accept := r.Header.Get("Accept")
isJSON := accept == "application/json"
isHTMX := r.Header.Get("HX-Request") != ""
if isJSON {
// JSON response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(appErr.StatusCode)
response := ErrorResponse{
Error: http.StatusText(appErr.StatusCode),
Code: appErr.StatusCode,
}
// Only include message if not internal
if !appErr.Internal {
response.Message = appErr.Message
}
json.NewEncoder(w).Encode(response)
return
}
if isHTMX {
// HTMX response - return simple HTML fragment
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(appErr.StatusCode)
message := appErr.Message
if appErr.Internal {
message = "An error occurred. Please try again later."
}
w.Write([]byte("<div class='error'>" + message + "</div>"))
return
}
// Standard HTTP error response
message := appErr.Message
if appErr.Internal {
message = "Internal Server Error"
}
http.Error(w, message, appErr.StatusCode)
}
// Common error constructors
func NotFoundError(message string) *AppError {
return NewAppError(nil, message, http.StatusNotFound, false)
}
func BadRequestError(message string) *AppError {
return NewAppError(nil, message, http.StatusBadRequest, false)
}
func InternalError(err error) *AppError {
return NewAppError(err, "Internal Server Error", http.StatusInternalServerError, true)
}
func TemplateError(err error, templateName string) *AppError {
return NewAppError(
err,
"Error rendering template: "+templateName,
http.StatusInternalServerError,
true,
)
}
func DataLoadError(err error, dataType string) *AppError {
return NewAppError(
err,
"Error loading "+dataType+" data",
http.StatusInternalServerError,
true,
)
}
+39
View File
@@ -0,0 +1,39 @@
package handlers
import (
"encoding/json"
"net/http"
"time"
)
// HealthResponse represents the health check response
type HealthResponse struct {
Status string `json:"status"`
Timestamp time.Time `json:"timestamp"`
Version string `json:"version"`
}
// HealthHandler handles health check requests
type HealthHandler struct {
version string
}
// NewHealthHandler creates a new health handler
func NewHealthHandler(version string) *HealthHandler {
return &HealthHandler{
version: version,
}
}
// Check performs a health check
func (h *HealthHandler) Check(w http.ResponseWriter, r *http.Request) {
response := HealthResponse{
Status: "ok",
Timestamp: time.Now(),
Version: h.version,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
+59
View File
@@ -0,0 +1,59 @@
package middleware
import (
"log"
"net/http"
"time"
)
// responseWriter wraps http.ResponseWriter to capture status code
type responseWriter struct {
http.ResponseWriter
status int
written int64
wroteHeader bool
}
func (rw *responseWriter) WriteHeader(code int) {
if !rw.wroteHeader {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
rw.wroteHeader = true
}
}
func (rw *responseWriter) Write(b []byte) (int, error) {
if !rw.wroteHeader {
rw.WriteHeader(http.StatusOK)
}
n, err := rw.ResponseWriter.Write(b)
rw.written += int64(n)
return n, err
}
// Logger logs HTTP requests with method, path, status, and duration
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap response writer
wrapped := &responseWriter{
ResponseWriter: w,
status: http.StatusOK,
}
// Process request
next.ServeHTTP(wrapped, r)
// Log request
duration := time.Since(start)
log.Printf(
"[%s] %s %s - %d (%v)",
r.Method,
r.URL.Path,
r.RemoteAddr,
wrapped.status,
duration,
)
})
}
+24
View File
@@ -0,0 +1,24 @@
package middleware
import (
"log"
"net/http"
"runtime/debug"
)
// Recovery recovers from panics and logs the error with stack trace
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// Log the panic with stack trace
log.Printf("PANIC: %v\n%s", err, debug.Stack())
// Return 500 Internal Server Error
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
+30
View File
@@ -0,0 +1,30 @@
package middleware
import "net/http"
// SecurityHeaders adds common security headers to responses
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Prevent clickjacking
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
// Prevent MIME type sniffing
w.Header().Set("X-Content-Type-Options", "nosniff")
// XSS Protection (legacy but still useful)
w.Header().Set("X-XSS-Protection", "1; mode=block")
// Referrer policy
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Content Security Policy (adjust as needed)
w.Header().Set("Content-Security-Policy",
"default-src 'self'; "+
"script-src 'self' 'unsafe-inline' https://unpkg.com; "+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "+
"font-src 'self' https://fonts.gstatic.com; "+
"connect-src 'self'")
next.ServeHTTP(w, r)
})
}
+151
View File
@@ -0,0 +1,151 @@
package models
import (
"encoding/json"
"fmt"
"os"
)
// CV represents the complete curriculum vitae structure
type CV struct {
Personal Personal `json:"personal"`
Summary string `json:"summary"`
Experience []Experience `json:"experience"`
AIDevelopment AIDevelopment `json:"ai_development"`
Education []Education `json:"education"`
Skills Skills `json:"skills"`
Languages []Language `json:"languages"`
Projects []Project `json:"projects"`
Awards []Award `json:"awards"`
Certifications []Certification `json:"certifications"`
Other Other `json:"other"`
Meta Meta `json:"meta"`
}
type Personal struct {
Name string `json:"name"`
Title string `json:"title"`
Location string `json:"location"`
Email string `json:"email"`
Phone string `json:"phone"`
DateOfBirth string `json:"dateOfBirth"`
PlaceOfBirth string `json:"placeOfBirth"`
Citizenship string `json:"citizenship"`
LinkedIn string `json:"linkedin"`
GitHub string `json:"github"`
Behance string `json:"behance"`
Website string `json:"website"`
Photo string `json:"photo"`
}
type Experience struct {
Position string `json:"position"`
Company string `json:"company"`
Location string `json:"location"`
StartDate string `json:"startDate"`
EndDate string `json:"endDate"`
Current bool `json:"current"`
Responsibilities []string `json:"responsibilities"`
Technologies []string `json:"technologies"`
Highlights []string `json:"highlights"`
}
type AIDevelopment struct {
Title string `json:"title"`
Period string `json:"period"`
Description string `json:"description"`
Skills []AISkill `json:"skills"`
Achievements []string `json:"achievements"`
}
type AISkill struct {
Category string `json:"category"`
Proficiency string `json:"proficiency"`
Items []string `json:"items"`
}
type Education struct {
Degree string `json:"degree"`
Institution string `json:"institution"`
Location string `json:"location"`
StartDate string `json:"startDate"`
EndDate string `json:"endDate"`
Field string `json:"field"`
}
type Skills struct {
Technical []SkillCategory `json:"technical"`
SoftSkills []string `json:"soft_skills"`
}
type SkillCategory struct {
Category string `json:"category"`
Proficiency int `json:"proficiency"`
Items []string `json:"items"`
}
type Language struct {
Language string `json:"language"`
Proficiency string `json:"proficiency"`
Level int `json:"level"`
}
type Project struct {
Name string `json:"name"`
Role string `json:"role"`
URL string `json:"url"`
Period string `json:"period"`
Description string `json:"description"`
Technologies []string `json:"technologies"`
Highlights []string `json:"highlights"`
}
type Award struct {
Title string `json:"title"`
Issuer string `json:"issuer"`
Date string `json:"date"`
Description string `json:"description"`
}
type Certification struct {
Name string `json:"name"`
Issuer string `json:"issuer"`
Date string `json:"date"`
Description string `json:"description"`
}
type Other struct {
DriverLicense string `json:"driverLicense"`
}
type Meta struct {
Version string `json:"version"`
LastUpdated string `json:"lastUpdated"`
Format string `json:"format"`
Language string `json:"language"`
}
// LoadCV loads CV data from a JSON file for the specified language
func LoadCV(lang string) (*CV, error) {
// Validate language
if lang != "en" && lang != "es" {
return nil, fmt.Errorf("unsupported language: %s", lang)
}
// Determine which JSON file to load
filename := fmt.Sprintf("data/cv-%s.json", lang)
// Read the JSON file
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("error reading file %s: %w", filename, err)
}
// Parse JSON
var cv CV
if err := json.Unmarshal(data, &cv); err != nil {
return nil, fmt.Errorf("error parsing JSON: %w", err)
}
return &cv, nil
}
+99
View File
@@ -0,0 +1,99 @@
package templates
import (
"fmt"
"html/template"
"log"
"path/filepath"
"sync"
"github.com/juanatsap/cv-site/internal/config"
)
// Manager handles template parsing and rendering
type Manager struct {
templates *template.Template
config *config.TemplateConfig
mu sync.RWMutex
}
// NewManager creates a new template manager
func NewManager(cfg *config.TemplateConfig) (*Manager, error) {
m := &Manager{
config: cfg,
}
if err := m.loadTemplates(); err != nil {
return nil, fmt.Errorf("failed to load templates: %w", err)
}
return m, nil
}
// loadTemplates parses all templates from the configured directory
func (m *Manager) loadTemplates() error {
m.mu.Lock()
defer m.mu.Unlock()
// Create template with custom functions
funcMap := template.FuncMap{
"iterate": func(count int) []int {
var result []int
for i := 0; i < count; i++ {
result = append(result, i)
}
return result
},
"eq": func(a, b string) bool {
return a == b
},
}
// Parse main templates
pattern := filepath.Join(m.config.Dir, "*.html")
tmpl, err := template.New("").Funcs(funcMap).ParseGlob(pattern)
if err != nil {
return fmt.Errorf("error parsing templates from %s: %w", pattern, err)
}
// Try to parse partials if they exist
partialsPattern := filepath.Join(m.config.PartialsDir, "*.html")
partials, _ := filepath.Glob(partialsPattern)
if len(partials) > 0 {
tmpl, err = tmpl.ParseGlob(partialsPattern)
if err != nil {
log.Printf("Warning: error parsing partials: %v", err)
}
}
m.templates = tmpl
log.Printf("✓ Templates loaded successfully from %s", m.config.Dir)
return nil
}
// Reload reloads all templates (useful for hot-reload in development)
func (m *Manager) Reload() error {
return m.loadTemplates()
}
// Render executes a template with the given data
func (m *Manager) Render(name string) (*template.Template, error) {
// Hot reload in development mode
if m.config.HotReload {
if err := m.Reload(); err != nil {
log.Printf("Warning: template reload failed: %v", err)
// Continue with cached templates
}
}
m.mu.RLock()
defer m.mu.RUnlock()
tmpl := m.templates.Lookup(name)
if tmpl == nil {
return nil, fmt.Errorf("template %q not found", name)
}
return tmpl, nil
}
+123
View File
@@ -0,0 +1,123 @@
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/juanatsap/cv-site/internal/config"
"github.com/juanatsap/cv-site/internal/handlers"
"github.com/juanatsap/cv-site/internal/middleware"
"github.com/juanatsap/cv-site/internal/templates"
)
const version = "1.0.0"
func main() {
// Initialize logger
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("🚀 Starting CV Server v" + version)
// Load configuration
cfg := config.Load()
log.Printf("✓ Configuration loaded (env: %s)", os.Getenv("GO_ENV"))
// Initialize template manager
templateMgr, err := templates.NewManager(&cfg.Template)
if err != nil {
log.Fatalf("❌ Failed to initialize templates: %v", err)
}
// Initialize handlers
cvHandler := handlers.NewCVHandler(templateMgr)
healthHandler := handlers.NewHealthHandler(version)
// Setup router
mux := http.NewServeMux()
// Routes
mux.HandleFunc("/", cvHandler.Home)
mux.HandleFunc("/cv", cvHandler.CVContent)
mux.HandleFunc("/export/pdf", cvHandler.ExportPDF)
mux.HandleFunc("/health", healthHandler.Check)
// Static files with cache control
staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("static")))
mux.Handle("/static/", cacheControl(staticHandler))
// Apply middleware chain
handler := middleware.Recovery(
middleware.Logger(
middleware.SecurityHeaders(mux),
),
)
// Create server with timeouts
server := &http.Server{
Addr: ":" + cfg.Server.Port,
Handler: handler,
ReadTimeout: time.Duration(cfg.Server.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(cfg.Server.WriteTimeout) * time.Second,
IdleTimeout: 120 * time.Second,
}
// Start server in goroutine
serverErrors := make(chan error, 1)
go func() {
log.Printf("✓ Server listening on http://%s:%s", cfg.Server.Host, cfg.Server.Port)
log.Printf("📄 English: http://%s:%s/?lang=en", cfg.Server.Host, cfg.Server.Port)
log.Printf("📄 Spanish: http://%s:%s/?lang=es", cfg.Server.Host, cfg.Server.Port)
log.Printf("❤️ Health: http://%s:%s/health", cfg.Server.Host, cfg.Server.Port)
log.Println("Press Ctrl+C to shutdown")
serverErrors <- server.ListenAndServe()
}()
// Setup graceful shutdown
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
// Wait for shutdown signal or server error
select {
case err := <-serverErrors:
if !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("❌ Server error: %v", err)
}
case sig := <-shutdown:
log.Printf("🛑 Shutdown signal received: %v", sig)
// Create shutdown context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Attempt graceful shutdown
if err := server.Shutdown(ctx); err != nil {
log.Printf("⚠️ Graceful shutdown failed, forcing: %v", err)
if err := server.Close(); err != nil {
log.Fatalf("❌ Failed to close server: %v", err)
}
}
log.Println("✓ Server stopped gracefully")
}
}
// cacheControl adds cache headers to static files
func cacheControl(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Cache static files for 1 hour in development, 1 day in production
maxAge := "3600" // 1 hour
if os.Getenv("GO_ENV") == "production" {
maxAge = "86400" // 1 day
}
w.Header().Set("Cache-Control", "public, max-age="+maxAge)
h.ServeHTTP(w, r)
})
}
+849
View File
@@ -0,0 +1,849 @@
/* Root Variables */
:root {
--bg-gray: #525659;
--paper-white: #ffffff;
--text-dark: #1a1a1a;
--text-gray: #4a4a4a;
--text-light: #6a6a6a;
--accent-blue: #2563eb;
--accent-blue-hover: #1d4ed8;
--border-gray: #e5e5e5;
--error-red: #dc2626;
--error-bg: #fee2e2;
--success-green: #10b981;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
/* Transition timings */
--transition-fast: 150ms;
--transition-base: 200ms;
--transition-slow: 300ms;
}
/* Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--bg-gray);
color: var(--text-dark);
line-height: 1.6;
min-height: 100vh;
}
/* Screen reader only text */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
a {
color: var(--accent-blue);
text-decoration: none;
transition: color var(--transition-base);
}
a:hover {
color: var(--accent-blue-hover);
text-decoration: underline;
}
a:focus-visible {
outline: 2px solid var(--accent-blue);
outline-offset: 2px;
border-radius: 2px;
}
/* Action Bar */
.action-bar {
background: rgba(255, 255, 255, 0.95);
border-bottom: 1px solid var(--border-gray);
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(10px);
box-shadow: var(--shadow);
}
.action-bar-content {
max-width: 1200px;
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.language-toggle {
display: flex;
gap: 0.5rem;
}
.lang-btn {
padding: 0.5rem 1rem;
border: 2px solid var(--border-gray);
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all var(--transition-base);
position: relative;
}
.lang-btn:hover:not(.active) {
border-color: var(--accent-blue);
background: #f0f9ff;
transform: translateY(-1px);
}
.lang-btn:focus-visible {
outline: 2px solid var(--accent-blue);
outline-offset: 2px;
}
.lang-btn.active {
border-color: var(--accent-blue);
background: var(--accent-blue);
color: white;
}
/* Loading state for buttons */
.lang-btn[aria-busy="true"] {
opacity: 0.7;
cursor: wait;
pointer-events: none;
}
.export-actions {
display: flex;
gap: 0.5rem;
}
.export-btn {
padding: 0.5rem 1.5rem;
background: var(--accent-blue);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all var(--transition-base);
box-shadow: var(--shadow);
}
.export-btn:hover {
background: var(--accent-blue-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.export-btn:focus-visible {
outline: 2px solid white;
outline-offset: 2px;
}
/* HTMX Indicator */
.htmx-indicator {
display: none;
align-items: center;
gap: 0.5rem;
}
.htmx-indicator.htmx-request {
display: flex;
}
.loader {
border: 3px solid #f3f3f3;
border-top: 3px solid var(--accent-blue);
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
display: inline-block;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* CV Container - Paper Effect */
.cv-container {
max-width: 900px;
margin: 2rem auto;
padding: 0 1rem;
}
.cv-paper {
background: var(--paper-white);
padding: 3rem;
box-shadow: var(--shadow-lg);
border-radius: 8px;
min-height: 11in; /* A4 height */
/* HTMX swap transitions */
transition: opacity var(--transition-base);
}
/* HTMX swap animation */
.cv-paper.htmx-swapping {
opacity: 0;
transition: opacity var(--transition-fast);
}
.cv-paper.htmx-settling {
opacity: 1;
transition: opacity var(--transition-base);
}
/* Error Toast */
.error-toast {
position: fixed;
bottom: 2rem;
right: 2rem;
background: var(--error-bg);
color: var(--error-red);
padding: 1rem 1.5rem;
border-radius: 8px;
border-left: 4px solid var(--error-red);
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
gap: 1rem;
max-width: 400px;
z-index: 1000;
animation: slideIn var(--transition-base) ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.error-toast button {
background: none;
border: none;
font-size: 1.5rem;
color: var(--error-red);
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity var(--transition-base);
}
.error-toast button:hover {
opacity: 0.7;
}
/* CV Header */
.cv-header {
border-bottom: 3px solid var(--accent-blue);
padding-bottom: 2rem;
margin-bottom: 2rem;
}
.cv-header-main {
margin-bottom: 1.5rem;
}
.cv-name {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-dark);
margin-bottom: 0.5rem;
}
.cv-title {
font-size: 1.5rem;
font-weight: 400;
color: var(--text-gray);
}
.cv-contact {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
font-size: 0.9rem;
}
.contact-item {
color: var(--text-gray);
}
/* Sections */
.cv-section {
margin-bottom: 2.5rem;
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-dark);
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border-gray);
}
.summary-text {
font-size: 1rem;
line-height: 1.8;
color: var(--text-gray);
text-align: justify;
}
/* AI Development Section */
.ai-section {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
padding: 2rem;
border-radius: 8px;
border-left: 4px solid var(--accent-blue);
}
.ai-period {
font-size: 0.9rem;
color: var(--text-gray);
margin-bottom: 1rem;
font-style: italic;
}
.ai-description {
margin-bottom: 1.5rem;
line-height: 1.8;
}
.ai-skills {
display: grid;
gap: 1.5rem;
}
.ai-skill-category {
background: white;
padding: 1.5rem;
border-radius: 6px;
box-shadow: var(--shadow);
}
.ai-skill-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--text-dark);
}
.proficiency-badge {
background: var(--accent-blue);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
margin-left: 0.5rem;
}
.ai-skill-list {
list-style: none;
display: grid;
gap: 0.5rem;
}
.ai-skill-list li:before {
content: "✨ ";
margin-right: 0.5rem;
}
.ai-achievements {
margin-top: 1.5rem;
background: white;
padding: 1.5rem;
border-radius: 6px;
}
.ai-achievements h4 {
margin-bottom: 1rem;
font-size: 1.1rem;
}
.achievement-list {
list-style: none;
}
.achievement-list li {
padding: 0.5rem 0;
border-bottom: 1px solid var(--border-gray);
}
.achievement-list li:last-child {
border-bottom: none;
}
.achievement-list li:before {
content: "🏆 ";
margin-right: 0.5rem;
}
/* Experience Items */
.experience-item {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--border-gray);
}
.experience-item:last-child {
border-bottom: none;
}
.experience-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
gap: 1rem;
}
.experience-title {
flex: 1;
}
.position {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-dark);
margin-bottom: 0.25rem;
}
.company {
font-size: 0.95rem;
color: var(--text-gray);
font-weight: 500;
}
.experience-period {
font-size: 0.9rem;
color: var(--text-light);
white-space: nowrap;
font-style: italic;
}
.responsibilities {
list-style: none;
margin-bottom: 1rem;
}
.responsibilities li {
padding-left: 1.5rem;
margin-bottom: 0.5rem;
position: relative;
}
.responsibilities li:before {
content: "▸";
position: absolute;
left: 0;
color: var(--accent-blue);
font-weight: bold;
}
.technologies {
font-size: 0.9rem;
color: var(--text-gray);
margin-top: 1rem;
}
.highlights {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.highlight-badge {
background: #fef3c7;
color: #92400e;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 500;
}
/* Skills Grid */
.skills-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.skill-category {
background: #f9fafb;
padding: 1.5rem;
border-radius: 6px;
border-left: 3px solid var(--accent-blue);
}
.skill-category-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.proficiency-stars {
color: #fbbf24;
margin-bottom: 1rem;
font-size: 1.1rem;
}
.skill-items {
list-style: none;
}
.skill-items li {
padding: 0.25rem 0;
padding-left: 1rem;
position: relative;
}
.skill-items li:before {
content: "•";
position: absolute;
left: 0;
color: var(--accent-blue);
}
.soft-skills {
margin-top: 2rem;
}
.soft-skills h4 {
margin-bottom: 1rem;
font-size: 1.1rem;
}
.soft-skills-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.soft-skill-tag {
background: #e0e7ff;
color: #3730a3;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
/* Projects */
.project-item {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--border-gray);
}
.project-item:last-child {
border-bottom: none;
}
.project-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.project-name {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-dark);
}
.project-period {
font-size: 0.9rem;
color: var(--text-light);
font-style: italic;
}
.project-role {
font-size: 0.9rem;
color: var(--text-gray);
margin-bottom: 0.5rem;
font-weight: 500;
}
.project-url {
font-size: 0.85rem;
margin-bottom: 0.75rem;
}
.project-description {
margin-bottom: 1rem;
line-height: 1.7;
}
.project-highlights {
list-style: none;
margin-top: 1rem;
}
.project-highlights li {
padding-left: 1.5rem;
margin-bottom: 0.25rem;
position: relative;
}
.project-highlights li:before {
content: "✓";
position: absolute;
left: 0;
color: #10b981;
font-weight: bold;
}
/* Education */
.education-item {
margin-bottom: 1.5rem;
}
.education-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.degree {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-dark);
}
.education-period {
font-size: 0.9rem;
color: var(--text-light);
font-style: italic;
}
.institution {
font-size: 0.95rem;
color: var(--text-gray);
margin-bottom: 0.25rem;
}
.field {
font-size: 0.9rem;
color: var(--text-light);
}
/* Certifications */
.certifications-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.certification-item {
background: #f9fafb;
padding: 1.25rem;
border-radius: 6px;
border-left: 3px solid #10b981;
}
.certification-name {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text-dark);
}
.certification-issuer {
font-size: 0.9rem;
color: var(--text-gray);
margin-bottom: 0.25rem;
}
.certification-date {
font-size: 0.85rem;
color: var(--text-light);
}
/* Awards */
.award-item {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-gray);
}
.award-item:last-child {
border-bottom: none;
}
.award-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-dark);
margin-bottom: 0.5rem;
}
.award-issuer {
font-size: 0.9rem;
color: var(--text-gray);
margin-bottom: 0.5rem;
}
.award-description {
font-size: 0.95rem;
color: var(--text-gray);
}
/* Languages */
.languages-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.language-item {
text-align: center;
padding: 1.5rem;
background: #f9fafb;
border-radius: 6px;
}
.language-name {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.language-proficiency {
font-size: 0.9rem;
color: var(--text-gray);
margin-bottom: 0.5rem;
}
/* Footer */
footer {
text-align: center;
padding: 2rem;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
}
/* Responsive */
@media (max-width: 768px) {
.cv-paper {
padding: 1.5rem;
}
.cv-name {
font-size: 2rem;
}
.cv-title {
font-size: 1.2rem;
}
.action-bar-content {
flex-direction: column;
padding: 1rem;
gap: 1rem;
}
.language-toggle {
width: 100%;
flex-wrap: wrap;
}
.lang-btn {
flex: 1;
min-width: 120px;
}
.export-actions {
width: 100%;
}
.export-btn {
width: 100%;
}
.error-toast {
bottom: 1rem;
right: 1rem;
left: 1rem;
max-width: none;
}
.experience-header,
.project-header,
.education-header {
flex-direction: column;
gap: 0.5rem;
}
.experience-period,
.project-period,
.education-period {
align-self: flex-start;
}
.skills-grid,
.certifications-grid,
.languages-grid {
grid-template-columns: 1fr;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Print-specific hiding */
.no-print {
/* Will be hidden in print.css */
}
/* High contrast mode support */
@media (prefers-contrast: high) {
.lang-btn {
border-width: 3px;
}
.lang-btn:focus-visible,
.export-btn:focus-visible,
a:focus-visible {
outline-width: 3px;
}
}
+351
View File
@@ -0,0 +1,351 @@
/* Minimal CV Design - Clean & Professional */
:root {
--bg-gray: #525659;
--paper-white: #ffffff;
--text-dark: #2d2d2d;
--text-gray: #555555;
--accent-blue: #0066cc;
--border-gray: #dddddd;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', Arial, sans-serif;
background-color: var(--bg-gray);
color: var(--text-dark);
line-height: 1.6;
}
a {
color: var(--accent-blue);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Action Bar */
.action-bar {
background: white;
border-bottom: 1px solid var(--border-gray);
position: sticky;
top: 0;
z-index: 100;
}
.action-bar-content {
max-width: 900px;
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.language-toggle {
display: flex;
gap: 0.5rem;
}
.lang-btn {
padding: 0.4rem 1rem;
border: 1px solid var(--border-gray);
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
.lang-btn:hover {
background: #f5f5f5;
}
.lang-btn.active {
background: var(--accent-blue);
color: white;
border-color: var(--accent-blue);
}
.export-btn {
padding: 0.4rem 1.2rem;
background: var(--accent-blue);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
.export-btn:hover {
background: #0052a3;
}
/* Loading Indicator */
.htmx-indicator {
display: none;
}
.htmx-indicator.htmx-request {
display: inline-block;
}
.loader {
border: 2px solid #f3f3f3;
border-top: 2px solid var(--accent-blue);
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* CV Container - Paper Design */
.cv-container {
max-width: 900px;
margin: 2rem auto;
padding: 0 1rem;
}
.cv-paper {
background: var(--paper-white);
padding: 3rem 4rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
min-height: 11in;
}
/* Header */
.cv-header {
border-bottom: 2px solid var(--text-dark);
padding-bottom: 1.5rem;
margin-bottom: 2rem;
}
.cv-name {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.cv-title {
font-size: 1.3rem;
font-weight: 400;
color: var(--text-gray);
margin-bottom: 1rem;
}
.cv-contact {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
font-size: 0.9rem;
color: var(--text-gray);
}
/* Sections */
.cv-section {
margin-bottom: 2rem;
}
.section-title {
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 1rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--border-gray);
}
.summary-text {
line-height: 1.7;
text-align: justify;
}
/* Experience */
.experience-item {
margin-bottom: 1.5rem;
}
.experience-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.75rem;
gap: 1rem;
}
.position {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.company {
color: var(--text-gray);
font-size: 0.95rem;
}
.experience-period {
color: var(--text-gray);
font-size: 0.9rem;
white-space: nowrap;
font-style: italic;
}
.responsibilities {
list-style: none;
margin-bottom: 0.75rem;
}
.responsibilities li {
padding-left: 1.2rem;
margin-bottom: 0.4rem;
position: relative;
}
.responsibilities li:before {
content: "•";
position: absolute;
left: 0;
font-weight: bold;
}
.technologies {
font-size: 0.85rem;
color: var(--text-gray);
font-style: italic;
}
/* Education */
.education-item {
margin-bottom: 1rem;
}
.education-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.degree {
font-size: 1rem;
font-weight: 600;
}
.education-period {
color: var(--text-gray);
font-size: 0.9rem;
font-style: italic;
}
.institution {
color: var(--text-gray);
font-size: 0.95rem;
}
/* Skills */
.skill-block {
margin-bottom: 1rem;
}
.skill-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.4rem;
}
.skill-list {
color: var(--text-gray);
font-size: 0.95rem;
}
/* Projects */
.project-item {
margin-bottom: 1.5rem;
}
.project-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.project-name {
font-size: 1.1rem;
font-weight: 600;
}
.project-period {
color: var(--text-gray);
font-size: 0.9rem;
font-style: italic;
}
.project-role {
color: var(--text-gray);
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.project-description {
margin-bottom: 0.5rem;
}
/* Certifications & Awards */
.cert-item,
.award-item {
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
/* Languages */
.languages-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.75rem;
}
.language-item {
font-size: 0.95rem;
}
/* Footer */
footer {
text-align: center;
padding: 2rem;
color: rgba(255,255,255,0.7);
font-size: 0.85rem;
}
/* Responsive */
@media (max-width: 768px) {
.cv-paper {
padding: 2rem 1.5rem;
}
.cv-name {
font-size: 2rem;
}
.experience-header,
.project-header,
.education-header {
flex-direction: column;
gap: 0.25rem;
}
.action-bar-content {
flex-direction: column;
gap: 1rem;
}
}
.no-print {}
+282
View File
@@ -0,0 +1,282 @@
/* Print Styles - Optimized for PDF Export */
@media print {
/* Reset for print */
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
color-adjust: exact !important;
}
/* Page setup */
@page {
size: A4;
margin: 1.5cm;
}
body {
background: white;
margin: 0;
padding: 0;
}
/* Hide non-print elements */
.no-print,
.action-bar,
footer {
display: none !important;
}
/* CV Container adjustments */
.cv-container {
max-width: 100%;
margin: 0;
padding: 0;
}
.cv-paper {
background: white;
box-shadow: none;
border-radius: 0;
padding: 0;
min-height: auto;
}
/* Typography */
body {
font-size: 10pt;
line-height: 1.4;
}
.cv-name {
font-size: 24pt;
page-break-after: avoid;
}
.cv-title {
font-size: 14pt;
page-break-after: avoid;
}
.section-title {
font-size: 14pt;
page-break-after: avoid;
margin-top: 1.5em;
}
/* Prevent page breaks */
.cv-header,
.cv-section,
.experience-item,
.project-item,
.education-item,
.award-item {
page-break-inside: avoid;
}
/* Links */
a {
color: #2563eb;
text-decoration: none;
}
a[href]:after {
content: none; /* Don't print URLs */
}
/* Compact spacing for print */
.cv-header {
margin-bottom: 1em;
padding-bottom: 0.5em;
}
.cv-section {
margin-bottom: 1.5em;
}
.experience-item,
.project-item {
margin-bottom: 1em;
padding-bottom: 0.75em;
}
/* Contact info - make it more compact */
.cv-contact {
grid-template-columns: repeat(2, 1fr);
gap: 0.3em;
font-size: 9pt;
}
/* AI Section - maintain visibility in print */
.ai-section {
background: #f0f9ff !important;
border-left: 3px solid #2563eb !important;
padding: 1em !important;
margin: 1em 0 !important;
}
.ai-skill-category {
background: white !important;
padding: 0.75em !important;
margin-bottom: 0.5em;
box-shadow: none;
border: 1px solid #e5e5e5;
}
.ai-achievements {
background: white !important;
padding: 0.75em !important;
border: 1px solid #e5e5e5;
}
/* Skills grid - more compact */
.skills-grid {
grid-template-columns: repeat(2, 1fr);
gap: 0.75em;
}
.skill-category {
background: #f9fafb !important;
padding: 0.75em !important;
}
/* Certifications grid */
.certifications-grid {
grid-template-columns: repeat(2, 1fr);
gap: 0.5em;
}
.certification-item {
background: #f9fafb !important;
padding: 0.75em !important;
}
/* Languages */
.languages-grid {
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
}
.language-item {
background: #f9fafb !important;
padding: 0.75em !important;
}
/* Badges and tags */
.proficiency-badge,
.highlight-badge,
.soft-skill-tag {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.proficiency-badge {
background: #2563eb !important;
color: white !important;
}
.highlight-badge {
background: #fef3c7 !important;
color: #92400e !important;
}
.soft-skill-tag {
background: #e0e7ff !important;
color: #3730a3 !important;
}
/* Ensure borders print */
.cv-header {
border-bottom: 2px solid #2563eb !important;
}
.section-title {
border-bottom: 1px solid #e5e5e5 !important;
}
.experience-item,
.project-item,
.award-item {
border-bottom: 1px solid #e5e5e5 !important;
}
/* Reduce list spacing */
.responsibilities,
.ai-skill-list,
.achievement-list,
.skill-items,
.project-highlights {
margin: 0.5em 0;
}
.responsibilities li,
.ai-skill-list li,
.achievement-list li,
.skill-items li,
.project-highlights li {
margin-bottom: 0.25em;
}
/* Font size adjustments for print */
.summary-text {
font-size: 9.5pt;
line-height: 1.5;
}
.position {
font-size: 11pt;
}
.company,
.institution {
font-size: 9.5pt;
}
.responsibilities,
.project-description {
font-size: 9pt;
}
.technologies {
font-size: 8.5pt;
}
/* Optimize spacing */
.ai-description {
margin-bottom: 0.75em;
}
.experience-header,
.project-header,
.education-header {
margin-bottom: 0.5em;
}
/* Ensure stars print correctly */
.proficiency-stars {
color: #fbbf24 !important;
}
/* Compact soft skills */
.soft-skills-list {
gap: 0.25em;
}
.soft-skill-tag {
padding: 0.25em 0.5em;
font-size: 8pt;
}
}
/* Print button functionality */
@media screen {
.print-only {
display: none;
}
}
@media print {
.print-only {
display: block;
}
}
+137
View File
@@ -0,0 +1,137 @@
<!-- CV Content Template - Minimal Design -->
<div class="cv-header">
<div class="cv-header-main">
<h1 class="cv-name">{{.CV.Personal.Name}}</h1>
<h2 class="cv-title">{{.CV.Personal.Title}}</h2>
</div>
<div class="cv-contact">
<div class="contact-item">{{.CV.Personal.Location}}</div>
<div class="contact-item"><a href="mailto:{{.CV.Personal.Email}}">{{.CV.Personal.Email}}</a></div>
<div class="contact-item">{{.CV.Personal.Phone}}</div>
<div class="contact-item"><a href="{{.CV.Personal.LinkedIn}}" target="_blank">LinkedIn</a></div>
<div class="contact-item"><a href="{{.CV.Personal.GitHub}}" target="_blank">GitHub</a></div>
</div>
</div>
<!-- Summary -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Resumen{{else}}Summary{{end}}</h3>
<p class="summary-text">{{.CV.Summary}}</p>
</section>
<!-- Experience -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Experiencia Laboral{{else}}Work History{{end}}</h3>
{{range .CV.Experience}}
<div class="experience-item">
<div class="experience-header">
<div class="experience-title">
<h4 class="position">{{.Position}}</h4>
<div class="company">{{.Company}}, {{.Location}}</div>
</div>
<div class="experience-period">
{{.StartDate}} - {{if .Current}}{{if eq $.Lang "es"}}Presente{{else}}Present{{end}}{{else}}{{.EndDate}}{{end}}
</div>
</div>
<ul class="responsibilities">
{{range .Responsibilities}}
<li>{{.}}</li>
{{end}}
</ul>
{{if .Technologies}}
<div class="technologies">
{{range $index, $tech := .Technologies}}{{if $index}}, {{end}}{{$tech}}{{end}}
</div>
{{end}}
</div>
{{end}}
</section>
<!-- Education -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Formación{{else}}Education{{end}}</h3>
{{range .CV.Education}}
<div class="education-item">
<div class="education-header">
<h4 class="degree">{{.Degree}}</h4>
<div class="education-period">{{.StartDate}} - {{.EndDate}}</div>
</div>
<div class="institution">{{.Institution}}, {{.Location}}</div>
</div>
{{end}}
</section>
<!-- Skills -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}</h3>
{{range .CV.Skills.Technical}}
<div class="skill-block">
<h4 class="skill-title">{{.Category}}</h4>
<p class="skill-list">
{{range $index, $item := .Items}}{{if $index}}, {{end}}{{$item}}{{end}}
</p>
</div>
{{end}}
</section>
<!-- Projects -->
{{if .CV.Projects}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Proyectos{{else}}Projects{{end}}</h3>
{{range .CV.Projects}}
<div class="project-item">
<div class="project-header">
<h4 class="project-name">{{.Name}}</h4>
<div class="project-period">{{.Period}}</div>
</div>
<div class="project-role">{{.Role}}</div>
<p class="project-description">{{.Description}}</p>
</div>
{{end}}
</section>
{{end}}
<!-- Certifications -->
{{if .CV.Certifications}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Certificaciones{{else}}Certifications{{end}}</h3>
{{range .CV.Certifications}}
<div class="cert-item">
<strong>{{.Name}}</strong> - {{.Issuer}} ({{.Date}})
</div>
{{end}}
</section>
{{end}}
<!-- Awards -->
{{if .CV.Awards}}
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Premios{{else}}Awards{{end}}</h3>
{{range .CV.Awards}}
<div class="award-item">
<strong>{{.Title}}</strong> - {{.Issuer}} ({{.Date}})
</div>
{{end}}
</section>
{{end}}
<!-- Languages -->
<section class="cv-section">
<h3 class="section-title">{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</h3>
<div class="languages-list">
{{range .CV.Languages}}
<div class="language-item">
<strong>{{.Language}}</strong>: {{.Proficiency}}
</div>
{{end}}
</div>
</section>
+171
View File
@@ -0,0 +1,171 @@
<!DOCTYPE html>
<html lang="{{if eq .Lang "es"}}es{{else}}en{{end}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{.CV.Personal.Name}} - {{.CV.Personal.Title}}">
<meta name="keywords" content="CV, Resume, {{.CV.Personal.Name}}, Developer, SAP, AI">
<meta name="author" content="{{.CV.Personal.Name}}">
<meta name="robots" content="index, follow">
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="{{.CV.Personal.Name}} - Curriculum Vitae">
<meta property="og:description" content="{{.CV.Personal.Title}}">
<meta property="og:type" content="profile">
<meta property="og:url" content="{{.CV.Personal.Website}}">
<title>{{.CV.Personal.Name}} - Curriculum Vitae</title>
<!-- HTMX with Integrity Check -->
<script src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"></script>
<!-- CSS -->
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/print.css" media="print">
<!-- Fonts with Preload -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Favicon -->
<link rel="icon" type="image/png" href="/static/favicon.png">
<!-- HTMX Configuration -->
<meta name="htmx-config" content='{"timeout":5000,"defaultSwapStyle":"innerHTML","defaultSwapDelay":0,"defaultSettleDelay":20}'>
</head>
<body>
<!-- Language & Export Bar (hidden in print) -->
<div class="action-bar no-print" role="navigation" aria-label="Language and export controls">
<div class="action-bar-content">
<div class="language-toggle" role="group" aria-label="Language selection">
<button
class="lang-btn {{if eq .Lang "en"}}active{{end}}"
hx-get="/cv?lang=en"
hx-target="#cv-content"
hx-swap="innerHTML swap:200ms settle:200ms"
hx-indicator="#loading"
hx-push-url="/?lang=en"
hx-on::before-request="this.setAttribute('aria-busy', 'true')"
hx-on::after-request="this.setAttribute('aria-busy', 'false')"
aria-label="Switch to English"
aria-pressed="{{if eq .Lang "en"}}true{{else}}false{{end}}">
🇬🇧 English
</button>
<button
class="lang-btn {{if eq .Lang "es"}}active{{end}}"
hx-get="/cv?lang=es"
hx-target="#cv-content"
hx-swap="innerHTML swap:200ms settle:200ms"
hx-indicator="#loading"
hx-push-url="/?lang=es"
hx-on::before-request="this.setAttribute('aria-busy', 'true')"
hx-on::after-request="this.setAttribute('aria-busy', 'false')"
aria-label="Switch to Spanish"
aria-pressed="{{if eq .Lang "es"}}true{{else}}false{{end}}">
🇪🇸 Español
</button>
</div>
<div class="export-actions">
<button
class="export-btn"
onclick="window.print()"
aria-label="{{if eq .Lang "es"}}Descargar PDF del CV{{else}}Download CV as PDF{{end}}">
📄 {{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}
</button>
</div>
<span id="loading" class="htmx-indicator" role="status" aria-live="polite" aria-label="Loading">
<span class="loader" aria-hidden="true"></span>
<span class="sr-only">Loading...</span>
</span>
</div>
</div>
<!-- CV Content Container -->
<div class="cv-container">
<main id="cv-content"
class="cv-paper"
role="main"
aria-live="polite"
aria-atomic="false">
{{template "cv-content.html" .}}
</main>
</div>
<!-- Error Toast (hidden by default) -->
<div id="error-toast" class="error-toast no-print" role="alert" aria-live="assertive" style="display: none;">
<span id="error-message"></span>
<button onclick="this.parentElement.style.display='none'" aria-label="Close error message">×</button>
</div>
<!-- Footer (hidden in print) -->
<footer class="no-print" role="contentinfo">
<p>&copy; {{.CV.Meta.LastUpdated}} {{.CV.Personal.Name}} |
{{if eq .Lang "es"}}Última actualización{{else}}Last updated{{end}}: {{.CV.Meta.LastUpdated}}</p>
</footer>
<!-- HTMX Event Handlers -->
<script>
// Global error handler for HTMX requests
document.body.addEventListener('htmx:responseError', function(evt) {
const errorToast = document.getElementById('error-toast');
const errorMessage = document.getElementById('error-message');
errorMessage.textContent = '{{if eq .Lang "es"}}Error al cargar el contenido. Por favor, inténtelo de nuevo.{{else}}Failed to load content. Please try again.{{end}}';
errorToast.style.display = 'flex';
// Auto-hide after 5 seconds
setTimeout(() => {
errorToast.style.display = 'none';
}, 5000);
});
// Smooth scroll to top on language change
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'cv-content') {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
// Save language preference
document.body.addEventListener('htmx:afterRequest', function(evt) {
const url = new URL(evt.detail.xhr.responseURL);
const lang = url.searchParams.get('lang');
if (lang) {
localStorage.setItem('cv-lang', lang);
}
});
// Load saved language preference on page load
window.addEventListener('DOMContentLoaded', function() {
const savedLang = localStorage.getItem('cv-lang');
const currentLang = '{{.Lang}}';
if (savedLang && savedLang !== currentLang) {
document.querySelector(`[hx-get="/cv?lang=${savedLang}"]`)?.click();
}
});
// Keyboard shortcuts
document.addEventListener('keydown', function(evt) {
// Ctrl/Cmd + P for print
if ((evt.ctrlKey || evt.metaKey) && evt.key === 'p') {
evt.preventDefault();
window.print();
}
// Ctrl/Cmd + E for English, Ctrl/Cmd + S for Spanish
if (evt.ctrlKey || evt.metaKey) {
if (evt.key === 'e') {
evt.preventDefault();
document.querySelector('[hx-get="/cv?lang=en"]')?.click();
} else if (evt.key === 's' && evt.shiftKey) {
evt.preventDefault();
document.querySelector('[hx-get="/cv?lang=es"]')?.click();
}
}
});
</script>
</body>
</html>
+72
View File
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="{{if eq .Lang "es"}}es{{else}}en{{end}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{.CV.Personal.Name}} - {{.CV.Personal.Title}}">
<meta name="keywords" content="CV, Resume, {{.CV.Personal.Name}}, Developer, SAP, AI">
<title>{{.CV.Personal.Name}} - Curriculum Vitae</title>
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- CSS -->
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/print.css" media="print">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<!-- Language & Export Bar (hidden in print) -->
<div class="action-bar no-print">
<div class="action-bar-content">
<div class="language-toggle">
<button
class="lang-btn {{if eq .Lang "en"}}active{{end}}"
hx-get="/cv?lang=en"
hx-target="#cv-content"
hx-swap="innerHTML"
hx-indicator="#loading">
🇬🇧 English
</button>
<button
class="lang-btn {{if eq .Lang "es"}}active{{end}}"
hx-get="/cv?lang=es"
hx-target="#cv-content"
hx-swap="innerHTML"
hx-indicator="#loading">
🇪🇸 Español
</button>
</div>
<div class="export-actions">
<button
class="export-btn"
onclick="window.print()">
📄 {{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}
</button>
</div>
<span id="loading" class="htmx-indicator">
<span class="loader"></span>
</span>
</div>
</div>
<!-- CV Content Container -->
<div class="cv-container">
<div id="cv-content" class="cv-paper">
{{template "cv-content.html" .}}
</div>
</div>
<!-- Footer (hidden in print) -->
<footer class="no-print">
<p>© {{.CV.Meta.LastUpdated}} {{.CV.Personal.Name}} |
{{if eq .Lang "es"}}Última actualización{{else}}Last updated{{end}}: {{.CV.Meta.LastUpdated}}</p>
</footer>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
# Placeholder for future partial templates