diff --git a/src/components/InlineLeadForm.astro b/src/components/InlineLeadForm.astro new file mode 100644 index 0000000..3021295 --- /dev/null +++ b/src/components/InlineLeadForm.astro @@ -0,0 +1,464 @@ +--- +// InlineLeadForm — Three-field lead capture rendered inline on the homepage. +// +// The goal is to cut the friction step of routing every CTA through /contact. +// Visitors who reach the bottom of the homepage can submit name + contact +// (email OR phone) + project type without a navigation. The submission reuses +// /api/contact under the hood and is tagged `source: 'homepage-inline'` so the +// backend / analytics layer can segment these leads cleanly. +// +// Why a single "contact" field instead of separate email + phone: +// - 3 fields keeps the form psychologically tiny and dramatically lifts +// completion. Real-world conversion testing repeatedly shows 3 → 5+ fields +// is the biggest single-step drop-off on B2B lead forms. +// - We sniff the value on submit. If it has an "@" we treat it as email. +// Otherwise we treat it as a phone and synthesize a placeholder email to +// satisfy the backend's strict email validation. The phone is also sent +// through, so the admin email body shows the real reachable value. +--- + +
+
+
+
+ + + +
+ +

+ Tell us about your project — get a free 1-page proposal in 24 hours. +

+ + +

+ Free 30-min call · NDA on request · No obligation +

+ + +
+ + + + +
+ + + +
+ + +
+ + +

+ Drop your best email or phone number — whichever is easiest. +

+ +
+ + +
+ +
+ + +
+ +
+ + + + + +
+ +

+ We respect your inbox — one reply, no marketing list. +

+
+
+ + + +
+
+
+
+
+ + diff --git a/src/pages/api/contact.ts b/src/pages/api/contact.ts index 1137a35..dec46d7 100644 --- a/src/pages/api/contact.ts +++ b/src/pages/api/contact.ts @@ -50,9 +50,25 @@ const VALID_SUBJECTS = [ 'ai-ml', 'consulting', 'support', + 'erp-solutions', + 'government-systems', 'other', ]; +// Allowed lead-source tags. Used so leads can be segmented downstream +// (e.g. "homepage-inline" vs the regular /contact page submissions). +// Anything outside this allowlist is dropped to "unknown" to keep the +// downstream analytics tidy and prevent free-text injection into logs. +const VALID_SOURCES = [ + 'contact-page', + 'homepage-inline', + 'homepage-hero', + 'homepage-final-cta', + 'mobile-sticky', + 'whatsapp-fab', + 'unknown', +]; + interface ContactFormData { name: string; email: string; @@ -60,6 +76,8 @@ interface ContactFormData { subject: string; message: string; website?: string; // honeypot + source?: string; + projectType?: string; } function validateContactForm(data: ContactFormData): string | null { @@ -161,6 +179,8 @@ async function sendContactEmail(data: ContactFormData): Promise { email: data.email, subject: data.subject, name: data.name, + source: data.source, + projectType: data.projectType, }); return; } @@ -189,9 +209,13 @@ async function sendContactEmail(data: ContactFormData): Promise { 'ai-ml': 'AI & Machine Learning', 'consulting': 'IT Consulting', 'support': 'Technical Support', + 'erp-solutions': 'ERP / Custom Software', + 'government-systems': 'Government / GeM', 'other': 'Other', }; + const sourceLabel = data.source ? data.source : 'contact-page'; + const fromAddress = smtpFrom || smtpUser; // 1) Admin notification email with BCC to SMTP_USER for delivery confirmation @@ -204,10 +228,12 @@ async function sendContactEmail(data: ContactFormData): Promise { text: [ `New contact form submission`, ``, + `Source: ${sourceLabel}`, `Name: ${data.name}`, `Email: ${data.email}`, `Phone: ${data.phone || 'Not provided'}`, `Subject: ${subjectLabels[data.subject] ?? data.subject}`, + ...(data.projectType ? [`Project: ${data.projectType}`] : []), ``, `Message:`, data.message, @@ -218,10 +244,12 @@ async function sendContactEmail(data: ContactFormData): Promise { html: `

New Contact Form Submission

+ + ${data.projectType ? `` : ''}
Source${escapeHtml(sourceLabel)}
Name${escapeHtml(data.name)}
Email${escapeHtml(data.email)}
Phone${escapeHtml(data.phone || 'Not provided')}
Subject${escapeHtml(subjectLabels[data.subject] ?? data.subject)}
Project${escapeHtml(data.projectType)}
Message${escapeHtml(data.message)}

Submitted at: ${new Date().toISOString()}

@@ -392,6 +420,19 @@ export const POST: APIRoute = async ({ request, clientAddress }) => { ); } + // Normalize the lead source. Free-text is rejected to keep the analytics + // taxonomy clean and prevent log-injection style abuse. + const rawSource = body.source ? String(body.source).trim().toLowerCase() : ''; + const source = VALID_SOURCES.includes(rawSource) ? rawSource : 'contact-page'; + + // Project type is free-text from a controlled