Website module
The Website tab — the shop's public marketing site, framed inside the Hub rather than reproduced. What edits here IS the page the public gets; the alternative (a separate CMS copy) drifts. The tab also carries the Reserve Your Bike flow — the public-facing rental funnel that lands a real booking back in the Rentals tab.
| Tab label | Website |
| Folder | src/modules/website/ |
| Tab accent | #1e3a5f (Kvick mid-blue) |
| Public URL | /website on the Hub Worker — the same Worker serves the store's marketing site |
| Migrations | 122 (website_content editable overlay), 124 (screen.website + sys.website.edit permissions), 123 (rental unit description + fit — the copy the site displays), 119 + 120 + 121 (rental rate cards the site quotes from) |
Editable site — overlay, not fork (mig 122)
The site under /website is a build artefact: scripts/ingest-website.mjs wipes the directory and re-copies from the monolith. Anything written back into those HTML files is destroyed the next time the site is ingested. So edits are not written to the files — they're stored in website_content and laid over the page at serve time, the same way the rental rates and fleet already are.
What a block key is. The ingest stamps every editable element with data-wc="<tag>-<hash of its original contents>"; the overlay reads that key back at serve time and swaps the tag's innerHTML for whatever the shop wrote in edit mode. Deleting the row restores the original — the overlay does not destroy the source.
Edit mode — asked for with ?edit=1 and granted by sys.website.edit (mig 122). The save route checks the permission again for itself — a query string is a request, not an authorisation.
Two permissions, not one (mig 124)
screen.website— "may look at the site from in here". The tab is drawn from thescreen.*permissions family; without this row the tab is hidden. Mig 124 added it — before that the module was there but had no way to reach it (shipped dark).sys.website.edit— "may change what the public sees". Separate from the tab visibility on purpose: a staffer allowed to preview the site is not automatically allowed to change it.
Rate cards on the site — quotes the shop's numbers (mig 119 + 120 + 121)
The website's Reserve flow quotes the shop's rate cards, not typed-in prices (f319b78). Rate cards live per rental category (mig 119: category, not per bike — same physical bike could have shipped with three different numbers under the old per-unit model); mig 120 reconciled the vocabulary to what the app already speaks (ht_mtn, fs_mtb, ebike_city, ebike_mtn). Mig 121 flagged the hardtails + full-suspension bikes into the actual rental fleet — the site had always advertised the three categories but before this only the e-bikes carried the flag.
Rate card table on the tile. Each rental bike's card shows its rate ladder as a small grid above the Reserve Your Bike button (cf976c0). The Rates-table-below variant was tried and dropped; per-bike tile grids read better (164cf27). N/A shows for a length the shop does not sell (30212f8).
Reserve Your Bike — the funnel
The website Reserve flow (05d2a57):
- Pick a bike from the rental fleet (each is its own card because each is its own bike — c2ed1c6).
- Pick how long — the days ARE the length; the flow doesn't ask twice (0ca3703).
- Pick a date on a calendar (multiple days tickable, 120d10d).
- Nested tabs at the top choose Rent vs. Demo, each with its own explanation (234411a).
- Step 3 — Reserve creates a real booking, and the deposit is quoted (de122bc). The booking lands in Rentals marked as a website booking with nothing paid yet (c882ad6) — the shop takes payment when the rider shows up.
Order-of-questions matters: date first, then length, then show what's free (9b0a99e) — asking length before date meant the calendar didn't know how many days to check availability across.
Unit description + fit (mig 123)
"Tell about this bike, and say what size it fits; the data belongs in the Rental section of the Inventory item." (Tom.) The site used to list a rental bike as "C3 - SZ2 - Brown - 2026" — the shop's own shorthand, meaningless to a customer. Mig 123 adds a shop-authored description + fit blurb per rental unit, entered on the Inventory Stock Card, rendered on the Website tile ("Tell about a rental bike, and say who it fits" — 34cedcf).
Security headers on the public site (3e23890)
The Hub itself already carries X-Content-Type-Options: nosniff, HSTS + refuse-plain-HTTP (78d004f), and X-Frame-Options: SAMEORIGIN (1b98339). Mig 3e23890 carries the same headers onto the public website pages too — the same-origin frame constraint means the public site cannot be embedded from another origin, and the plain-HTTP refusal means a rider hitting http:// gets bounced to HTTPS before any content leaves the Worker.
See also
- Rentals module — where website bookings land marked, with nothing paid
- Settings module — where rate cards are edited (site + till read the same numbers)
- Inventory module — the Stock Card carries the per-unit description + fit the website displays