Files
dylanmazzei-website/src/layout.mjs
T
Alejandro Veras 2d37e3fcce Use Dylan's DM logo, switch accent to brand blue, add dark mode
Recreate the DM monogram and wordmark as SVG and type so they stay sharp in
both themes, move the accent from violet to the logo's blue, and add a small
theme toggle in the nav that follows the system until the visitor picks one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:25:52 -04:00

241 lines
13 KiB
JavaScript

// Shared layout, icons and small HTML helpers for every page.
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/caposins',
// Replace with the real scheduling link (Calendly, Cal.com, etc.).
calendar: 'mailto:dylan@dylanmazzei.com?subject=Scheduling%20a%20conversation',
};
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"';
return `<svg class="${cls}" 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 = () => `${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} ${icon('arrow')}</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: 'Press', href: '/press/' },
];
function nav(path) {
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">
<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</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>
<a class="btn btn-primary" href="/contact/">START A CONVERSATION ${icon('arrow')}</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>
</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>© ${new Date().getFullYear()} Dylan Mazzei</small>
</div>
</div>
<div class="foot-word" aria-hidden="true">DYLAN MAZZEI</div>
</footer>`;
}
export function page({ path, title, description, body, schema = [] }) {
const canonical = SITE.url + path;
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="en">
<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}">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0b0b0d" media="(prefers-color-scheme: dark)">
<meta name="color-scheme" content="light dark">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Dylan Mazzei">
<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=Inter:wght@500;700&family=JetBrains+Mono:wght@500&family=Unbounded:wght@700&display=swap">
<link rel="stylesheet" href="/assets/styles.css">
<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')&&window.matchMedia&&matchMedia('(prefers-color-scheme: dark)').matches)r.setAttribute('data-theme','dark')})();document.documentElement.classList.add('js');setTimeout(function(){if(!window.__site)document.documentElement.classList.remove('js')},2500)</script>
${ld}
</head>
<body>
${nav(path)}
<main id="main">
${body}
</main>
${footer()}
<script src="/assets/site.js" defer></script>
</body>
</html>
`;
}