feat: add inline lead form to homepage
E2E Test Suite / Form Interaction Tests (push) Failing after 10m19s
E2E Test Suite / Security Header Tests (push) Failing after 10m5s
E2E Test Suite / Mobile Device Tests (push) Failing after 10m58s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 11m50s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 12m43s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 13m35s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 14m27s
Ping Search Engines / Notify Search Engines (push) Failing after 14m11s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 21m12s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Build & Verify (push) Blocked by required conditions
Deploy to Production / Pre-Deploy Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Railway (push) Blocked by required conditions
Deploy to Production / Deploy to Render (push) Blocked by required conditions
Deploy to Production / Deploy to VPS (PM2) (push) Blocked by required conditions
Deploy to Production / Deploy to Fly.io (push) Blocked by required conditions
Deploy to Production / Post-Deploy Verification (push) Blocked by required conditions
Deploy to Production / Notify on Failure (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Failing after 10m19s
E2E Test Suite / Security Header Tests (push) Failing after 10m5s
E2E Test Suite / Mobile Device Tests (push) Failing after 10m58s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 11m50s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 12m43s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 13m35s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 14m27s
Ping Search Engines / Notify Search Engines (push) Failing after 14m11s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 21m12s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Test Report Summary (push) Has been cancelled
Deploy to Production / Build & Verify (push) Blocked by required conditions
Deploy to Production / Pre-Deploy Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Railway (push) Blocked by required conditions
Deploy to Production / Deploy to Render (push) Blocked by required conditions
Deploy to Production / Deploy to VPS (PM2) (push) Blocked by required conditions
Deploy to Production / Deploy to Fly.io (push) Blocked by required conditions
Deploy to Production / Post-Deploy Verification (push) Blocked by required conditions
Deploy to Production / Notify on Failure (push) Has been cancelled
Mounts a 3-field lead-capture form (name, email-or-phone, project type)
between the FAQ/tech-stack block and the final CTA band so visitors who
are ready to start a project can submit without leaving the homepage.
Cuts the friction step of routing every CTA through /contact.
- New src/components/InlineLeadForm.astro:
* Heading, micro-trust subhead, honeypot, client-side validation
* Smart contact-field routing: detects email vs phone and synthesises
a placeholder email for phone-only leads so the existing /api/contact
validator accepts them (the real phone is preserved and surfaced in
the admin notification)
* In-place success state with phone + WhatsApp fallbacks; no navigation
* 48px touch targets, dark-mode safe, autocomplete + inputmode attrs
- src/pages/api/contact.ts: accept and surface `source` / `projectType`
metadata (allowlisted source values, free-text projectType capped at
80 chars), add `erp-solutions` and `government-systems` subject codes,
and include the source/project info in both the admin email and logs.
- src/pages/index.astro: import + mount InlineLeadForm above the final
CTA section.
- tests/inline-lead-form.spec.ts: 8 specs covering UI render, mobile
attributes, blank-field validation, email-submit + phone-submit
payload routing, and server-side acceptance of the new payload shape,
new subjects, and unknown-source fallback.
This commit is contained in:
@@ -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<void> {
|
||||
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<void> {
|
||||
'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<void> {
|
||||
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<void> {
|
||||
html: `
|
||||
<h2>New Contact Form Submission</h2>
|
||||
<table style="border-collapse:collapse;width:100%;max-width:600px">
|
||||
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Source</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(sourceLabel)}</td></tr>
|
||||
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Name</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(data.name)}</td></tr>
|
||||
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Email</td><td style="padding:8px;border-bottom:1px solid #eee"><a href="mailto:${escapeHtml(data.email)}">${escapeHtml(data.email)}</a></td></tr>
|
||||
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Phone</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(data.phone || 'Not provided')}</td></tr>
|
||||
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Subject</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(subjectLabels[data.subject] ?? data.subject)}</td></tr>
|
||||
${data.projectType ? `<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Project</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(data.projectType)}</td></tr>` : ''}
|
||||
<tr><td style="padding:8px;font-weight:bold;vertical-align:top">Message</td><td style="padding:8px;white-space:pre-wrap">${escapeHtml(data.message)}</td></tr>
|
||||
</table>
|
||||
<p style="color:#888;font-size:12px;margin-top:16px">Submitted at: ${new Date().toISOString()}</p>
|
||||
@@ -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 <select>. We only echo it
|
||||
// into the admin email and logs — never used in routing — so allow any
|
||||
// short reasonable label and just trim/cap it.
|
||||
const rawProjectType = body.projectType ? String(body.projectType).trim() : '';
|
||||
const projectType = rawProjectType.length > 0 && rawProjectType.length <= 80
|
||||
? rawProjectType
|
||||
: undefined;
|
||||
|
||||
// Build and validate form data
|
||||
const formData: ContactFormData = {
|
||||
name: String(body.name ?? '').trim(),
|
||||
@@ -399,6 +440,8 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
phone: body.phone ? String(body.phone).trim() : undefined,
|
||||
subject: String(body.subject ?? '').trim(),
|
||||
message: String(body.message ?? '').trim(),
|
||||
source,
|
||||
projectType,
|
||||
};
|
||||
|
||||
const validationError = validateContactForm(formData);
|
||||
@@ -428,6 +471,8 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
userRecipient: formData.email,
|
||||
userMessageId: result?.userConfirmInfo?.messageId,
|
||||
subject: formData.subject,
|
||||
source: formData.source,
|
||||
projectType: formData.projectType,
|
||||
});
|
||||
})
|
||||
.catch(async (err: any) => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import SEO from '../components/SEO.astro';
|
||||
import ClientLogos from '../components/ClientLogos.astro';
|
||||
import InlineLeadForm from '../components/InlineLeadForm.astro';
|
||||
|
||||
// Benefits/Features data
|
||||
const benefits = [
|
||||
@@ -708,6 +709,13 @@ const faqs = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Inline Lead Form — sits between the FAQ/tech-stack social-proof block
|
||||
and the final CTA band so visitors who are ready to start a project can
|
||||
submit a 3-field enquiry without leaving the homepage. Submits to the
|
||||
same /api/contact endpoint and is tagged `source: 'homepage-inline'` so
|
||||
the lead can be segmented downstream. -->
|
||||
<InlineLeadForm />
|
||||
|
||||
<!-- Final CTA Section - Enhanced -->
|
||||
<section class="py-24 bg-gradient-to-br from-secondary-900 via-primary-900 to-primary-800 relative overflow-hidden">
|
||||
<!-- Background decorations -->
|
||||
|
||||
Reference in New Issue
Block a user