2025-10-20 08:54:21 +01:00
<!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" >
2025-10-31 11:06:38 +00:00
<!-- Primary Meta Tags -->
2025-11-30 10:13:37 +00:00
< title > {{.CV.Personal.Name}} - {{.CV.SEO.PageTitle}}< / title >
< meta name = "title" content = "{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}" >
< meta name = "description" content = "{{.CV.Personal.Title}} | {{.CV.SEO.MetaDescription}}" >
< meta name = "keywords" content = "{{.CV.Personal.Name}}, {{.CV.SEO.Keywords}}" >
2025-10-31 11:06:38 +00:00
< meta name = "author" content = "{{.CV.Personal.Name}}" >
< meta name = "robots" content = "index, follow" >
2025-11-11 13:53:14 +00:00
< 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" >
2025-10-31 11:06:38 +00:00
<!-- Open Graph / Facebook -->
< meta property = "og:type" content = "profile" >
< meta property = "og:url" content = "{{.CV.Personal.Website}}" >
2025-11-30 10:13:37 +00:00
< meta property = "og:title" content = "{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}" >
< meta property = "og:description" content = "{{.CV.Personal.Title}} | {{.CV.SEO.OgDescription}}" >
2025-10-31 11:06:38 +00:00
< 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}}" >
2025-11-30 10:13:37 +00:00
< meta property = "profile:first_name" content = "{{.CV.Personal.FirstName}}" >
< meta property = "profile:last_name" content = "{{.CV.Personal.LastName}}" >
< meta property = "profile:username" content = "{{.CV.Personal.Username}}" >
2025-10-31 11:06:38 +00:00
<!-- Social Media Card (Generic) -->
< meta name = "twitter:card" content = "summary" >
2025-11-30 10:13:37 +00:00
< meta name = "twitter:title" content = "{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}" >
2025-10-31 11:06:38 +00:00
< 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}' >
2025-11-30 09:29:35 +00:00
<!-- FOUC Prevention: Inline critical CSS + Apply color theme before page render -->
<!-- Critical theme variables inlined to prevent flash of unstyled content -->
< style >
/* Light theme (default) - critical variables only */
: root {
--page-bg : #d6d6d6 ;
--paper-bg : #ffffff ;
--text-primary : #1a1a1a ;
--sidebar-bg : #d1d4d2 ;
}
/* Dark theme - critical variables only */
[ data-color-theme = "dark" ] {
--page-bg : #3a3a3a ;
--paper-bg : #1a1a1a ;
--text-primary : #e0e0e0 ;
--sidebar-bg : #3a3d3e ;
}
/* Auto theme follows system preference */
@ media ( prefers-color-scheme : dark ) {
[ data-color-theme = "auto" ] {
--page-bg : #3a3a3a ;
--paper-bg : #1a1a1a ;
--text-primary : #e0e0e0 ;
--sidebar-bg : #3a3d3e ;
}
}
/* Apply critical styles immediately */
html { background-color : var ( - - page - bg ) ; }
body { color : var ( - - text - primary ) ; }
< / style >
2025-11-18 15:49:30 +00:00
< script >
( function ( ) {
2025-11-30 09:35:31 +00:00
// First-time visitors ALWAYS get light theme (paper aesthetic)
// Users can switch to dark/auto and their preference is saved
let theme = localStorage . getItem ( 'color-theme-mode' ) || 'light' ;
2025-11-18 15:49:30 +00:00
document . documentElement . setAttribute ( 'data-color-theme' , theme ) ;
} ) ( ) ;
< / script >
2025-11-24 20:48:12 +00:00
<!-- Device Detection - Detect real mobile devices vs desktop browser -->
< script src = "/static/js/device-detection.js" > < / script >
2025-10-31 11:06:38 +00:00
<!-- HTMX with SRI (Subresource Integrity) -->
< script src = "https://unpkg.com/htmx.org@1.9.10"
integrity = "sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin = "anonymous" > < / script >
2025-10-20 08:54:21 +01:00
2025-11-14 21:38:09 +00:00
<!-- Hyperscript Functions - Must load BEFORE hyperscript library -->
2025-11-30 05:58:44 +00:00
<!-- NOTE: cv - functions.js removed - hyperscript def statements are globally available -->
2025-11-17 16:28:52 +00:00
<!-- ✅ NO def limit with latest hyperscript - organized by category -->
< script type = "text/hyperscript" src = "/static/hyperscript/utils._hs" > < / script >
< script type = "text/hyperscript" src = "/static/hyperscript/toggles._hs" > < / script >
< script type = "text/hyperscript" src = "/static/hyperscript/hover-sync._hs" > < / script >
2025-11-20 09:17:09 +00:00
< script type = "text/hyperscript" src = "/static/hyperscript/keyboard._hs" > < / script >
2025-11-30 05:58:44 +00:00
< script type = "text/hyperscript" src = "/static/hyperscript/zoom._hs" > < / script >
< script type = "text/hyperscript" src = "/static/hyperscript/pdf-modal._hs" > < / script >
2025-11-14 21:38:09 +00:00
2025-11-18 15:49:30 +00:00
<!-- Color Theme System (JavaScript - hyperscript had parsing issues with colons in strings) -->
< script src = "/static/js/color-theme.js" > < / script >
2025-11-30 05:58:44 +00:00
<!-- NOTE: footer - buttons - interaction.js removed - moved to hyperscript on footer element -->
<!-- NOTE: scroll - at - bottom - handler.js removed - duplicate of handleScroll() in utils._hs -->
2025-11-22 16:23:05 +00:00
2025-11-12 22:54:46 +00:00
<!-- Hyperscript - Declarative event handling for enhanced interactivity -->
2025-11-17 13:00:03 +00:00
< script src = "https://unpkg.com/hyperscript.org@0.9.14" > < / script >
2025-11-12 22:54:46 +00:00
2025-12-01 13:03:06 +00:00
<!-- Ninja Keys - CMD+K Command Bar -->
< script type = "module" src = "https://unpkg.com/ninja-keys?module" > < / script >
2025-11-07 11:49:47 +00:00
<!-- Iconify - Load synchronously for immediate rendering -->
2025-11-18 15:49:30 +00:00
<!-- Using unpkg CDN (more reliable than code.iconify.design) -->
< script src = "https://cdn.jsdelivr.net/npm/iconify-icon@2.1.0/dist/iconify-icon.min.js" > < / script >
2025-11-06 10:36:00 +00:00
2025-11-30 12:32:46 +00:00
<!-- CSS - Conditional loading: bundled in production, modular in development -->
{{if .IsProduction}}
< link rel = "stylesheet" href = "/static/dist/bundle.min.css" >
{{else}}
2025-10-20 08:54:21 +01:00
< link rel = "stylesheet" href = "/static/css/main.css" >
2025-11-30 12:32:46 +00:00
{{end}}
2025-11-30 11:13:47 +00:00
<!-- Print styles - loaded separately, only applied when printing -->
< link rel = "stylesheet" href = "/static/css/print.css" media = "print" >
2025-10-20 08:54:21 +01:00
2025-10-31 11:06:38 +00:00
<!-- Fonts with Preload -->
2025-10-20 08:54:21 +01:00
< link rel = "preconnect" href = "https://fonts.googleapis.com" >
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin >
2025-10-31 11:06:38 +00:00
< link rel = "dns-prefetch" href = "https://fonts.googleapis.com" >
2025-11-30 12:49:16 +00:00
< link href = "https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&family=Source+Sans+Pro:wght@300;400;600&display=swap" rel = "stylesheet" >
2025-10-31 11:06:38 +00:00
2025-11-30 13:23:22 +00:00
<!-- Structured Data (JSON - LD) - Enhanced for AI - era SEO -->
<!-- Person Schema -->
2025-10-31 11:06:38 +00:00
< script type = "application/ld+json" >
{
"@context" : "https://schema.org" ,
"@type" : "Person" ,
2025-11-30 13:23:22 +00:00
"@id" : "{{.CV.Personal.Website}}/#person" ,
2025-10-31 11:06:38 +00:00
"name" : "{{.CV.Personal.Name}}" ,
2025-11-30 13:23:22 +00:00
"givenName" : "{{.CV.Personal.FirstName}}" ,
"familyName" : "{{.CV.Personal.LastName}}" ,
2025-10-31 11:06:38 +00:00
"jobTitle" : "{{.CV.Personal.Title}}" ,
2025-11-30 13:23:22 +00:00
"description" : "{{.CV.Summary}}" ,
2025-10-31 11:06:38 +00:00
"url" : "{{.CV.Personal.Website}}" ,
2025-11-30 13:23:22 +00:00
"image" : "{{.CV.Personal.Website}}/static/images/profile.jpg" ,
2025-10-31 11:06:38 +00:00
"email" : "{{.CV.Personal.Email}}" ,
"telephone" : "{{.CV.Personal.Phone}}" ,
2025-11-30 13:23:22 +00:00
"birthDate" : "{{.CV.Personal.DateOfBirth}}" ,
"birthPlace" : {
"@type" : "Place" ,
"name" : "{{.CV.Personal.PlaceOfBirth}}"
} ,
"nationality" : {
"@type" : "Country" ,
"name" : "{{.CV.Personal.Citizenship}}"
} ,
2025-10-31 11:06:38 +00:00
"address" : {
"@type" : "PostalAddress" ,
2025-11-30 13:23:22 +00:00
"addressLocality" : "{{.CV.Personal.Location}}" ,
"addressCountry" : "ES"
2025-10-31 11:06:38 +00:00
} ,
"sameAs" : [
"{{.CV.Personal.LinkedIn}}" ,
"{{.CV.Personal.GitHub}}" ,
2025-11-08 10:23:31 +00:00
"{{.CV.Personal.Domestika}}"
2025-10-31 11:06:38 +00:00
] ,
"alumniOf" : {
2025-11-30 13:23:22 +00:00
"@type" : "CollegeOrUniversity" ,
"name" : "Universidad de Extremadura" ,
"address" : {
"@type" : "PostalAddress" ,
"addressLocality" : "Cáceres" ,
"addressCountry" : "ES"
}
2025-10-31 11:06:38 +00:00
} ,
"knowsAbout" : [
"Web Development" ,
"SAP Customer Data Cloud" ,
"React" ,
"Node.js" ,
"Go" ,
"HTMX" ,
"AI-Assisted Development" ,
2025-11-30 13:23:22 +00:00
"Full Stack Development" ,
"Authentication Systems" ,
"GDPR Compliance" ,
"Identity Management"
2025-10-31 11:06:38 +00:00
] ,
2025-11-30 13:23:22 +00:00
"knowsLanguage" : [
{
"@type" : "Language" ,
"name" : "Spanish" ,
"alternateName" : "es"
} ,
{
"@type" : "Language" ,
"name" : "English" ,
"alternateName" : "en"
} ,
{
"@type" : "Language" ,
"name" : "Portuguese" ,
"alternateName" : "pt"
}
] ,
"worksFor" : [
{
"@type" : "Organization" ,
"name" : "Olympic Broadcasting Services" ,
"url" : "https://www.obs.tv/"
} ,
{
"@type" : "Organization" ,
"name" : "LIV Golf" ,
"url" : "https://www.livgolf.com/"
}
] ,
"hasOccupation" : [
{ { - range $i , $exp : = . CV . Experience } } { { if $i } } , { { end } }
{
"@type" : "Occupation" ,
"name" : "{{$exp.Position}}" ,
"occupationLocation" : {
"@type" : "Place" ,
"name" : "{{$exp.Location}}"
} ,
"description" : "{{$exp.ShortDescription}}" ,
"skills" : "{{range $j, $tech := $exp.Technologies}}{{if $j}}, {{end}}{{$tech}}{{end}}"
}
{ { - end } }
]
}
< / script >
<!-- WebSite Schema -->
< script type = "application/ld+json" >
{
"@context" : "https://schema.org" ,
"@type" : "WebSite" ,
"@id" : "{{.CV.Personal.Website}}/#website" ,
"name" : "{{.CV.Personal.Name}} - Professional CV" ,
"url" : "{{.CV.Personal.Website}}" ,
"description" : "Interactive curriculum vitae of {{.CV.Personal.Name}}, {{.CV.Personal.Title}}" ,
"inLanguage" : [ "en" , "es" ] ,
"author" : {
"@id" : "{{.CV.Personal.Website}}/#person"
} ,
"potentialAction" : {
"@type" : "SearchAction" ,
"target" : "{{.CV.Personal.Website}}/?lang={search_term_string}" ,
"query-input" : "required name=search_term_string"
2025-10-31 11:06:38 +00:00
}
}
< / script >
2025-11-30 13:23:22 +00:00
<!-- BreadcrumbList Schema -->
< script type = "application/ld+json" >
{
"@context" : "https://schema.org" ,
"@type" : "BreadcrumbList" ,
"itemListElement" : [
{
"@type" : "ListItem" ,
"position" : 1 ,
"name" : "Home" ,
"item" : "{{.CV.Personal.Website}}"
} ,
{
"@type" : "ListItem" ,
"position" : 2 ,
"name" : "CV {{if eq .Lang " es "}}(Español){{else}}(English){{end}}" ,
"item" : "{{.CV.Personal.Website}}/?lang={{.Lang}}"
}
]
}
< / script >
<!-- ProfilePage Schema (for CV/Resume) -->
< script type = "application/ld+json" >
{
"@context" : "https://schema.org" ,
"@type" : "ProfilePage" ,
"mainEntity" : {
"@id" : "{{.CV.Personal.Website}}/#person"
} ,
"dateCreated" : "2024-01-01" ,
"dateModified" : "{{.CV.Meta.LastUpdated}}" ,
"name" : "{{.CV.Personal.Name}} - Curriculum Vitae" ,
"description" : "{{.CV.SEO.MetaDescription}}" ,
"inLanguage" : "{{.Lang}}"
}
< / script >
<!-- EducationalOccupationalCredential Schema -->
{{range .CV.Education}}
< script type = "application/ld+json" >
{
"@context" : "https://schema.org" ,
"@type" : "EducationalOccupationalCredential" ,
"name" : "{{.Degree}}" ,
"description" : "{{.Field}}" ,
"educationalLevel" : "Bachelor's Degree" ,
"credentialCategory" : "degree" ,
"recognizedBy" : {
"@type" : "CollegeOrUniversity" ,
"name" : "{{.Institution}}" ,
"address" : {
"@type" : "PostalAddress" ,
"addressLocality" : "{{.Location}}"
}
} ,
"dateCreated" : "{{.EndDate}}"
}
< / script >
{{end}}
<!-- Course Schemas -->
{{range .CV.Courses}}
< script type = "application/ld+json" >
{
"@context" : "https://schema.org" ,
"@type" : "Course" ,
"name" : "{{.Title}}" ,
"description" : "{{if .ShortDescription}}{{.ShortDescription}}{{else}}{{.Description}}{{end}}" ,
"provider" : {
"@type" : "Organization" ,
"name" : "{{.Institution}}"
} ,
"hasCourseInstance" : {
"@type" : "CourseInstance" ,
"courseMode" : "onsite" ,
"location" : {
"@type" : "Place" ,
"name" : "{{.Location}}"
}
} ,
"timeRequired" : "{{.Duration}}"
}
< / script >
{{end}}
2025-10-20 08:54:21 +01:00
< / head >
2025-11-12 23:07:44 +00:00
< body { { if . ThemeClean } } class = "theme-clean" { { end } }
2025-11-30 04:35:16 +00:00
_ = "on load call initScrollBehavior()
on scroll from window call handleScroll()
on keydown
2025-11-30 05:58:44 +00:00
set tag to event.target.tagName
2025-12-01 13:03:06 +00:00
set ninjaKeys to document.getElementById('cmd-k-bar')
set ninjaOpen to (ninjaKeys is not null and ninjaKeys.opened)
set skip to (tag is 'INPUT' or tag is 'TEXTAREA' or ninjaOpen)
2025-11-30 05:58:44 +00:00
set noMod to (not event.ctrlKey and not event.metaKey and not event.altKey)
if event.key is '?' and noMod and not skip then halt the event then call openModalShortcut('shortcuts-modal') end
if (event.key is 'l' or event.key is 'L') and noMod and not skip then halt the event then call handleToggleShortcut('lengthToggle', 'lengthToggleMenu') end
if (event.key is 'i' or event.key is 'I') and noMod and not skip then halt the event then call handleToggleShortcut('iconToggle', 'iconToggleMenu') end
if (event.key is 'v' or event.key is 'V') and noMod and not skip then halt the event then call handleToggleShortcut('themeToggle', 'themeToggleMenu') end
2025-11-15 15:59:54 +00:00
end" >
2025-11-12 19:54:56 +00:00
<!-- Top anchor for back - to - top link -->
< div id = "top" > < / div >
2025-11-12 18:26:18 +00:00
{{template "action-bar" .}}
{{template "hamburger-menu" .}}
2025-11-18 15:49:30 +00:00
{{template "color-theme-switcher" .}}
2025-11-07 19:11:21 +00:00
2025-11-12 15:24:09 +00:00
<!-- Zoom Wrapper (for zoom functionality) -->
< div id = "zoom-wrapper" class = "zoom-wrapper" >
<!-- CV Content Container -->
< div class = "cv-container" >
2025-11-12 18:55:06 +00:00
{{template "cv-content.html" .}}
2025-11-09 19:30:05 +00:00
< / div >
2025-11-12 18:26:18 +00:00
< / div > <!-- End zoom - wrapper -->
2025-11-12 11:00:29 +00:00
2025-11-17 17:10:46 +00:00
{{template "page-footer" .}}
2025-11-12 18:26:18 +00:00
{{template "error-toast" .}}
2025-11-20 13:00:06 +00:00
{{template "pdf-toast" .}}
2025-11-23 08:21:12 +00:00
<!-- iOS - style blur backdrop for mobile buttons -->
< div class = "fixed-buttons-backdrop no-print" > < / div >
2025-11-12 18:26:18 +00:00
{{template "back-to-top" .}}
2025-11-17 08:34:50 +00:00
{{template "info-button" .}}
{{template "download-button" .}}
{{template "print-friendly-button" .}}
2025-12-01 13:03:06 +00:00
{{template "contact-button" .}}
2025-11-17 08:34:50 +00:00
{{template "zoom-toggle-button" .}}
{{template "shortcuts-button" .}}
2025-12-01 13:03:06 +00:00
{{template "cmd-k-button" .}}
2025-11-12 18:26:18 +00:00
{{template "info-modal" .}}
2025-11-15 15:59:54 +00:00
{{template "shortcuts-modal" .}}
2025-11-12 18:26:18 +00:00
{{template "pdf-modal" .}}
2025-11-30 14:31:58 +00:00
{{template "contact-modal" .}}
2025-11-12 18:26:18 +00:00
{{template "zoom-control" .}}
2025-11-12 11:00:29 +00:00
2025-12-01 13:03:06 +00:00
<!-- CMD+K Command Bar -->
< ninja-keys id = "cmd-k-bar" placeholder = "Type a command or search..." hideBreadcrumbs openHotkey = "cmd+k,ctrl+k" > < / ninja-keys >
2025-11-11 21:43:12 +00:00
<!-- External JavaScript - CSP Compliant -->
< script src = "/static/js/main.js" > < / script >
2025-12-01 13:03:06 +00:00
< script src = "/static/js/ninja-keys-init.js" > < / script >
2025-11-09 15:02:31 +00:00
2025-11-20 16:17:56 +00:00
<!-- Matomo Analytics - First - party subdomain to bypass ad blockers -->
2025-11-18 19:47:38 +00:00
< script >
2025-11-09 15:02:31 +00:00
var _paq = window . _paq = window . _paq || [ ] ;
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq . push ( [ 'trackPageView' ] ) ;
_paq . push ( [ 'enableLinkTracking' ] ) ;
( function ( ) {
2025-11-20 16:17:56 +00:00
var u = "https://matomo.morenorub.io/" ;
2025-11-09 15:02:31 +00:00
_paq . push ( [ 'setTrackerUrl' , u + 'matomo.php' ] ) ;
_paq . push ( [ 'setSiteId' , '4' ] ) ;
var d = document , g = d . createElement ( 'script' ) , s = d . getElementsByTagName ( 'script' ) [ 0 ] ;
2025-11-20 16:17:56 +00:00
g . async = true ;
g . src = u + 'matomo.js' ;
s . parentNode . insertBefore ( g , s ) ;
2025-11-09 15:02:31 +00:00
} ) ( ) ;
2025-10-27 22:32:32 +00:00
< / script >
2025-11-09 15:02:31 +00:00
<!-- End Matomo Code -->
2025-10-20 08:54:21 +01:00
< / body >
< / html >