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

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:
platform-mcp
2026-06-17 11:49:47 +05:30
parent 64be202f75
commit a4505635e1
4 changed files with 746 additions and 0 deletions
+45
View File
@@ -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) => {