Skip to content

Landing Page with a Contact Form, No Backend

The standard advice for putting a contact form on a static landing page is to outsource it. Formspree. Netlify Forms. A Google Form in an iframe. A Lambda you will have forgotten you own by the time it breaks. Each one bolts a second vendor onto a page whose entire appeal was that it was one file.

Micropage treats the form as a markup block. You declare the fields, publish, and the submissions are yours.

A landing page contact form, in full

This is a complete two-page site. Nothing is elided.

[site]
title: Harbor
description: Scheduling for small boatyards

[nav]
Home -> /
Contact -> /contact

[Home -> /]

/// hero align:center
h1: Scheduling for small boatyards
p: One calendar, one crew, no spreadsheet.
button: Talk to us -> /contact

[Contact -> /contact]

/// section
h2: Contact us

form: Contact
input: Name*
input: Email*
select: What is this about? [Sales, Support, Something else]
textarea: Message*
submit: Send message
success: Thanks — we reply within one business day.

Each [Name -> /path] header starts a page. The [nav] block is shared across all of them. The form name — Contact here — is what identifies its submissions afterwards, so name it for what it collects rather than where it sits.

Fields

Six field elements exist, and that is the whole list:

  • input: — single line
  • textarea: (or text:) — multiple lines
  • select: — dropdown
  • checkboxes: — several choices
  • radios: — one choice
  • submit: — the button

Choice fields take their options inline in square brackets: radios: Plan [Solo, Team, Org]. The full reference, including the aliases each element accepts, is in how forms work in Micropage.

There is no date:, no file:, no conditional logic. A landing page form that needs branching logic is a form that wants to be an application, and you should build that somewhere else.

Required fields

A label ending in * is required. The asterisk is not part of the label text — it sets the field’s required attribute and renders as a red marker after the label. A form with at least one required field also renders a * indicates a required field key above its fields.

The per-field asterisk is aria-hidden, because required already announces the field to a screen reader and hearing “star” after every label is noise. The key above the fields is plain text and does get read out.

The * works on every field type, select and checkboxes and radios included, and it is re-checked on the server. The browser attribute is a convenience, not the enforcement.

What happens on submit

The published page carries a small script. It intercepts the submit, posts the values to Micropage, and swaps the form card for a confirmation in place. No page load, no redirect. The confirmation carries role="status" so it is announced, and it does not fade out on a timer — removing it after three seconds would cut the announcement off mid-sentence. Regular forms get a Submit again button underneath; newsletter forms don’t.

With JavaScript disabled the <form> still has a real action and method="post". The browser posts natively and Micropage returns a plain confirmation page with a link back to your site. Unstyled, but it works, and it shows the same success: text.

Either way, the row lands in the Form Submissions tab in the editor: timestamp, page URL, form name, field values, stored against the project. No external service is involved in storing it.

Spam

There is no captcha. A contact form on a small landing page does not earn one — the cost lands on every real visitor to stop a bot that a honeypot would have stopped for free. What ships instead:

  • A hidden honeypot field. Filled in, the submission is rejected.
  • A strict allowlist. Only fields declared in your markup are accepted. An unknown key is a rejection, not a quietly stored extra column.
  • Server-side required checks, as above.
  • A schema checksum. A fingerprint of the form’s fields travels with each submission. Republish with different fields and a stale browser tab posting the old shape gets told the form was updated and to refresh, instead of writing a row that half-matches.

The thank-you page you probably don’t need

Forms do not redirect. There is no thank-you URL to configure. The confirmation is the success: line — optional, trimmed, capped at 300 characters, and used on both the scripted path and the no-JavaScript fallback. Leave it out and a form shows Thank you. Your message was sent.

If you want a standalone /thank-you page anyway, because something else links to it, build it as a normal page and keep it out of search:

[Thank you -> /thank-you]
meta:
  noindex: true

/// section align:center
h2: Thanks
p: We'll be in touch.

noindex: true in a page’s meta: block emits <meta name="robots" content="noindex, nofollow"> and excludes the page from /sitemap.xml. Utility pages should not compete with your landing page for the same query.

What the plans gate

Storing and reading submissions works on Free. The gating is on volume and on getting told about them.

FreeProPro+
Submissions100 / month1,000 / month10,000 / month
CSV exportnoyesyes
Daily email digestnoyesyes
Instant email per submissionnonoyes
Webhooknonoyes

The Free allowance is 100 submissions a month — not per project. Every plan resets monthly. The Pro+ webhook POSTs JSON with the project id, form name, page URL, and the ordered field list, plus an X-Micropage-Submission-Id header you can deduplicate retries on.

From the CLI

The CLI ships on Pro and Pro+. It reads the same submissions as the editor:

micropage submissions list
micropage submissions show <id>
micropage submissions export --format csv --output submissions.csv

export is the honest answer to “can I get this into my CRM.” It is a CSV, or JSON with --format json. The form primitive is not a pipeline, and pretending otherwise is how you end up with a page that needs a backend again.

Next