Embed the GoHighLevel lead form and booking calendars
/contact/ now uses the GHL "Dylan – Lead Form", and a new /contact/book/ page lets visitors pick the free intro call or the paid 1:1 and book it in the matching GHL calendar. Both calendars load up front so switching tabs is instant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ build.mjs Genera dist/ + sitemap.xml + robots.txt + 404.html
|
||||
| `/consulting/education/` | `src/pages/education.mjs` |
|
||||
| `/consulting/fractional/` | `src/pages/fractional.mjs` |
|
||||
| `/contact/` | `src/pages/contact.mjs` |
|
||||
| `/contact/book/` | `src/pages/book.mjs` |
|
||||
| `/press/` | `src/pages/press.mjs` |
|
||||
|
||||
## Desarrollo
|
||||
@@ -44,5 +45,4 @@ Importa el repo en Vercel. `vercel.json` ya define el build (`npm run build`), l
|
||||
- **Press:** falta el link del artículo de Gulf Insider y confirmar su titular exacto (`FEATURE` en `src/pages/press.mjs`).
|
||||
- **Korrith:** falta el cargo de Dylan (el tercer campo, vacío, en `src/pages/portfolio.mjs`).
|
||||
- **Páginas fuera del alcance actual:** Playbook y Poetry no forman parte del sitio. Sus secciones se muestran sin botón; si se crean esas páginas, se añade el botón con su texto original del documento.
|
||||
- **Calendario:** poner el link real de agenda en `SITE.calendar`.
|
||||
- **Formulario de contacto:** hoy abre el cliente de correo con el mensaje prellenado. Para recibirlo directo hay que conectar un servicio (Formspree, Resend, etc.).
|
||||
- **Formulario y agenda (GoHighLevel):** los IDs están en `SITE.ghl` (`src/layout.mjs`). En GoHighLevel, el formulario "Dylan – Lead Form" debe redirigir a `https://dylanmazzei.com/contact/book/` al enviarse.
|
||||
|
||||
@@ -7,7 +7,7 @@ import { page, eyebrow, btn } from './src/layout.mjs';
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url));
|
||||
const out = join(root, 'dist');
|
||||
const pages = ['home', 'about', 'portfolio', 'consulting', 'marketing', 'business-setup', 'business-optimization', 'supply-chain', 'cybersecurity', 'education', 'fractional', 'contact', 'press'];
|
||||
const pages = ['home', 'about', 'portfolio', 'consulting', 'marketing', 'business-setup', 'business-optimization', 'supply-chain', 'cybersecurity', 'education', 'fractional', 'contact', 'book', 'press'];
|
||||
|
||||
await rm(out, { recursive: true, force: true });
|
||||
await mkdir(out, { recursive: true });
|
||||
@@ -15,7 +15,7 @@ await cp(join(root, 'static'), out, { recursive: true });
|
||||
|
||||
// Fingerprint CSS/JS so a new deploy never reuses a stale cached copy.
|
||||
const hashOf = async (f) => createHash('sha256').update(await readFile(join(root, 'static', f))).digest('hex').slice(0, 10);
|
||||
const assetVersions = Object.fromEntries(await Promise.all(['styles.css', 'site.js', 'home.css', 'home.js', 'fonts.css'].map(async (f) => ['/assets/' + f, await hashOf('assets/' + f)])));
|
||||
const assetVersions = Object.fromEntries(await Promise.all(['styles.css', 'site.js', 'home.css', 'home.js', 'fonts.css', 'book.js'].map(async (f) => ['/assets/' + f, await hashOf('assets/' + f)])));
|
||||
const fingerprint = (html) => Object.entries(assetVersions).reduce((h, [u, v]) => h.replaceAll(`"${u}"`, `"${u}?v=${v}"`), html);
|
||||
|
||||
const urls = [];
|
||||
|
||||
+16
-3
@@ -7,7 +7,13 @@ export const SITE = {
|
||||
linkedin: 'https://www.linkedin.com/in/dylanmazzei/',
|
||||
instagram: 'https://www.instagram.com/thedylanmazzei',
|
||||
// Replace with the real scheduling link (Calendly, Cal.com, etc.).
|
||||
calendar: 'mailto:dylan@dylanmazzei.com?subject=Scheduling%20a%20conversation',
|
||||
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 = {
|
||||
@@ -194,7 +200,7 @@ function footer() {
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
export function page({ path, title, description, body, schema = [], head = '', scripts = '' }) {
|
||||
export function page({ path, title, description, body, schema = [], head = '', scripts = '', noindex = false }) {
|
||||
const canonical = SITE.url + path;
|
||||
const person = {
|
||||
'@context': 'https://schema.org',
|
||||
@@ -215,7 +221,7 @@ export function page({ path, title, description, body, schema = [], head = '', s
|
||||
<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}">
|
||||
<link rel="canonical" href="${canonical}">${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">
|
||||
@@ -253,3 +259,10 @@ ${scripts}
|
||||
</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}" loading="lazy"></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>`;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { icon, eyebrow, SITE, ghlCalendar, GHL_SCRIPT } from '../layout.mjs';
|
||||
|
||||
// Step 2 of the GoHighLevel booking flow. The lead form redirects here after submit.
|
||||
// Copy mirrors the "Book Your Call" step Gabriela built in GoHighLevel.
|
||||
const calls = [
|
||||
{ id: 'free', tag: 'FREE', h: 'Free 15-min intro call with Dylan’s team', p: 'Discover how we can support your business goals and see if we are the right match for your journey.', cta: 'Book free call', cal: SITE.ghl.freeCalendar, icon: 'users' },
|
||||
{ id: 'vip', tag: 'VIP', h: '15-min 1:1 with Dylan ($100)', p: 'Get direct strategic direction, tactical feedback on your bottlenecks, and bespoke 1:1 guidance.', cta: 'Book with Dylan', cal: SITE.ghl.vipCalendar, icon: 'spark', featured: true },
|
||||
];
|
||||
|
||||
export default {
|
||||
path: '/contact/book/',
|
||||
title: 'Book Your Call | Dylan Mazzei',
|
||||
description: 'Book a free 15-minute intro call with Dylan Mazzei’s team or a 15-minute 1:1 session with Dylan.',
|
||||
scripts: GHL_SCRIPT + '\n<script src="/assets/book.js" defer></script>',
|
||||
body: `
|
||||
<section class="page-hero" aria-labelledby="bk-hero">
|
||||
<div class="wrap">
|
||||
<div class="stack center anim" style="align-items:center;text-align:center;max-width:760px;margin:0 auto">
|
||||
<nav class="crumbs" aria-label="Breadcrumb"><a href="/contact/">Contact</a>${icon('chev')}<span>Book your call</span></nav>
|
||||
${eyebrow('BOOK YOUR CALL')}
|
||||
<h1 id="bk-hero" class="h-display-lg">Thanks! Now choose your call.</h1>
|
||||
<p class="lead">Pick the option that fits you best.</p>
|
||||
</div>
|
||||
<div class="calls anim" role="tablist" aria-label="Call options">
|
||||
${calls
|
||||
.map(
|
||||
(c, i) => `<button class="call${c.featured ? ' call-vip' : ''}" type="button" role="tab" id="tab-${c.id}" aria-controls="cal-${c.id}" aria-selected="${i === 0}" data-call="${c.id}">
|
||||
<span class="call-top"><span class="tag ${c.featured ? 'tag-accent' : 'tag-white'}">${c.tag}</span><span class="ic">${icon(c.icon)}</span></span>
|
||||
<span class="call-h">${c.h}</span>
|
||||
<span class="call-p">${c.p}</span>
|
||||
<span class="btn ${c.featured ? 'btn-primary' : 'btn-ghost'} btn-sm">${c.cta}<span class="go">${icon('arrow')}</span></span>
|
||||
</button>`
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="schedule" aria-label="Choose a time" style="padding-top:0">
|
||||
<div class="wrap">
|
||||
${calls
|
||||
.map(
|
||||
(c, i) => `<div class="panel cal-panel" role="tabpanel" id="cal-${c.id}" aria-labelledby="tab-${c.id}"${i ? ' data-off' : ''}>
|
||||
<div class="panel-bar"><span class="lights" aria-hidden="true"><i></i><i></i><i></i></span><strong>${c.h}</strong></div>
|
||||
<div class="ghl-wrap ghl-wrap-cal">${ghlCalendar(c.cal, c.h)}</div>
|
||||
</div>`
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
};
|
||||
+5
-21
@@ -1,8 +1,9 @@
|
||||
import { icon, eyebrow, btn, SITE } from '../layout.mjs';
|
||||
import { icon, eyebrow, btn, SITE, ghlForm, GHL_SCRIPT } from '../layout.mjs';
|
||||
|
||||
export default {
|
||||
path: '/contact/',
|
||||
title: 'Contact Dylan Mazzei | Business & Partnerships',
|
||||
scripts: GHL_SCRIPT,
|
||||
description: 'Contact Dylan Mazzei about business opportunities, strategic partnerships, consulting, media inquiries, or other conversations worth having.',
|
||||
body: `
|
||||
<section class="page-hero" aria-labelledby="ct-hero">
|
||||
@@ -28,31 +29,14 @@ export default {
|
||||
</div>
|
||||
|
||||
<div class="stack anim">
|
||||
<form class="panel form-card" id="inquiry" data-to="${SITE.email}" novalidate>
|
||||
<div class="panel form-card ghl-card" id="inquiry">
|
||||
<div class="panel-bar"><span class="lights" aria-hidden="true"><i></i><i></i><i></i></span><span class="panel-url">${SITE.email}</span></div>
|
||||
<div class="stack-xs">
|
||||
<h2 class="h-md">Send an Inquiry</h2>
|
||||
<p class="body">Give me enough context to understand what you’re working on, where things stand, and what you’d like to discuss.</p>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="field"><label for="f-name">Name</label><input id="f-name" name="name" type="text" autocomplete="name" required></div>
|
||||
<div class="field"><label for="f-email">Email</label><input id="f-email" name="email" type="email" autocomplete="email" required></div>
|
||||
<div class="field"><label for="f-company">Company</label><input id="f-company" name="company" type="text" autocomplete="organization"></div>
|
||||
<div class="field"><label for="f-topic">Topic</label>
|
||||
<select id="f-topic" name="topic">
|
||||
<option value="">Select one</option>
|
||||
<option>Business opportunity</option>
|
||||
<option>Strategic partnership</option>
|
||||
<option>Consulting</option>
|
||||
<option>Media inquiry</option>
|
||||
<option>Something else</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field field-full"><label for="f-message">Message</label><textarea id="f-message" name="message" required></textarea></div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" type="submit">SEND AN INQUIRY<span class="go">${icon('arrow')}</span></button>
|
||||
<p class="form-status" id="form-status" role="status" aria-live="polite"></p>
|
||||
</form>
|
||||
<div class="ghl-wrap ghl-wrap-form">${ghlForm(SITE.ghl.form, 'Dylan – Lead Form')}</div>
|
||||
</div>
|
||||
|
||||
<a class="cal-card" href="${SITE.calendar}">
|
||||
<div><h3>Rather Put It on the Calendar?</h3><p>If you already know a conversation makes sense, skip the back-and-forth and find a time that works.</p></div>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
(function () {
|
||||
var tabs = document.querySelectorAll('[data-call]');
|
||||
var select = function (id, scroll) {
|
||||
tabs.forEach(function (t) {
|
||||
var on = t.getAttribute('data-call') === id;
|
||||
t.setAttribute('aria-selected', String(on));
|
||||
var panel = document.getElementById('cal-' + t.getAttribute('data-call'));
|
||||
if (panel) { if (on) panel.removeAttribute('data-off'); else panel.setAttribute('data-off', ''); }
|
||||
});
|
||||
if (scroll) document.getElementById('schedule').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
tabs.forEach(function (t) {
|
||||
t.addEventListener('click', function () {
|
||||
var id = t.getAttribute('data-call');
|
||||
select(id, true);
|
||||
try { history.replaceState(null, '', '#' + id); } catch (e) {}
|
||||
});
|
||||
});
|
||||
var hash = location.hash.replace('#', '');
|
||||
if (hash === 'free' || hash === 'vip') select(hash, false);
|
||||
})();
|
||||
@@ -1019,3 +1019,30 @@ h1, h2, h3, h4 { font-weight: 600; }
|
||||
@media (max-width: 900px) { .gallery { grid-template-columns: repeat(2, 1fr); } }
|
||||
.feature-press { position: relative; }
|
||||
.fp-link { position: absolute; inset: 0; z-index: 3; border-radius: inherit; }
|
||||
|
||||
/* GoHighLevel embeds (lead form + booking calendars) */
|
||||
.ghl-wrap { border-radius: 14px; overflow: hidden; background: #ffffff; }
|
||||
.ghl-frame { display: block; width: 100%; border: 0; background: transparent; }
|
||||
.ghl-wrap-form .ghl-frame { min-height: 460px; }
|
||||
.ghl-wrap-cal { padding: 8px; }
|
||||
.ghl-wrap-cal .ghl-frame { min-height: 720px; }
|
||||
.ghl-card { gap: 20px; }
|
||||
.calls { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; max-width: 980px; margin: 56px auto 0; }
|
||||
.call {
|
||||
all: unset; box-sizing: border-box; cursor: pointer; display: flex; flex-direction: column; gap: 14px;
|
||||
padding: 32px; border-radius: 28px; border: 1px solid var(--bd);
|
||||
background: linear-gradient(160deg, var(--glass), transparent 120%), var(--cloud); color: var(--ink);
|
||||
transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease;
|
||||
}
|
||||
.call:hover { transform: translateY(-2px); border-color: rgba(31, 183, 255, .45); }
|
||||
.call:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
||||
.call[aria-selected="true"] { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), 0 0 50px rgba(26, 127, 255, .18); }
|
||||
.call-top { display: flex; justify-content: space-between; align-items: center; }
|
||||
.call-top .ic { width: 44px; height: 44px; border-radius: 50%; }
|
||||
.call-h { font-size: var(--text-heading); font-weight: 500; letter-spacing: -0.03em; line-height: 1.15; }
|
||||
.call-p { color: var(--slate); }
|
||||
.call .btn { align-self: flex-start; margin-top: 6px; pointer-events: none; }
|
||||
.cal-panel { max-width: 980px; margin: 0 auto; border-radius: 24px; }
|
||||
@media (max-width: 760px) { .calls { grid-template-columns: 1fr; } .call { padding: 24px; } }
|
||||
#schedule .wrap { position: relative; }
|
||||
.cal-panel[data-off] { position: absolute; left: -10000px; top: 0; width: min(980px, 100%); visibility: hidden; pointer-events: none; }
|
||||
|
||||
Reference in New Issue
Block a user