Skip to content
New: Customize your theme with an AI assistant. Read the guide

Booking Request Form

Files: src/content/sections/english/booking-section.md, src/content/pages/english/book.md, src/config/config.toml ([settings.booking]) · Skill level: Beginner

Homio booking form

/book/ is a five-step booking request. The visitor picks a service and a size, chooses a day and an arrival window, gives an address and contact details, and sends. A summary ticket beside the form shows the price at every step, using the same rates as the estimator. The request reaches you through your form provider, by email or in your provider’s dashboard. You then call or text the customer to confirm.

Connect The Form First

Booking requests are sent through the same provider as the contact form. Until you set an endpoint, npm run dev shows a “Form not connected” note above the form (it never shows on the live site), and requests go nowhere.

In src/config/config.toml:

contactFormAction = "https://formsubmit.co/[email protected]"
# formspree | formsubmit.co | netlify (default is formsubmit.co)
contactFormProvider = "formsubmit.co"
[settings.booking]
enable = true
# Endpoint for booking requests. Empty uses `contactFormAction`. Set it to send bookings to a
# different inbox or Formspree form (same provider). PLACEHOLDER rules as contactFormAction.
formAction = ""
thankYouUrl = "/book/thanks/" # Where no-JS posts land (FormSubmit `_next`, Netlify action)
minLeadDays = 1 # Earliest date: today + this many days
maxDays = 60 # Latest date: today + this many days
zipPattern = "^\\d{5}$" # Postal code check (regular expression). UK example: "^[A-Za-z0-9 ]{5,8}$"
checkArea = true # Tell visitors whether their ZIP is in a service area (areas collection)
  • Leave formAction empty to send bookings to the same address as the contact form. Set it to send them somewhere else, with the same provider.
  • Provider setup (FormSubmit activation, Formspree form ids, Netlify) is in Forms.
  • enable = false removes the form.

The Five Steps

StepWhat the visitor fills inRequired
1. ServiceThe service, and how often (only for services with frequency discounts)Service
2. DetailsBedrooms and bathrooms, hours, or a count (it depends on the service), extras, and “What needs doing?” for hourly services“What needs doing?” for hourly services
3. WhenA date, an arrival window, and “Any other day this week works too”Date and window
4. WhereStreet address, apartment, ZIP code, how the crew gets in (with notes for the crew), parking, pets, “Anything else?”Address and ZIP
5. YouName, phone, email, best way to confirm, gift card or referral code, consent, and the cancellation note above the send buttonName, phone, email, consent

“Continue” checks the current step before moving on. Errors show under each field and in a summary at the top of the step, with links to the fields. Without JavaScript, the form is one long page with every step stacked and one Send button.

Edit The Steps And Fields

The step titles, field labels, and options are in src/content/sections/english/booking-section.md:

steps:
service: { title: "Service", description: "What do you need, and how often?" }
details: { title: "Details", description: "Size of the job and any extras." }
schedule: { title: "When", description: "Pick a day and an arrival window." }
address: { title: "Where", description: "Address and how we get in." }
contact: { title: "You", description: "So we can confirm the booking." }
fields:
# `id` is the value sent with the request; keep it a short readable slug
timeWindow:
label: "Arrival window"
options:
- { id: "morning", label: "Morning", hint: "8–11 am" } # sample hours
- { id: "midday", label: "Midday", hint: "11 am–2 pm" }
- { id: "afternoon", label: "Afternoon", hint: "2–5 pm" }
flexible: { enable: true, label: "Any other day this week works too" }
entry:
enable: true
label: "How do we get in?"
help: "Don't write door or alarm codes here. We ask for them by phone."
options:
- { id: "home", label: "Someone will be home" }
- { id: "key", label: "Key left for the crew" }
  • Each optional field (flexible, unit, entry, parking, pets, notes, contactPreference, code, cancellation) has enable. Set it to false to remove the field.
  • In a choice field, id is what you receive and label is what the visitor sees. Keep id short and the same in both languages.
  • consent.label and cancellation.text accept Markdown, so the privacy and cancellation links work.
  • services: [] offers every rate card. List rate-card ids to offer only some, in that order.
  • summaryTitle is the caption of the summary ticket (“Your ticket”). photo is the picture under it on large screens.

The fixed labels (full name, phone, email, street address, ZIP code, date) and every error message are under booking in src/i18n/en.json and src/i18n/fr.json. The ZIP label says “ZIP code”; change booking.zip there for “Postcode”.

The /book/ page (src/content/pages/english/book.md) has a compact page header with your phone number, a bookingSection block that overrides the section file, a trust strip, and booking questions. The shipped page replaces the arrival windows with four two-hour windows:

bookingSection:
title: "" # the page header carries the title
fields: # merged over the section file's fields; a list replaces the whole list
timeWindow:
options: # sample hours
- { id: "early", label: "Early", hint: "8:00–10:00" }
- { id: "morning", label: "Morning", hint: "10:00–12:00" }
- { id: "midday", label: "Midday", hint: "12:00–14:00" }
- { id: "afternoon", label: "Afternoon", hint: "14:00–16:00" }

So to change the arrival windows on /book/, edit them in book.md, not only in the section file. Set your real hours: the samples are examples.

Dates And Arrival Windows

The date field accepts a day when all of these are true:

  • It is at least minLeadDays days from today and at most maxDays days ahead ([settings.booking]).
  • Your business is open that weekday. A day left empty in [business.hours] (for example sun = "") is refused with “We’re closed on Sundays”.
  • It is not in closedDates under [business] (your holidays, as "YYYY-MM-DD").

There is no live calendar. Two people can request the same window. You confirm or move the time when you call back.

Carry A Price Over From The Estimator

The estimator’s “Book this visit” button, the plan buttons, and the cleaning homepage’s booking card open /book/ with the selection in the link:

/book/?service=standard-cleaning&bedrooms=2&bathrooms=1&frequency=biweekly&addons=inside-oven
ParameterValue
serviceA rate-card id from the pricing file
bedrooms, bathrooms, hours, unitsNumbers. Out-of-range values are moved inside the card’s limits
frequencyA frequency id (weekly, biweekly, monthly, once)
addonsAdd-on ids, repeated (addons=a&addons=b) or comma-separated
ref or codeFills the gift card or referral code field

With a valid service, the form fills steps 1 and 2 and opens on step 3 (When). Values a service does not offer are dropped. The link never carries personal details. You can use these links in your own buttons, emails, or ads, for example /book/?service=window-cleaning&units=30.

The form also keeps an unsent draft of the job (service, size, extras, date, window) in the browser tab, so a visitor who leaves and comes back finds it filled in. The draft never holds the address or contact details, and it is cleared after a request is sent.

Service Area Check

With checkArea = true, the form compares the ZIP code with the zips of your area pages when the visitor leaves the field. It shows “Elm Park is in our area.” or “We may not cover 55999 yet. Send anyway and we’ll confirm.” with a link to /areas/. It never blocks the request. zipPattern sets the postal code format. See Service Areas.

What You Receive

Each request contains the field values (service, bedrooms, bathrooms, hours or units, job_description (hourly services), frequency, addons, date, time_window, flexible, address, unit, zip, entry, entry_notes, parking, pets, name, phone, email, contact_preference, code, notes, consent), plus:

  • ticket: the estimate code the visitor saw, such as EST-4K7Q2 (ticketPrefix in [settings.estimator]).
  • estimate_total and estimate_lines: the total and each line of the price.
  • language and page_url.

The email subject is emailSubject from the section file, with the service and date added (“Booking request · Standard clean · Tue, Nov 18”). Without JavaScript, it is emailSubject alone.

After Sending

With JavaScript, the steps are replaced by the success message from success in the section file (overridden in book.md): a title, a description, the visitor’s ticket code, and up to three next steps. The page also fires a bookingsubmit event on document with the service, frequency, and total, for analytics (see Cookies & Analytics).

Without JavaScript, the provider redirects to thankYouUrl (/book/thanks/), set by src/content/pages/english/book-thanks.md. That page is kept out of search results and the sitemap.

If sending fails, the form shows “We couldn’t send your request. Try again, or call” with your phone number.

Spam protection is built in: a hidden honeypot field, and a request sent less than 4 seconds after the visitor first touched the form is dropped (the visitor still sees the success message).

The Booking Card On The Cleaning Homepage

/home-cleaning/ shows the same form in the card layout: a photo backdrop and a white card with step 1 only (service, size, extras, frequency). “Continue” opens /book/ with that selection. Set it in the bookingSection block of src/content/homepage/english/home-cleaning.md (options.layout: "card", backdrop, photo, metrics).

Do It With AI

Set up my booking form. Send requests to [EMAIL OR FORMSPREE URL] with provider [formsubmit.co |
formspree | netlify] in src/config/config.toml. Earliest booking [N] days ahead, latest [N] days.
Our arrival windows are [LABEL: HOURS; ...]: put them in the bookingSection block of
src/content/pages/english/book.md and in src/content/sections/english/booking-section.md. Remove
the [parking | pets | code] field. Postal code format: [EXAMPLE]. Do the French files too, then
tell me how to send a test request.
Write a booking link for my [SERVICE] ad: [SIZE], [FREQUENCY], with the add-on [ADD-ON]. Use the
ids from src/content/pricing/english/-index.md and the parameters /book/ accepts.
Type something to search...