Files
dylanmazzei-website/src/layout.mjs
T
Alejandro Veras eb9137b300 Add Spanish and Arabic versions with a language switch
- English stays at /, Spanish at /es/ and Arabic at /ar/ (right to left)
- EN · ES · AR switch in the header and mobile menu
- Pages are written in English; the build translates them through
  src/i18n/es.json and src/i18n/ar.json, including browser-script strings
- rtl.css mirrors the layout and loads Arabic fonts on /ar/ pages
- hreflang alternates on every page and a multilingual sitemap
- scripts/i18n-extract.mjs lists strings that still need translating

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:18:11 -04:00

287 lines
17 KiB
JavaScript
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.
// Shared layout, icons and small HTML helpers for every page.
import { LOCALES, LOCALE_NAMES, LOCALE_SHORT, OG_LOCALE, dirOf, localize } from './i18n.mjs';
export const SITE = {
url: 'https://dylanmazzei.com',
email: 'dylan@dylanmazzei.com',
pressEmail: 'press@dylanmazzei.com',
linkedin: 'https://www.linkedin.com/in/dylanmazzei/',
instagram: 'https://www.instagram.com/thedylanmazzei',
// Replace with the real scheduling link (Calendly, Cal.com, etc.).
calendar: '/contact/book/',
// GoHighLevel (LeadConnector) widgets. Contacts land in Dylan's GHL account.
ghl: {
form: 'xm10A7alZrxeKDtkMkD2', // "Dylan – Lead Form"
freeCalendar: '81JqpCRit6APOwavApzh', // Free 15-min intro call with Dylan's team
vipCalendar: 'lK9IYtCRGIuA7W93WjMF', // 15-min 1:1 with Dylan ($100)
},
};
const ICONS = {
arrow: '<path d="M3 8h10M9 4l4 4-4 4" stroke-width="1.75"/>',
'arrow-ur': '<path d="M5 11 11 5M6 5h5v5" stroke-width="1.75"/>',
check: '<path d="m3.5 8.5 3 3 6-6.5" stroke-width="1.75"/>',
plus: '<path d="M8 3v10M3 8h10" stroke-width="1.75"/>',
chev: '<path d="m4 6 4 4 4-4" stroke-width="1.75"/>',
menu: '<path d="M2 5h14M2 9h14M2 13h14" stroke-width="1.75"/>',
close: '<path d="M4 4l10 10M14 4 4 14" stroke-width="1.75"/>',
brace: '<path d="M6 2.5H5a1.5 1.5 0 0 0-1.5 1.5v2.3c0 .8-.6 1.7-1.5 1.7.9 0 1.5.9 1.5 1.7V12A1.5 1.5 0 0 0 5 13.5h1M10 2.5h1A1.5 1.5 0 0 1 12.5 4v2.3c0 .8.6 1.7 1.5 1.7-.9 0-1.5.9-1.5 1.7V12a1.5 1.5 0 0 1-1.5 1.5h-1" stroke-width="1.5"/>',
search: '<circle cx="10.5" cy="10.5" r="6.5"/><path d="m20 20-4.8-4.8M10.5 7.5v6M7.5 10.5h6"/>',
chip: '<rect x="6" y="6" width="12" height="12" rx="2.5"/><path d="M9.5 9.5h5v5h-5zM9 3v3M15 3v3M9 18v3M15 18v3M3 9h3M3 15h3M18 9h3M18 15h3"/>',
bottle: '<path d="M10 2.5h4M10.5 2.5v4.2L8.6 9.4a3 3 0 0 0-.6 1.8V20a1.5 1.5 0 0 0 1.5 1.5h5A1.5 1.5 0 0 0 16 20v-8.8a3 3 0 0 0-.6-1.8l-1.9-2.7V2.5"/><path d="M8 13.5h8v4H8z"/>',
edu: '<path d="m2.5 9 9.5-4.5L21.5 9 12 13.5z"/><path d="M6.5 11v4.5c0 1.6 2.5 3 5.5 3s5.5-1.4 5.5-3V11M21.5 9v5"/>',
shield: '<path d="M12 2.8 19.5 6v5.4c0 4.6-3.1 8.2-7.5 9.8-4.4-1.6-7.5-5.2-7.5-9.8V6z"/><path d="m8.8 12 2.3 2.3 4.3-4.6"/>',
bag: '<path d="M4.5 8h15l-1.2 12.1a1.5 1.5 0 0 1-1.5 1.4H7.2a1.5 1.5 0 0 1-1.5-1.4z"/><path d="M8.5 10.5V7a3.5 3.5 0 0 1 7 0v3.5"/>',
umbrella: '<path d="M3 12a9 9 0 0 1 18 0z"/><path d="M12 12v6.5a2 2 0 0 1-4 0M12 3v-.5"/>',
megaphone: '<path d="M3.5 10v4a1 1 0 0 0 1 1H7l9 5V4L7 9H4.5a1 1 0 0 0-1 1z"/><path d="M7 15l1.5 5M19.5 9.5a3.5 3.5 0 0 1 0 5"/>',
heart: '<path d="M12 20.5s-8-4.6-8-10.7A4.3 4.3 0 0 1 12 7.4a4.3 4.3 0 0 1 8 2.4c0 6.1-8 10.7-8 10.7z"/><path d="M8.5 12h2l1-2 1.5 4 1-2h2"/>',
shirt: '<path d="M9 3.5 4 5.8 2.5 10.5l3.3 1.3V20.5h12.4v-8.7l3.3-1.3L20 5.8l-5-2.3c-.4 1.4-1.6 2.3-3 2.3s-2.6-.9-3-2.3z"/>',
building: '<path d="M4 21V5a1.5 1.5 0 0 1 1.5-1.5h8A1.5 1.5 0 0 1 15 5v16M15 9h3.5A1.5 1.5 0 0 1 20 10.5V21M2.5 21h19M7.5 7.5h4M7.5 11h4M7.5 14.5h4"/>',
gauge: '<path d="M4 17a8 8 0 1 1 16 0"/><path d="m12 17 4-5M4 17h2M18 17h2M12 9V7.5"/>',
truck: '<path d="M2.5 6.5h11v10h-11zM13.5 10h4l3 3.2v3.3h-7"/><circle cx="6.5" cy="17.5" r="1.8"/><circle cx="17" cy="17.5" r="1.8"/>',
compass: '<circle cx="12" cy="12" r="9"/><path d="m15.5 8.5-2 5-5 2 2-5z"/>',
target: '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.2"/>',
users: '<circle cx="9" cy="8.5" r="3.5"/><path d="M2.5 20c.6-3.4 3.3-5.5 6.5-5.5s5.9 2.1 6.5 5.5"/><path d="M15.5 5.2a3.5 3.5 0 0 1 0 6.6M17.5 14.8c2.1.6 3.6 2.4 4 5.2"/>',
chart: '<path d="M3.5 20.5h17"/><path d="M6.5 16.5v-4M11 16.5V8M15.5 16.5v-6M20 16.5V5"/>',
layers: '<path d="m12 3 9 4.8-9 4.8-9-4.8z"/><path d="m3 12.2 9 4.8 9-4.8M3 16.4l9 4.8 9-4.8"/>',
mic: '<rect x="9" y="2.5" width="6" height="11" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5v4M8.5 21.5h7"/>',
rocket: '<path d="M12 15.5 8.5 12c1.5-4.6 4.6-8.3 11-8.5-.2 6.4-3.9 9.5-8.5 11z"/><path d="M8.5 12 5 11.5l2.5-3h4M12 15.5l.5 3.5 3-2.5v-4"/><path d="M6.5 16c-1.6.6-2.4 2.3-2.5 4 1.7-.1 3.4-.9 4-2.5"/>',
route: '<circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="6" r="2.5"/><path d="M8.5 18H16a3 3 0 0 0 0-6H8a3 3 0 0 1 0-6h7.5"/>',
mail: '<rect x="3" y="5" width="18" height="14" rx="2.5"/><path d="m3.5 6.5 8.5 6.5 8.5-6.5"/>',
calendar: '<rect x="3.5" y="5" width="17" height="15.5" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4"/>',
pin: '<path d="M12 21s7-6.2 7-11.5a7 7 0 0 0-14 0C5 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/>',
book: '<path d="M4 4.5A1.5 1.5 0 0 1 5.5 3H19v15H5.5A1.5 1.5 0 0 0 4 19.5z"/><path d="M4 19.5A1.5 1.5 0 0 0 5.5 21H19v-3"/>',
pen: '<path d="M4 20l1-4.5L15.5 5a2.1 2.1 0 0 1 3 3L8 18.5z"/><path d="m13.5 7 3 3"/>',
moon: '<path d="M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5z"/>',
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.3 5.3l1.4 1.4M17.3 17.3l1.4 1.4M18.7 5.3l-1.4 1.4M6.7 17.3l-1.4 1.4"/>',
spark: '<path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8"/>',
};
const SMALL = new Set(['arrow', 'arrow-ur', 'check', 'plus', 'chev', 'brace']);
export function icon(name, cls = '') {
const vb = SMALL.has(name) ? '0 0 16 16' : name === 'menu' || name === 'close' ? '0 0 18 18' : '0 0 24 24';
const sw = SMALL.has(name) || name === 'menu' || name === 'close' ? '' : ' stroke-width="1.75"';
// Arrows point the other way on right-to-left pages (rtl.css).
const c = name.startsWith('arrow') ? (cls ? cls + ' flip' : 'flip') : cls;
return `<svg class="${c}" viewBox="${vb}" fill="none" stroke="currentColor"${sw} stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS[name]}</svg>`;
}
// Dylan Mazzei monogram: italic "D" (inherits text color) and the sky-blue blade.
export const mark = (cls = 'logo-mark') => `<svg class="${cls}" viewBox="0 0 104 60" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M9 4h55c10 0 13 7 9 14L58 46c-3 6-8 8-15 8.5L3 57zM21.5 15.5 18.3 45.8 40 44.5c4-.3 6-1.5 7.5-4.3l12-21.4c1.2-2.2.3-3.3-2-3.3z"/><path fill="#33b9ff" d="M24 59 102 1 91 52 72 54.5 79 33z"/><path fill="#8fdcff" d="M24 59 102 1 79 33z"/></svg>`;
export const logo = () => `<img class="logo-img" src="/assets/o2/logo.png" alt="" width="640" height="121">${mark()}<span class="brand-word">Dylan <b>Mazzei</b></span>`;
export const eyebrow = (t) => `<span class="eyebrow">${t}</span>`;
export function btn(text, href, variant = 'primary', extra = '') {
const ext = /^https?:/.test(href) ? ' target="_blank" rel="noopener"' : '';
return `<a class="btn btn-${variant}${extra ? ' ' + extra : ''}" href="${href}"${ext}>${text}<span class="go">${icon('arrow')}</span></a>`;
}
export function textLink(text, href) {
const ext = /^https?:/.test(href) ? ' target="_blank" rel="noopener"' : '';
return `<a class="text-link" href="${href}"${ext}>${text} ${icon('arrow')}</a>`;
}
export const paras = (arr, cls = '') => arr.map((t) => `<p${cls ? ` class="${cls}"` : ''}>${t}</p>`).join('');
export function faq(items) {
return `<div class="faq">${items
.map(
([q, a], i) => `<details${i === 0 ? ' open' : ''}><summary>${q}<span class="pm">${icon('plus')}</span></summary><div class="answer">${paras(a)}</div></details>`
)
.join('')}</div>`;
}
const NAV = [
{ label: 'Portfolio', href: '/portfolio/' },
{ label: 'About', href: '/about/' },
{
label: 'Consulting',
href: '/consulting/',
children: [
{ label: 'Consulting', desc: 'Find the Real Problem. Then Fix the Right One.', href: '/consulting/', icon: 'compass' },
{ label: 'Marketing & AI Search', desc: 'Get Found Where Attention Is Moving.', href: '/consulting/marketing/', icon: 'search' },
{ label: 'Business Setup', desc: 'Start With Fewer Expensive Mistakes.', href: '/consulting/business-setup/', icon: 'building' },
{ label: 'Business Optimization', desc: 'Find What Growth Made Messy.', href: '/consulting/business-optimization/', icon: 'gauge' },
{ label: 'Supply Chain', desc: 'Know What Happens Before It Reaches the Customer.', href: '/consulting/supply-chain/', icon: 'truck' },
{ label: 'Cybersecurity', desc: 'Find the Exposure Before Someone Else Does.', href: '/consulting/cybersecurity/', icon: 'shield' },
{ label: 'Education', desc: 'Make the Strategy Survive the Classroom.', href: '/consulting/education/', icon: 'edu' },
{ label: 'Fractional Leadership', desc: 'When the Business Needs an Owner, Not Another Recommendation.', href: '/consulting/fractional/', icon: 'users' },
],
},
{ label: 'Press', href: '/press/' },
];
// EN · ES · AR. Plain links: each language is its own static page.
function langSwitch(path, locale) {
return `<nav class="lang-switch" aria-label="Language">${LOCALES.map((l) =>
l === locale
? `<span aria-current="true" lang="${l}" title="${LOCALE_NAMES[l]}">${LOCALE_SHORT[l]}</span>`
: `<a href="${localize(path, l)}" hreflang="${l}" lang="${l}" title="${LOCALE_NAMES[l]}">${LOCALE_SHORT[l]}</a>`
).join('')}</nav>`;
}
function nav(path, locale) {
const isActive = (href) => (href === '/' ? path === '/' : path.startsWith(href));
const desktop = NAV.map((item) => {
if (!item.children) {
return `<li><a href="${item.href}"${isActive(item.href) ? ' aria-current="page"' : ''}>${item.label}</a></li>`;
}
return `<li class="has-drop${isActive(item.href) ? ' is-active' : ''}">
<button type="button" aria-haspopup="true">${item.label} ${icon('chev', 'chev')}</button>
<div class="dropdown">${item.children
.map((c) => `<a href="${c.href}"><span class="di">${icon(c.icon)}</span><span><strong>${c.label}</strong><small>${c.desc}</small></span></a>`)
.join('')}</div>
</li>`;
}).join('');
const mobile = NAV.map((item) => {
const top = `<li><a href="${item.href}">${item.label} ${icon('arrow', 'check')}</a></li>`;
if (!item.children) return top;
return top + item.children.slice(1).map((c) => `<li class="sub"><a href="${c.href}">${c.label}</a></li>`).join('');
}).join('');
return `<a class="skip" href="#main">Skip to content</a>
<header class="nav" id="nav">
<div class="wrap nav-inner">
<a class="brand" href="/" aria-label="Dylan Mazzei, home">${logo()}</a>
<nav aria-label="Primary"><ul class="nav-links">${desktop}</ul></nav>
<div class="nav-cta">
${langSwitch(path, locale)}
<button class="theme-btn" id="theme-btn" type="button" aria-label="Switch to dark mode">${icon('moon', 'moon')}${icon('sun', 'sun')}</button>
<a class="btn btn-primary btn-sm" href="/contact/">START A CONVERSATION<span class="go">${icon('arrow')}</span></a>
<button class="menu-btn" id="menu-btn" type="button" aria-expanded="false" aria-controls="mobile-menu" aria-label="Open menu">${icon('menu')}</button>
</div>
</div>
<div class="wrap mobile-menu" id="mobile-menu" hidden>
<ul>${mobile}</ul>
${langSwitch(path, locale)}
<a class="btn btn-primary" href="/contact/">START A CONVERSATION<span class="go">${icon('arrow')}</span></a>
</div>
</header>`;
}
function footer() {
return `<footer class="footer">
<div class="wrap">
<div class="foot-top">
<div class="foot-intro">
<a class="brand" href="/" aria-label="Dylan Mazzei, home">${logo()}</a>
<p>DYLAN MAZZEI | FOUNDER &amp; OPERATOR</p>
</div>
<div class="foot-col">
<h4>Explore</h4>
<ul>
<li><a href="/portfolio/">Portfolio</a></li>
<li><a href="/about/">About</a></li>
<li><a href="/press/">Press &amp; Media</a></li>
<li><a href="/contact/">Contact</a></li>
</ul>
</div>
<div class="foot-col">
<h4>Consulting</h4>
<ul>
<li><a href="/consulting/">Consulting</a></li>
<li><a href="/consulting/marketing/">Marketing &amp; AI Search</a></li>
<li><a href="/consulting/business-setup/">Business Setup</a></li>
<li><a href="/consulting/business-optimization/">Business Optimization</a></li>
<li><a href="/consulting/supply-chain/">Supply Chain</a></li>
<li><a href="/consulting/cybersecurity/">Cybersecurity</a></li>
<li><a href="/consulting/education/">Education</a></li>
<li><a href="/consulting/fractional/">Fractional Leadership</a></li>
</ul>
</div>
<div class="foot-col">
<h4>Direct Email</h4>
<ul>
<li><a href="mailto:${SITE.email}">${SITE.email}</a></li>
<li><a href="mailto:${SITE.pressEmail}">${SITE.pressEmail}</a></li>
<li><a href="${SITE.linkedin}" target="_blank" rel="noopener">LinkedIn</a></li>
<li><a href="${SITE.instagram}" target="_blank" rel="noopener">Instagram</a></li>
</ul>
</div>
</div>
<div class="foot-big">
<div class="stack-xs">
<span class="eyebrow">WHERE I WORK</span>
<p class="places">Texas<i>•</i>Florida<i>•</i>New York<i>•</i>Dubai</p>
</div>
<small>© <span>${new Date().getFullYear()}</span> Dylan Mazzei</small>
</div>
</div>
<div class="foot-word" aria-hidden="true">DYLAN MAZZEI</div>
</footer>`;
}
export function page({ path, title, description, body, schema = [], head = '', scripts = '', noindex = false }, locale = 'en') {
const canonical = SITE.url + localize(path, locale);
const alternates = noindex ? '' : [...LOCALES.map((l) => [l, l]), ['x-default', 'en']]
.map(([hl, l]) => `\n<link rel="alternate" hreflang="${hl}" href="${SITE.url + localize(path, l)}">`).join('');
const person = {
'@context': 'https://schema.org',
'@type': 'Person',
'@id': SITE.url + '/#dylan',
name: 'Dylan Mazzei',
url: SITE.url,
email: SITE.email,
image: SITE.url + '/assets/dylan-portrait.jpg',
jobTitle: 'Founder & Operator',
sameAs: [SITE.linkedin, SITE.instagram],
};
const ld = [person, ...schema].map((s) => `<script type="application/ld+json">${JSON.stringify(s)}</script>`).join('\n');
return `<!doctype html>
<html lang="${locale}" dir="${dirOf(locale)}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>${title}</title>
<meta name="description" content="${description}">
<link rel="canonical" href="${canonical}">${alternates}${noindex ? '\n<meta name="robots" content="noindex">' : ''}
<meta name="theme-color" content="#04060c">
<meta name="color-scheme" content="light dark">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Dylan Mazzei">
<meta property="og:locale" content="${OG_LOCALE[locale]}">
<meta property="og:title" content="${title}">
<meta property="og:description" content="${description}">
<meta property="og:url" content="${canonical}">
<meta property="og:image" content="${SITE.url}/assets/og.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${title}">
<meta name="twitter:description" content="${description}">
<meta name="twitter:image" content="${SITE.url}/assets/og.jpg">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500&family=Unbounded:wght@700&display=swap">
<link rel="preload" as="font" type="font/woff2" crossorigin href="/assets/fonts/inter-tight-latin-500-normal.woff2">
<link rel="stylesheet" href="/assets/fonts.css">
<link rel="stylesheet" href="/assets/styles.css">${locale === 'ar' ? '\n<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Kufi+Arabic:wght@600;700&family=Noto+Sans+Arabic:wght@400;500;600;700&display=swap">\n<link rel="stylesheet" href="/assets/rtl.css">' : ''}
${head}
<script>(function(){var r=document.documentElement;try{var t=localStorage.getItem('theme');if(t==='dark'||t==='light')r.setAttribute('data-theme',t)}catch(e){}if(!r.hasAttribute('data-theme'))r.setAttribute('data-theme','dark')})();document.documentElement.classList.add('js');setTimeout(function(){if(!window.__site)document.documentElement.classList.remove('js')},2500)</script>
<script>window.__i18n={}</script>
${ld}
</head>
<body>
<div class="bgfx" aria-hidden="true"></div>
<div class="progress" id="progress" aria-hidden="true"></div>
${nav(path, locale)}
<main id="main">
${body}
</main>
${footer()}
<script src="/assets/site.js" defer></script>
${scripts}
</body>
</html>
`;
}
// GoHighLevel embeds. form_embed.js resizes the iframes to their content.
export const GHL_SCRIPT = '<script src="https://link.msgsndr.com/js/form_embed.js" defer></script>';
export const ghlForm = (id, name) =>
`<iframe class="ghl-frame" src="https://api.leadconnectorhq.com/widget/form/${id}" id="inline-${id}" title="${name}" data-layout="{'id':'INLINE'}" data-trigger-type="alwaysShow" data-trigger-value="" data-activation-type="alwaysActivated" data-activation-value="" data-deactivation-type="neverDeactivate" data-deactivation-value="" data-form-name="${name}" data-layout-iframe-id="inline-${id}" data-form-id="${id}"></iframe>`;
export const ghlCalendar = (id, title) =>
`<iframe class="ghl-frame ghl-cal" src="https://api.leadconnectorhq.com/widget/booking/${id}" id="${id}_cal" title="${title}" scrolling="no"></iframe>`;