Files
cv-site/templates/index.html
T
juanatsap 92dffe8c60 feat: add comprehensive testing infrastructure and security hardening
- Enhanced CI/CD pipeline with coverage reporting, benchmarks, and artifact uploads
- Implemented rate limiter IP validation with proxy support and spoofing protection
- Added extensive Makefile test targets for coverage, benchmarks, and continuous testing
- Expanded middleware chain with request validation, size limits, and suspicious activity logging
2025-11-11 21:43:12 +00:00

477 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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">
<!-- Primary Meta Tags -->
<title>{{.CV.Personal.Name}} - {{if eq .Lang "es"}}Curriculum Vitae{{else}}Curriculum Vitae{{end}}</title>
<meta name="title" content="{{.CV.Personal.Name}} - {{if eq .Lang "es"}}CV Profesional{{else}}Professional CV{{end}}">
<meta name="description" content="{{.CV.Personal.Title}} | {{if eq .Lang "es"}}18 años de experiencia en desarrollo web, SAP CDC, React, Node.js, Go, HTMX y desarrollo asistido por IA{{else}}18 years of experience in web development, SAP CDC, React, Node.js, Go, HTMX and AI-assisted development{{end}}">
<meta name="keywords" content="{{if eq .Lang "es"}}CV, Curriculum Vitae, {{.CV.Personal.Name}}, Desarrollador FullStack, SAP CDC, React, Node.js, Go, HTMX, IA, Desarrollo Web, Consultor Técnico{{else}}CV, Resume, {{.CV.Personal.Name}}, FullStack Developer, SAP CDC, React, Node.js, Go, HTMX, AI, Web Development, Technical Consultant{{end}}">
<meta name="author" content="{{.CV.Personal.Name}}">
<meta name="robots" content="index, follow">
<link rel="canonical" href="{{.CanonicalURL}}">
<!-- Hreflang tags for international SEO -->
<link rel="alternate" hreflang="en" href="{{.AlternateEN}}">
<link rel="alternate" hreflang="es" href="{{.AlternateES}}">
<link rel="alternate" hreflang="x-default" href="https://juan.andres.morenorub.io/?lang=en">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="profile">
<meta property="og:url" content="{{.CV.Personal.Website}}">
<meta property="og:title" content="{{.CV.Personal.Name}} - {{if eq .Lang "es"}}CV Profesional{{else}}Professional CV{{end}}">
<meta property="og:description" content="{{.CV.Personal.Title}} | {{if eq .Lang "es"}}Consultor Técnico Senior con 18 años de experiencia{{else}}Senior Technical Consultant with 18 years of experience{{end}}">
<meta property="og:image" content="{{.CV.Personal.Website}}/static/images/profile.jpg">
<meta property="og:locale" content="{{if eq .Lang "es"}}es_ES{{else}}en_US{{end}}">
<meta property="og:site_name" content="{{.CV.Personal.Name}}">
<meta property="profile:first_name" content="Juan Andrés">
<meta property="profile:last_name" content="Moreno Rubio">
<meta property="profile:username" content="txeo">
<!-- Social Media Card (Generic) -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{.CV.Personal.Name}} - {{if eq .Lang "es"}}CV Profesional{{else}}Professional CV{{end}}">
<meta name="twitter:description" content="{{.CV.Personal.Title}}">
<meta name="twitter:image" content="{{.CV.Personal.Website}}/static/images/profile.jpg">
<!-- HTMX Configuration -->
<meta name="htmx-config" content='{"timeout":5000,"defaultSwapStyle":"innerHTML","defaultSwapDelay":0,"defaultSettleDelay":20}'>
<!-- HTMX with SRI (Subresource Integrity) -->
<script src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"></script>
<!-- Iconify - Load synchronously for immediate rendering -->
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
<!-- CSS -->
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/logo-toggle.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 rel="dns-prefetch" href="https://fonts.googleapis.com">
<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}}",
"email": "{{.CV.Personal.Email}}",
"telephone": "{{.CV.Personal.Phone}}",
"address": {
"@type": "PostalAddress",
"addressLocality": "{{.CV.Personal.Location}}"
},
"sameAs": [
"{{.CV.Personal.LinkedIn}}",
"{{.CV.Personal.GitHub}}",
"{{.CV.Personal.Domestika}}"
],
"alumniOf": {
"@type": "EducationalOrganization",
"name": "Universidad de Extremadura"
},
"knowsAbout": [
"Web Development",
"SAP Customer Data Cloud",
"React",
"Node.js",
"Go",
"HTMX",
"AI-Assisted Development",
"Full Stack Development"
],
"worksFor": {
"@type": "Organization",
"name": "Olympic Broadcasting Services"
}
}
</script>
</head>
<body>
<!-- Single Black Bar with Everything -->
<div class="action-bar no-print" role="navigation" aria-label="Language and export controls">
<div class="action-bar-content">
<!-- Left: Site Title + Hamburger Menu + Language -->
<div class="site-title">
<!-- Left group: Logo + Hamburger -->
<div class="site-title-left">
<a href="/" class="site-logo-link" id="logoLink" aria-label="Home">
<iconify-icon icon="mdi:file-account" width="24" height="24" class="site-icon"></iconify-icon>
</a>
<!-- Hamburger Menu Button -->
<button class="hamburger-btn" aria-label="Toggle navigation menu">
<iconify-icon icon="mdi:menu" width="24" height="24"></iconify-icon>
</button>
</div>
<!-- Center: Title -->
<a href="/" class="site-title-link" id="titleLink">
<iconify-icon icon="mdi:file-account" width="20" height="20" class="site-icon-mobile"></iconify-icon>
<span class="site-title-text">CV JAMR<span class="site-title-year"> - {{.CurrentYear}}</span></span>
</a>
<!-- Right: Language selector -->
<div class="language-selector">
<button class="selector-btn {{if eq .Lang "en"}}active{{end}}" data-short="EN" onclick="selectLanguage('en')" aria-label="English">
English
</button>
<button class="selector-btn {{if eq .Lang "es"}}active{{end}}" data-short="ES" onclick="selectLanguage('es')" aria-label="Español">
Español
</button>
</div>
</div>
<!-- Center: View controls with labels -->
<div class="view-controls-center">
<!-- CV Length toggle -->
<div class="selector-group">
<label class="selector-label">{{if eq .Lang "es"}}Longitud{{else}}Length{{end}}:</label>
<label class="icon-toggle">
<input type="checkbox" id="lengthToggle" onchange="toggleCVLength()">
<span class="icon-toggle-slider">
<iconify-icon icon="mdi:file-document-outline" width="16" height="16" class="icon-left"></iconify-icon>
<iconify-icon icon="mdi:file-document-multiple-outline" width="16" height="16" class="icon-right"></iconify-icon>
</span>
</label>
</div>
<!-- Logo toggle -->
<div class="selector-group">
<label class="selector-label">{{if eq .Lang "es"}}Logos{{else}}Logos{{end}}:</label>
<label class="icon-toggle">
<input type="checkbox" id="logoToggle" checked onchange="toggleLogos()">
<span class="icon-toggle-slider">
<iconify-icon icon="mdi:image-off-outline" width="16" height="16" class="icon-left"></iconify-icon>
<iconify-icon icon="mdi:image-multiple-outline" width="16" height="16" class="icon-right"></iconify-icon>
</span>
</label>
</div>
<!-- Theme toggle -->
<div class="selector-group">
<label class="selector-label">{{if eq .Lang "es"}}Vista{{else}}View{{end}}:</label>
<label class="icon-toggle">
<input type="checkbox" id="themeToggle" onchange="toggleTheme()">
<span class="icon-toggle-slider">
<iconify-icon icon="mdi:page-layout-sidebar-left" width="16" height="16" class="icon-left"></iconify-icon>
<iconify-icon icon="mdi:page-layout-body" width="16" height="16" class="icon-right"></iconify-icon>
</span>
</label>
</div>
</div>
<!-- Right: Action buttons -->
<div class="action-buttons-right">
<button
class="action-btn pdf-btn"
onclick="openPdfModal()"
aria-label="{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}">
<iconify-icon icon="mdi:download" width="18" height="18"></iconify-icon>
{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}
</button>
<button
class="action-btn print-btn"
onclick="printFriendly()"
aria-label="{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}">
<iconify-icon icon="mdi:leaf" width="18" height="18"></iconify-icon>
{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}
</button>
</div>
<span id="loading"
class="htmx-indicator"
role="status"
aria-live="polite"
aria-label="Loading">
<span class="loader"></span>
</span>
</div>
</div>
<!-- Navigation Menu (Hidden by default) -->
<nav id="navigation-menu" class="navigation-menu no-print" role="navigation" aria-label="CV sections">
<div class="menu-content">
<!-- CV Sections - Quick Navigation -->
<div class="menu-item-submenu">
<a href="#" class="menu-item has-submenu">
<iconify-icon icon="mdi:menu" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Secciones CV{{else}}CV Sections{{end}}</span>
<iconify-icon icon="mdi:chevron-right" width="16" height="16" class="submenu-arrow"></iconify-icon>
</a>
<div class="submenu-content">
<a href="#education" class="menu-item" onclick="scrollToSection('education')">
<iconify-icon icon="mdi:school" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Formación{{else}}Training{{end}}</span>
</a>
<a href="#skills" class="menu-item" onclick="scrollToSection('skills')">
<iconify-icon icon="mdi:brain" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Competencias{{else}}Skills{{end}}</span>
</a>
<a href="#experience" class="menu-item" onclick="scrollToSection('experience')">
<iconify-icon icon="mdi:office-building" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Experiencia{{else}}Experience{{end}}</span>
</a>
<a href="#awards" class="menu-item" onclick="scrollToSection('awards')">
<iconify-icon icon="mdi:trophy" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Premios y Reconocimientos{{else}}Awards{{end}}</span>
</a>
<a href="#projects" class="menu-item" onclick="scrollToSection('projects')">
<iconify-icon icon="mdi:web" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Proyectos Personales / Freelance{{else}}Personal / Freelance Projects{{end}}</span>
</a>
<a href="#courses" class="menu-item" onclick="scrollToSection('courses')">
<iconify-icon icon="mdi:school" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Cursos Realizados{{else}}Courses{{end}}</span>
</a>
<a href="#languages" class="menu-item" onclick="scrollToSection('languages')">
<iconify-icon icon="mdi:translate" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Idiomas{{else}}Languages{{end}}</span>
</a>
<a href="#references" class="menu-item" onclick="scrollToSection('references')">
<iconify-icon icon="mdi:link-variant" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Referencias{{else}}References{{end}}</span>
</a>
<a href="#other" class="menu-item" onclick="scrollToSection('other')">
<iconify-icon icon="mdi:information" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Otros{{else}}Other{{end}}</span>
</a>
</div>
</div>
<!-- Quick Actions Section -->
<div class="menu-section-wrapper">
<div class="menu-item menu-item-header">
<iconify-icon icon="mdi:cog-outline" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Acciones Rápidas{{else}}Quick Actions{{end}}</span>
</div>
<a href="#" class="menu-item menu-item-action" onclick="collapseAllSections(event)">
<iconify-icon icon="mdi:arrow-collapse-all" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Colapsar Todo{{else}}Collapse All{{end}}</span>
</a>
<a href="#" class="menu-item menu-item-action" onclick="expandAllSections(event)">
<iconify-icon icon="mdi:arrow-expand-all" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Expandir Todo{{else}}Expand All{{end}}</span>
</a>
</div>
<!-- View Controls in menu (visible only on mobile < 900px) -->
<div class="menu-controls-section">
<div class="menu-item menu-item-header">
<iconify-icon icon="mdi:tune-variant" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Controles de Vista{{else}}View Controls{{end}}</span>
</div>
<!-- CV Length toggle -->
<div class="menu-control-item">
<label class="menu-control-label">
<iconify-icon icon="mdi:file-document-outline" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Longitud{{else}}Length{{end}}</span>
</label>
<label class="icon-toggle">
<input type="checkbox" id="lengthToggleMenu" onchange="toggleCVLength()">
<span class="icon-toggle-slider">
<iconify-icon icon="mdi:file-document-outline" width="16" height="16" class="icon-left"></iconify-icon>
<iconify-icon icon="mdi:file-document-multiple-outline" width="16" height="16" class="icon-right"></iconify-icon>
</span>
</label>
</div>
<!-- Logo toggle -->
<div class="menu-control-item">
<label class="menu-control-label">
<iconify-icon icon="mdi:image-multiple-outline" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Logos{{else}}Logos{{end}}</span>
</label>
<label class="icon-toggle">
<input type="checkbox" id="logoToggleMenu" checked onchange="toggleLogos()">
<span class="icon-toggle-slider">
<iconify-icon icon="mdi:image-off-outline" width="16" height="16" class="icon-left"></iconify-icon>
<iconify-icon icon="mdi:image-multiple-outline" width="16" height="16" class="icon-right"></iconify-icon>
</span>
</label>
</div>
<!-- Theme toggle -->
<div class="menu-control-item">
<label class="menu-control-label">
<iconify-icon icon="mdi:page-layout-sidebar-left" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Vista{{else}}View{{end}}</span>
</label>
<label class="icon-toggle">
<input type="checkbox" id="themeToggleMenu" onchange="toggleTheme()">
<span class="icon-toggle-slider">
<iconify-icon icon="mdi:page-layout-sidebar-left" width="16" height="16" class="icon-left"></iconify-icon>
<iconify-icon icon="mdi:page-layout-body" width="16" height="16" class="icon-right"></iconify-icon>
</span>
</label>
</div>
</div>
<!-- Action Buttons in menu (visible only on mobile < 900px) -->
<div class="menu-actions-section">
<div class="menu-item menu-item-header">
<iconify-icon icon="mdi:lightning-bolt" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Acciones{{else}}Actions{{end}}</span>
</div>
<button class="menu-action-btn" onclick="openPdfModal()">
<iconify-icon icon="mdi:download" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Descargar como PDF{{else}}Download as PDF{{end}}</span>
</button>
<button class="menu-action-btn" onclick="printFriendly()">
<iconify-icon icon="mdi:leaf" width="20" height="20"></iconify-icon>
<span>{{if eq .Lang "es"}}Imprimir amigable{{else}}Print Friendly{{end}}</span>
</button>
</div>
</div>
</nav>
<!-- CV Content Container -->
<div class="cv-container">
<main id="cv-content"
class="cv-paper"
role="main"
aria-live="polite">
{{template "cv-content.html" .}}
</main>
</div>
<!-- Footer (hidden in print) -->
<footer class="no-print">
<p style="text-align: center; margin-bottom: 0.5rem;">
<a href="https://github.com/juanatsap/cv-site" target="_blank" rel="noopener noreferrer" class="github-repo-link" style="text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem;">
<iconify-icon icon="mdi:github" width="20" height="20"></iconify-icon>
{{if eq .Lang "es"}}Ver este proyecto en GitHub{{else}}View this project on GitHub{{end}}
</a>
</p>
<p>© {{.CV.Meta.LastUpdated}} {{.CV.Personal.Name}} |
{{if eq .Lang "es"}}Última actualización{{else}}Last updated{{end}}: {{.CV.Meta.LastUpdated}}</p>
</footer>
<!-- Error Toast -->
<div id="error-toast" class="error-toast no-print" role="alert" aria-live="assertive" style="display: none;">
<span class="error-icon">⚠️</span>
<span id="error-message"></span>
<button onclick="this.parentElement.style.display='none'" aria-label="Close error message" class="error-close">×</button>
</div>
<!-- Back to Top Button -->
<button id="back-to-top" class="back-to-top no-print" aria-label="{{if eq .Lang "es"}}Volver arriba{{else}}Back to top{{end}}" style="display: none;">
<iconify-icon icon="mdi:arrow-up" width="24" height="24"></iconify-icon>
</button>
<!-- Info Button (Bottom Left) -->
<button id="info-button" class="info-button no-print" aria-label="{{if eq .Lang "es"}}Información{{else}}Information{{end}}" onclick="openInfoModal()">
<iconify-icon icon="mdi:information-outline" width="24" height="24"></iconify-icon>
</button>
<!-- Info Modal -->
<div id="info-modal" class="info-modal no-print" onclick="closeInfoModalOnBackdrop(event)">
<div class="info-modal-content" onclick="event.stopPropagation()">
<button class="info-modal-close" onclick="closeInfoModal()" aria-label="Close">
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
</button>
<div class="info-modal-header">
<h2>{{.UI.InfoModal.Title}}</h2>
<div class="info-modal-cv-title">
CV {{.CurrentYear}} JAMR -
<span class="photo-bracket-wrapper">
<img src="/static/images/profile/dni.jpeg" alt="JAMR" class="info-modal-photo">
</span>
</div>
</div>
<div class="info-modal-body">
<p class="info-modal-description">
{{.UI.InfoModal.Description}}
</p>
<div class="info-modal-tech">
<div class="info-tech-item">
<iconify-icon icon="mdi:language-go" width="32" height="32"></iconify-icon>
<span>{{.UI.InfoModal.TechStack.GoHono}}</span>
</div>
<div class="info-tech-item">
<iconify-icon icon="mdi:lightning-bolt" width="32" height="32"></iconify-icon>
<span>{{.UI.InfoModal.TechStack.HTMX}}</span>
</div>
<div class="info-tech-item">
<iconify-icon icon="mdi:language-html5" width="32" height="32"></iconify-icon>
<span>{{.UI.InfoModal.TechStack.HTML5}}</span>
</div>
<div class="info-tech-item">
<iconify-icon icon="mdi:language-css3" width="32" height="32"></iconify-icon>
<span>{{.UI.InfoModal.TechStack.CSS3}}</span>
</div>
</div>
<p class="info-modal-github-subtext">{{.UI.InfoModal.ViewSourceSubtext}}</p>
<a href="https://github.com/juanatsap/cv-site" target="_blank" rel="noopener noreferrer" class="info-modal-github">
<iconify-icon icon="mdi:github" width="24" height="24"></iconify-icon>
<span>{{.UI.InfoModal.ViewSource}}</span>
<iconify-icon icon="mdi:arrow-right" width="20" height="20"></iconify-icon>
</a>
</div>
</div>
</div>
<!-- PDF Export Modal -->
<div id="pdf-modal" class="info-modal no-print" onclick="closePdfModalOnBackdrop(event)">
<div class="info-modal-content" onclick="event.stopPropagation()">
<button class="info-modal-close" onclick="closePdfModal()" aria-label="Close">
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
</button>
<div class="info-modal-header">
<div class="icon" style="font-size: 3rem; margin-bottom: 1rem;">🚧</div>
<h2>{{if eq .Lang "es"}}Exportación PDF - En Desarrollo{{else}}PDF Export - Work in Progress{{end}}</h2>
</div>
<div class="info-modal-body">
<p class="info-modal-description">
{{if eq .Lang "es"}}
La función de exportación a PDF está siendo mejorada. Por favor, usa el botón <strong>Imprimir Amigable</strong> en su lugar (Ctrl+P o Cmd+P para guardar como PDF).
{{else}}
The PDF export feature is currently being improved. Please use the <strong>Print Friendly</strong> button instead (Ctrl+P or Cmd+P to save as PDF).
{{end}}
</p>
</div>
</div>
</div>
<!-- External JavaScript - CSP Compliant -->
<script src="/static/js/main.js"></script>
<!-- Matomo Analytics - Nonce-based CSP -->
<script nonce="{{.CSPNonce}}">
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://matomo.drolo.club/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '4']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
</body>
</html>