prelo
Plugins / Booking form

Booking form

Appointment requests with availability windows and email confirmation.

Lgithub.com/lena-mFormsprelo >= 0.5Updated Aug 2026
Use this plugin
1
2
or point your agent at the spec:
Your agent reads the spec and writes the code into your project. Review the diff like any change.

What this adds

An appointment-request flow: visitors pick a free slot inside the availability windows the owner sets in Studio, requests arrive as a queue in Studio, and confirming a request emails the visitor automatically. It's a request-and-confirm flow by design — the owner always has the final say, so no double bookings and no calendar sync required.

How it works

Availability lives in a global (weekly windows + slot length) the owner edits in Studio. A custom route computes free slots for a given date — windows minus already-confirmed bookings. Requests are drafts of a booking type; publishing a booking is confirming it, which both blocks the slot for everyone else and triggers the confirmation email via the afterPublish hook.

The spec your agent followsexpand ▾
You are adding appointment booking to a Prelo project. Outbound email is required for confirmations — check the SMTP env vars `prelo doctor` reports and tell the user if they're missing.

1. **Availability global.** In `cms.config.js` under `globals:`:

   ```js
   booking: {
     fields: {
       slotMinutes: { type: "number", required: true },   // e.g. 30
       leadHours:   { type: "number" },                   // min notice, default 24
       windows: { type: "list", of: {
         day:   { type: "select", options: ["mon","tue","wed","thu","fri","sat","sun"], required: true },
         start: { type: "text", required: true },         // "09:00"
         end:   { type: "text", required: true },         // "17:00"
       }},
     },
   },
   ```

   Read it via `GET /api/globals/booking`; the owner edits it in Studio's globals.

2. **Booking type.**

   ```js
   booking: {
     label: "Bookings",
     fields: {
       when:    { type: "text", required: true },  // ISO start, e.g. 2026-09-12T10:00
       name:    { type: "text", required: true },
       email:   { type: "text", required: true },
       message: { type: "text" },
     },
     studio: { columns: ["when", "name"] },
   },
   ```

   Draft = requested. **Published = confirmed.** Deleted = declined (see step 6 for the decline email caveat).

3. **Slots route.** `"GET /booking/slots"` in `routes:` — query `?date=2026-09-12`:
   - Load the global; find windows matching that weekday; slice into `slotMinutes` steps.
   - Remove slots that collide with **published** bookings on that date (query the `content` table for type `booking`, status published, `when` starting with the date).
   - Remove slots earlier than now + `leadHours` (default 24).
   - Respond `{ date, slots: ["09:00","09:30",…] }`. Empty array is a valid answer.

4. **Request route.** `"POST /booking"`:
   - Validate name/email/message; validate `when` is exactly one of the currently free slots for its date (recompute server-side — never trust the client's slot list). Honeypot `website`: silently accept and discard. Rate-limit 3/hour/IP via `x_booking_hits` created in `hooks.boot`.
   - Insert as a **draft** booking (title = `{when} — {name}`). Requests do **not** block the slot until confirmed; if two people request the same slot, the owner confirms one and the other's slot validation would have passed — handle this by re-checking collisions in the confirmation hook (step 5) and refusing there is impossible (hooks can't veto), so instead: on `afterPublish`, if another published booking already holds the slot, email the owner a conflict warning instead of confirming the visitor. Keep this logic small but present.
   - Optionally email the owner "New booking request" per new draft.

5. **Confirmation email.** In `cms.config.js` `hooks:`, add `afterPublish: (row, db) => …`. When `row.type === "booking"`, send the visitor a confirmation email: date/time formatted, site name, plus the owner's reply-to. This makes Studio's Publish button the entire confirmation workflow — no custom Studio work at all.

6. **Frontend.** A booking page/section: date picker (native `<input type="date">`, min = today) → fetches `/api/x/booking/slots?date=…` → renders slot buttons → form (name, email, message, honeypot) posts to `/api/x/booking`. States: "Requested — you'll get an email when it's confirmed" on success; slot-taken (`409` from the route) re-fetches slots and asks the visitor to pick again; inputs preserved on failure. For declines, note to the owner (README comment) that deleting a request sends nothing — decline by emailing the visitor directly, or ask the agent to add a `declined` flag flow later.

7. **Verify.** Set windows in Studio; request a slot; confirm it in Studio; check the email, the slot vanishing from `/slots`, and the double-request conflict path.

After it's built

The owner sets their week once in Studio globals, then lives in the Bookings list: publish to confirm (visitor gets the email, slot locks), leave as draft to think, delete to decline. Changing slotMinutes or windows reshapes future availability instantly.