Skip to main content
ASoc
Tutorial

A Health Landing Page Is a Lead Form With Rules Attached

The sections are the easy part. Three constraints a SaaS page skips: no clinical detail in the form, no person in the analytics, and a CSP that makes every tracker a commit.

The ASoc Team9 min read

A health landing page needs the same parts as any other — a clear offer, proof, one primary action — plus three constraints a SaaS page does not carry: the form must not collect clinical detail, the analytics must contain no person, and every third-party script is a data disclosure you have to be able to justify. Get those three wrong and a beautiful page is a liability.

This is written from the engineering side of a storefront that runs a cookieless analytics setup with five whitelisted events, a Content-Security-Policy that blocks embeds by default, and lead forms that collect exactly one field. None of it is legal advice — your jurisdiction and your relationship to a covered entity decide what applies. All of it is the technical shape that makes the legal conversation short.

The sections are the easy part

SectionGeneric SaaS landing pageHealth / clinic landing page
Hero CTA"Start free trial""Book an appointment" / "Check availability" — a time, not a trial
Core proofCustomer logosNamed practitioners with credentials, registration numbers, and photos
Secondary proofG2 badges, star ratingsAccreditations, insurers accepted, years in practice
Primary formEmail + company + team sizeName + contact method. Nothing about why they are calling
Objection handlingPricing tableCost, insurance cover, what the first visit involves, cancellation policy
LocationFootnoteA section — address, parking, transit, accessibility, opening hours
Structured dataSoftwareApplicationMedicalBusiness / Physician, with address and openingHours
Chat widgetDefault yesDefault no, until someone has read what it stores and where

The last two rows are where health pages diverge most from the template they were built on. Location is a conversion section, not a courtesy: a person choosing a clinic is choosing a journey across town. And the chat widget, dropped in by habit, is the single most common way an otherwise careful page starts collecting symptom descriptions into a third party's database.

The closest shape in this catalog is ASoc Cover, an insurance landing template whose four cover types include health. It is worth looking at for the funnel rather than the vertical: a two-minute-quote hero, a review trust band, a transparent-pricing section, and a five-question FAQ that ends in one action. That is the structure a clinic page wants. What changes is what the form is allowed to ask.

The lead form: one field, and the ones you must refuse

The waitlist form on this site is a Server Action and it collects a single field. The full validation path, copied from src/lib/actions/newsletter.ts:

"use server";
import { EMAIL_RE } from "@/lib/validation";

export async function subscribeToWaitlist(_prev: FormState, formData: FormData) {
  // Honeypot: real users never fill this hidden field.
  if (formData.get("company")) return { ok: true, message: "You're on the list!" };

  const email = String(formData.get("email") ?? "").trim().toLowerCase();
  if (!EMAIL_RE.test(email)) {
    return { ok: false, message: "Please enter a valid email address." };
  }
  // …deliver, then return a message. No logging of the address itself.
}

Three things in nine lines are worth copying onto a health page.

The honeypot returns success. A bot that filled the hidden company field gets the same cheerful message a human gets, and nothing is stored. Failing loudly tells the bot what to change.

Validation is a regex, not a library. A two-field form does not need client-side cross-field logic, so it does not need a validation package in the bundle. The whole check is server-side, which is also the only place it counts.

Nothing is logged on the happy path. The error branches log a status code and a message; the success branch logs nothing. On a health form that distinction is the whole game — a logging line that includes formData is how a free-text field ends up in a log aggregator, a crash reporter, and an analytics replay tool in one step.

Then the refusals. A marketing lead form should not have a field for why the person is getting in touch. "Tell us about your symptoms", "What are you being treated for", a condition dropdown, a date of birth, an insurance member number — each one converts a marketing contact into a health record, and does it before the person has any relationship with you. The clinical conversation belongs behind a login or on the phone. A landing page's job is to get someone to the point where that conversation can happen safely.

The practical rule: every field on the form must be justifiable as "needed to contact this person back". Name and one contact method pass. A free-text box labelled "How can we help?" is the ambiguous one — people will put clinical detail in it whether you asked for it or not, so if you keep it, treat whatever arrives in it as sensitive from the moment it lands.

Analytics with no person in it

This site runs cookieless Vercel Web Analytics and sends five conversion events, defined as a closed TypeScript union in src/lib/analytics.ts:

type ConversionEvent =
  | "preview_opened"
  | "buy_clicked"
  | "waitlist_joined"
  | "blog_cta_clicked"
  | "wishlist_added";

export function track(event: ConversionEvent, props?: Record<string, string | number>) {
  try {
    vercelTrack(event, props);
  } catch {
    // Analytics must never break UX — swallow any client-side error.
  }
}

The union is the control. A typo cannot invent a sixth event, and because every call site has to name one of five constants, there is one short list to audit when someone asks what this page sends. The props are catalog slugs and tier names — no email, no name, no user id, by a rule stated in the file's own comment.

A health page should be at least this strict, and the reason is specific rather than abstract: on a clinic site the URL itself can be the sensitive value. A page view on /treatments/fertility attached to a stable visitor id is a health inference about a person, even though no form was filled. That is why cookieless matters more here than on a storefront — no cookie means no durable identifier to attach the path to — and why "we only send page views" is not automatically safe.

Two things follow:

  • Keep sensitive routes out of event props. If you fire a custom event on a treatment page, send a coarse category, not the exact slug.
  • Turn off session replay, or confine it to pages with no form. A replay tool records keystrokes; a keystroke in a free-text box on a clinic page is the exact thing you decided not to store.

The CSP is the list of third parties you have agreed to

Every script you add to a health page is a party that can see the URL, the referrer, and anything the script chooses to read from the DOM. The useful discipline is to make that list explicit and small. This project's policy, in next.config.ts, names every external origin and why:

const contentSecurityPolicy = [
  "default-src 'self'",
  `script-src 'self' 'unsafe-inline' https://app.lemonsqueezy.com https://va.vercel-scripts.com`,
  "style-src 'self' 'unsafe-inline'",
  `connect-src 'self' ${supabaseOrigin}`,
  "frame-src 'self' https://*.lemonsqueezy.com https://*.vercel.app",
  "frame-ancestors 'none'",
  "form-action 'self' https://*.lemonsqueezy.com",
  "object-src 'none'",
].join("; ");

The file carries a one-line rule above it — each origin verified against the code that loads it; do not add speculative hosts — and that rule is what makes the policy useful rather than decorative. Dropping a marketing pixel onto a page covered by this header fails twice: the script will not load (script-src), and if it did, its beacon would not send (connect-src). Both failures are visible in the browser console, which means adding a tracker becomes a deliberate act with a commit attached to it, instead of something that happens in a tag manager on a Friday.

form-action is the quiet one. It restricts where a form on your page is allowed to POST — which is the difference between an injected form exfiltrating a submission and an injected form doing nothing at all. On a page that collects patient contact details, that directive earns its line.

frame-ancestors 'none' is the other. It stops the page being embedded in someone else's site, which is how a clickjacked "Book now" button gets built.

Mistakes and how they show up

MistakeSymptomFix
Free-text "how can we help" on the marketing formSymptom descriptions in your inbox, CRM, and log aggregatorRemove it, or treat the whole pipeline as handling sensitive data
Chat widget added by defaultA third party holds transcripts you have never readRemove until someone has reviewed retention and location; it is not a launch requirement
Session replay on a page with a formKeystrokes recorded in free-text fieldsDisable, or exclude every page with an input
Treatment slug sent as an event propCondition inferred per visitor from your own analyticsSend a coarse category; keep the path out of props
Pixels managed in a tag managerNobody can answer "what does this page send?"Put third-party origins in the CSP so additions need a commit
Trust section is stock photosReads as a template; no credential a visitor can verifyNamed practitioners, registrations, accreditations, insurers accepted
Location buried in the footerVisitors bounce to a competitor they can findA location section with address, hours, transit and accessibility
SoftwareApplication schema inherited from the templateWrong entity type for rich resultsMedicalBusiness or Physician, with address and openingHours
No cancellation or cost information"Book now" clicked, then abandoned at the first unknownAnswer cost, cover and first-visit expectations in the FAQ

Frequently asked questions

Is a health landing page subject to HIPAA? It depends on who you are and what you collect, and it is a question for counsel rather than a blog post. What is unambiguous is the engineering direction it points: the less a marketing page collects, stores and forwards, the smaller that question gets. A page with one contact field, no clinical inputs, no replay and a short third-party list is the easy version of the conversation.

Can I run analytics on a healthcare site at all? Generally yes, and cookieless, aggregate analytics is the conservative starting point. The thing to watch is not the pageview count but the identifiers and the URLs: a durable visitor id plus a condition-specific path is an inference about a person, regardless of which tool produced it.

Do I need a separate page per treatment? Only if each one has its own audience and its own question to answer. A treatment page that exists to repeat the homepage with one word changed will not rank, and on a health site it also multiplies the number of sensitive URLs you now have to think about in analytics.

What is the single highest-impact section to add? Named practitioners with verifiable credentials. It is the proof a visitor actually evaluates before booking, it is the hardest thing for a competitor to copy, and it is the part generic landing-page templates leave as a stock-photo placeholder.

Templates in this post

ASoc Cover is the closest funnel in the catalog: an insurance marketing site whose four cover types include health, built around a two-minute-quote hero, a review trust band, testimonials and a five-question FAQ that ends in one action. ASoc Echo and ASoc Edge are the SaaS-shaped counterexample this post argues with — a chatbot landing page and an applied-AI agency site, both "book a demo" pages with the trust, FAQ and team sections a clinic page needs and the form fields it does not.

All three are static Next.js builds, which is what lets a health page ship with no third-party script at all. Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Tutorial7 min read

How to Add Items to a Cart with HTML and JavaScript (Safely)

A 70-line cart with data attributes, one delegated click handler and localStorage, then the production version of the same pattern from a real storefront.

Read more
Tutorial8 min read

How to Share a Supabase Project: You Share the Organization

There is no per-project invite below the Team plan. The four roles, the 24-hour invite, and why 271 lines of migrations beat a dashboard seat for most collaborators.

Read more
Tutorial8 min read

Building an HTML/CSS Navigation Bar (and the Bug That Hides It)

Six ingredients build an accessible nav bar — and this site's own mobile drawer shipped the pointer-events-none bug that leaves closed links keyboard-reachable.

Read more