# Booking Request Form

## 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

<div class="border border-dark">
  ![Homio booking form](/images/homio/book.jpg)
</div>

`/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.

<Aside type="caution" title="A request, not a confirmed booking">
  Homio takes **no payment**, has **no calendar sync**, and does **not check
  your real availability**. The form only blocks dates outside your rules (see
  "Dates And Arrival Windows" below). Every request needs a person to confirm
  it. The shipped copy says so ("We confirm the time and price by phone or
  text"); keep a line like that.
</Aside>

### 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`:

```toml
contactFormAction = "https://formsubmit.co/your-email@example.com"
# 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](/homio/customization/forms/).
- `enable = false` removes 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`:

```yaml
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:

```yaml
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:

```text
/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](/homio/customization/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](/homio/customization/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).

<Aside type="tip" title="Check it end to end">
  After you connect the form, send one test request from the live site and check
  that every field arrives. FormSubmit sends an activation email on the first
  request; confirm it, or later requests are not delivered.
</Aside>

### 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
```text
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.
```

```text
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.
```