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

/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
formActionempty 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 = falseremoves the form.
The Five Steps
| Step | What the visitor fills in | Required |
|---|---|---|
| 1. Service | The service, and how often (only for services with frequency discounts) | Service |
| 2. Details | Bedrooms 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. When | A date, an arrival window, and “Any other day this week works too” | Date and window |
| 4. Where | Street address, apartment, ZIP code, how the crew gets in (with notes for the crew), parking, pets, “Anything else?” | Address and ZIP |
| 5. You | Name, phone, email, best way to confirm, gift card or referral code, consent, and the cancellation note above the send button | Name, 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) hasenable. Set it tofalseto remove the field. - In a choice field,
idis what you receive andlabelis what the visitor sees. Keepidshort and the same in both languages. consent.labelandcancellation.textaccept Markdown, so the privacy and cancellation links work.services: []offers every rate card. List rate-card ids to offer only some, in that order.summaryTitleis the caption of the summary ticket (“Your ticket”).photois 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
minLeadDaysdays from today and at mostmaxDaysdays ahead ([settings.booking]). - Your business is open that weekday. A day left empty in
[business.hours](for examplesun = "") is refused with “We’re closed on Sundays”. - It is not in
closedDatesunder[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| Parameter | Value |
|---|---|
service | A rate-card id from the pricing file |
bedrooms, bathrooms, hours, units | Numbers. Out-of-range values are moved inside the card’s limits |
frequency | A frequency id (weekly, biweekly, monthly, once) |
addons | Add-on ids, repeated (addons=a&addons=b) or comma-separated |
ref or code | Fills 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 asEST-4K7Q2(ticketPrefixin[settings.estimator]).estimate_totalandestimate_lines: the total and each line of the price.languageandpage_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 ofsrc/content/pages/english/book.md and in src/content/sections/english/booking-section.md. Removethe [parking | pets | code] field. Postal code format: [EXAMPLE]. Do the French files too, thentell me how to send a test request.Write a booking link for my [SERVICE] ad: [SIZE], [FREQUENCY], with the add-on [ADD-ON]. Use theids from src/content/pricing/english/-index.md and the parameters /book/ accepts.