PRD 12 — Website builder (online store)
Status: draft · Priority tier: P1 · Size: L · Depends on: auth/user (existing), PRD 01 (locations, staff), PRD 02/04/05 (classes, appointments, courses), PRD 06 (packages, memberships), PRD 11 (contact form), media storage
1. Summary
A hosted public site per studio with a theme and section customizer, content pages, navigation menus, blog, FAQ, legal documents, domain, and tracking IDs. The site renders live booking data (timetable, packages, memberships, locations, team) and sends visitors into booking/login. This PRD covers the admin side and the public rendering contract; the booking/checkout flow itself belongs to other PRDs.
2. Problem & goals
Studios need a bookable web presence without a developer. Goals:
- New studio has a published, correct-looking site with zero configuration (default theme + system pages).
- Owner can change brand colours, fonts, logo and the homepage text and publish in <= 6 clicks.
- Public pages: LCP <= 2.5 s on 4G for the home page; indexable (SEO fields, sitemap, canonical).
- No customer-visible change until Publish (draft vs live separation). Non-goals: arbitrary HTML/JS layout editing, section reordering/adding (v1 sections are fixed), e-commerce beyond packages/memberships, multi-language sites, our own domain registrar.
3. Users & roles
Owner/admin (everything), staff with website permission (content only), visitor/customer (public site). Front desk: none.
4. Scope
In (v1)
- must: themes: one live theme + library of drafts; publish, customize; at least 1 built-in theme.
- must: customizer with sections Header, Hero, About, Appointments, Events, CTA banner, Classes, Locations, Blogs, Footer; theme settings colours, typography, social links, favicon.
- must: pages (system fixed set + custom), custom page editor, SEO, visibility.
- must: navigation Main + Footer, nesting, reorder, kebab actions.
- must: platform subdomain
<slug>.<our domain>as primary domain. - must: blog posts + blog types; FAQ; legal docs incl. Terms flag.
- should: custom CSS; preferences (tracking IDs, site verification); preview (desktop/mobile); scheduled visibility; blog bulk actions; custom domain connect with DNS instructions; primary-domain change and redirect toggle.
-
could: blog sub type, tags, featured pin; page author; theme preview from library; image crop 16:9. Later (v2+)
-
Additional themes, section layout variants, section show/hide and reorder, per-section item count, extra pages per theme, floating WhatsApp button, reviews display, multi-language. Out
-
Automated DNS/SSL provisioning at launch is infrastructure-heavy: ship manual DNS instructions + verification first; auto-cert via a managed proxy is a decision (open question).
- Copying the original's theme designs, names ("Lavander", "Alpha") or copy text.
5. User stories
- US-12-01 As an owner, I want a ready site on day one so that customers can book immediately.
- US-12-02 As an owner, I want to set colours, fonts and logo so that the site matches my brand.
- US-12-03 As an owner, I want to edit headline text, buttons and featured items per section so that the home page says what I want.
- US-12-04 As an owner, I want to preview before publishing so that customers never see half-done edits.
- US-12-05 As an owner, I want custom pages (e.g. Pricing FAQ, Teacher training) with SEO so that I appear in search.
- US-12-06 As an owner, I want to arrange the menu, including submenus, so that visitors find things.
- US-12-07 As an owner, I want to hide a menu item or page without deleting it.
- US-12-08 As an owner, I want blog posts grouped by type so that the site has fresh content.
- US-12-09 As an owner, I want FAQ topics and legal documents so that policies and answers are public.
- US-12-10 As an owner, I want to use my own domain so that the site is mine.
- US-12-11 As a marketer, I want to paste Meta/GA4/GTM/TikTok IDs so that tracking works without code.
- US-12-12 As a visitor, I want a fast, mobile-friendly site that lets me book or buy.
6. Functional requirements
Themes and customizer
| ID | requirement | pri | evidence |
|---|---|---|---|
| FR-12-01 | Theme list: "Current theme" card (name, thumbnail, Customize) and "My library" cards (name, last edited, Publish, Customize, kebab > Preview) | must | S95 |
| FR-12-02 | New business gets a copy of the default theme, published | must | derived |
| FR-12-03 | Publish from library asks confirm ("replaces current theme"), then swaps live theme atomically; previous live theme moves to library | must | S97 |
| FR-12-04 | Customizer shell: section list, Theme settings pinned at bottom, live preview iframe, page switcher (Home, Classes, Packages, Membership, Timetable, Locate us, Team, Blog, Contact), desktop/mobile toggle, Save (disabled until change), Publish Theme, exit link | must | S96, S98, S99 |
| FR-12-05 | Editing the live theme: Save writes a draft; Publish makes it live (draft-vs-live separation). Assumption; original behaviour unobserved | must | S96; open Q |
| FR-12-06 | Section panels share the shape: Title (text), Description (textarea, 250 soft counter), Buttons (2: label, link, outline flag); blank label hides the button | must | S210-S217 |
| FR-12-07 | Button link = one of the fixed system pages, any custom page, or custom URL | must | S210, S104c |
| FR-12-08 | Header: logo image (JPG/PNG/GIF <= 10 MB), background and text colour | must | S96a |
| FR-12-09 | Hero: single image, overlay toggle + overlay colour, Title/Description/Buttons | must | S210 |
| FR-12-10 | About and CTA banner: section image + Title/Description/Buttons | must | S211, S214 |
| FR-12-11 | Appointments, Events (courses), Classes: Title/Description/Buttons + 3 item pickers each (None or a record) | must | S212, S213, S215 |
| FR-12-12 | Locations: Title/Description/Buttons; shows all locations automatically | must | S216 |
| FR-12-13 | Blogs: Title/Buttons + 4 post pickers; no description | should | S217 |
| FR-12-14 | Footer: background and text colour; shows menu "Footer", social links, legal links | must | S96b |
| FR-12-15 | Colours: primary background; secondary background + text; solid button text + background; outlined button/link colour (hex validated, swatch) | must | S219 |
| FR-12-16 | Typography: header font and body font from a curated list (v1: about 12-32 open-licence fonts, self-hosted) | should | S220, S220b |
| FR-12-17 | Social links: Facebook, X/Twitter, Instagram, YouTube, TikTok (URL validated; empty hidden) | should | S221 |
| FR-12-18 | Favicon upload (JPG/PNG/GIF <= 10 MB, converted to proper icon sizes) | should | S222 |
| FR-12-19 | Custom CSS (one textarea, applies site-wide, sanitised, size cap 20 KB, loaded after theme CSS) | could | S223 |
| FR-12-20 | Empty item picker slot = section renders only filled items; section with no content and no items is hidden | must | S212 (derived) |
| FR-12-21 | Library theme preview opens site in preview mode with that theme | could | S95 |
| FR-12-22 | Section group "hide" (eye) per element: Title and similar hide on site | could | S210; unverified |
| ### Pages | |||
| ID | requirement | pri | evidence |
| --- | --- | --- | --- |
| FR-12-30 | Pages list: search by title, status filter, sort by title/updated/status; columns title, slug, type, updated, status | must | S100 |
| FR-12-31 | System pages (Home, Timetable, Locate us, Contact us, Team, Class, Course, Package, Membership, Appointment, Blog) always exist, not deletable/editable except SEO | must | S100, S100a |
| FR-12-32 | Row menu: system = SEO settings; custom = Edit, SEO, Show/Hide (instant), Delete (confirm) | must | S100a, S100g |
| FR-12-33 | Custom page: title (required), slug auto from title, unique per site, editable; featured image; rich text; author; Preview; Save & Exit | must | S101 |
| FR-12-34 | Rich text: headings, bold/italic/underline/strike, lists, quote, alignment, link, image, video embed (allow-listed hosts); HTML sanitised server side | must | S101c |
| FR-12-35 | Visibility: visible now, hidden, scheduled (date+time, site time zone) | should | S101b |
| FR-12-36 | SEO drawer for every page: title (65), description (320), social image (<= 1 MB), noindex, SERP preview; slug read-only for system pages | should | S100f, S101d |
| FR-12-37 | Hidden/scheduled pages return 404 publicly and are excluded from sitemap | must | derived |
| ### Navigation | |||
| ID | requirement | pri | evidence |
| --- | --- | --- | --- |
| FR-12-40 | Two menus: Main, Footer; collapsible groups | must | S103 |
| FR-12-41 | Item: name, link (page or custom URL), read-only slug, visible flag; one nesting level; drag reorder and re-parent | must (nest: should) | S103, S104, S225 |
| FR-12-42 | Add/Edit drawer; Add disabled until name and link valid | must | S104, S225 |
| FR-12-43 | Kebab actions: Edit, Visible toggle (instant), Rename, Copy link, View live page, Delete (confirm) | must | S224 |
| FR-12-44 | Home item flagged "HomePage" and not deletable | should | S103 |
| FR-12-45 | Item linking to a hidden/deleted page shows a warning and is not rendered publicly | must | derived |
| FR-12-46 | Hidden parent hides its children publicly (assumption; original unclear) | should | S224; open Q |
| ### Domains | |||
| ID | requirement | pri | evidence |
| --- | --- | --- | --- |
| FR-12-50 | Each site has a platform subdomain from the business slug, status Connected, primary by default | must | S106 |
| FR-12-51 | Connect existing domain: enter domain, show DNS records (CNAME/A + TXT), Verify, status Pending/Connected/Failed | should | S107 |
| FR-12-52 | Change primary domain (confirm); other domains redirect to primary; toggle redirect off (confirm warning) | could | S107b, S107c |
| FR-12-53 | TLS certificate issued automatically for connected domains | should | infra; open Q |
| ### Blog | |||
| ID | requirement | pri | evidence |
| --- | --- | --- | --- |
| FR-12-60 | Blog types CRUD (title); list with search, sort, "View posts"; delete warns that posts of that type are deleted too (cascade) | must | S109-S109e |
| FR-12-61 | Post: title, feature image (16:9 crop, <= 3 MB), rich text, author, type (required), visibility (visible/hidden/scheduled), SEO | must | S110 |
| FR-12-62 | Optional sub type, tags, "pin as featured" | could | S110 |
| FR-12-63 | Posts list: search, filters (status, type, tag, author), rows per page, select-all and bulk set visible/hidden/delete with confirm | should | S108 |
| FR-12-64 | Public blog page lists visible posts newest first, featured pinned first, filter by type | must | derived |
| FR-12-65 | Feature image required in original; we make it optional with a placeholder | should | S110 (deviation) |
| ### FAQ and Legal | |||
| ID | requirement | pri | evidence |
| --- | --- | --- | --- |
| FR-12-70 | FAQ topics: title, ordered Q&A (question, answer rich text, 250 char limit), add/remove Q&A, drag reorder topics and items | must | S111, S112 |
| FR-12-71 | Topic list with search; delete confirms that all Q&As go with it | must | S111d |
| FR-12-72 | Legal docs: title, rich text, status active/draft, updated by/at, search/filter | must | S113, S114 |
| FR-12-73 | Exactly one doc may be "Terms & Conditions"; setting another clears the previous; the checkout and signup consent link to it | should | S114, S113c |
| FR-12-74 | Privacy policy doc reference (flag) for footer/consent links | could | derived |
| ### Preferences | |||
| ID | requirement | pri | evidence |
| --- | --- | --- | --- |
| FR-12-80 | Fields: Meta Pixel ID, GA4 ID, GTM ID, TikTok Pixel ID, Meta domain verification, Google site verification; one Save; format validation (e.g. G-XXXX, GTM-XXXX, digits) |
should | S115 |
| FR-12-81 | Public site injects only validated IDs through our own templates (never raw script text) | must | S115; security |
| ### Public rendering | |||
| ID | requirement | pri | evidence |
| --- | --- | --- | --- |
| FR-12-90 | Server-rendered pages from published theme + live data; page-level meta, OpenGraph, canonical, sitemap.xml, robots | must | S100f |
| FR-12-91 | Header shows main menu, Login/Sign up (existing auth) and My bookings | must | S96 |
| FR-12-92 | Mobile-first responsive; theme tokens exposed as CSS variables | must | derived |
| FR-12-93 | Uploaded images resized/served via CDN-style variants; alt text field for images | should | derived |
7. Screens
| our screen | modelled on | states |
|---|---|---|
| Theme list | S95, S97 | filled, publish confirm, publishing, error |
| Customizer (shell, section panels, theme settings, font picker) | S96-S99, S210-S223 | loading preview, clean, dirty, saving, save error, preview load error |
| Pages list + SEO drawer + delete confirm | S100-S100g | empty custom, filled, filter-empty |
| Page add/edit | S101-S102 | empty (Add disabled), filled, scheduled, error |
| Navigation + add/edit drawer | S103, S104, S224, S225 | filled, empty menu, drag, link-broken warning |
| Domains, connect, change primary | S106, S107 | connected, pending, failed |
| Blog posts, types, post form | S108-S110g | empty, filled, selected (bulk), crop dialog |
| FAQ list/form | S111, S112 | empty, filled |
| Legal list/form | S113, S114 | empty, filled |
| Preferences | S115 | pristine, dirty, invalid ID |
Sub-folders (e.g. theme/, content/) can be split out at implementation if the PRD grows; kept single-file for now. |
8. Flows
- Customize and publish: Theme > Customize > section > edit > Save > Publish Theme (6 clicks; original ~same).
- Switch theme: Publish on library card > confirm (3 clicks).
- Custom page: Pages > Add > title + content > Add (4 clicks + typing); then Navigation > Add item > pick page (4 clicks). Improvement: after creating a page offer "Add to menu" (1 click).
- Hide menu item: kebab > Visible (2 clicks).
- First blog post: Types > Add type > back > Add blog (<= 12 clicks original; target 8 by allowing inline "create type" in the post form).
- Edge: deleting a page referenced by menu/buttons shows impact and clears/flags links; two admins editing the same theme: last save wins with "changed by someone else" warning (could).
9. Data model (guesses marked)
site(id, business_id, slug, primary_domain_id, live_theme_id).theme(id, site_id, name, base_template, settings jsonb, sections jsonb, status live|library, draft jsonb?, updated_at): settings = colours (6), fonts (2), social (5), favicon_asset, custom_css; sections keyed by section name with title, description, buttons[2]{label, link, outline}, image_asset, overlay{on, colour}, item_ids[]. JSON is validated by Go types (guess: JSONB vs relational).site_page(id, site_id, kind system|custom, system_key, title, slug unique(site), content_html, featured_asset, status visible|hidden|scheduled, publish_at, author_id, seo_title, seo_description, seo_asset, noindex, updated_at).menu(id, site_id, key main|footer),menu_item(id, menu_id, parent_id, position, name, page_id null, url null, visible).domain(id, site_id, host unique, kind platform|custom, status, verification_token, is_primary, redirect_to_primary, created_at).blog_type(id, site_id, title),blog_post(id, site_id, type_id, title, slug, image_asset, content_html, status, publish_at, author_id, sub_type, tags text[], featured, seo_*, updated_at).faq_topic(id, site_id, title, position),faq_item(id, topic_id, position, question, answer).legal_doc(id, site_id, title, content_html, status, is_terms, updated_by, updated_at).site_preference(site_id, meta_pixel, ga4, gtm, tiktok, meta_verification, google_verification).- Assets reference a shared media table (owned by a platform PRD).
Domain packages singular:
site,page,blog,faq,legal(or onesitepackage; decide at build).
10. Business rules & edge cases
- Rich text is sanitised on save with an allow-list; custom CSS stripped of
@import/url(javascript:); no custom JS ever. - Slugs: lowercase, hyphenated, unique per site, reserved words blocked (system routes,
admin,api). - Deleting a blog type deletes its posts (explicit confirm showing count) vs safer reassign; we choose confirm with count (original cascades).
- Terms flag unique per site.
- Scheduled items publish via a job or evaluated at read time (read-time compare, no job needed).
- Contrast: warn when button text vs background contrast < 4.5:1.
- Item pickers show only active, publicly bookable items; a picked item later deleted is silently skipped.
- Changing the business slug changes the platform subdomain; old one redirects for 90 days (open Q).
- A site with no classes still renders without empty sections.
11. Permissions
| action | owner | admin | staff | front desk |
|---|---|---|---|---|
| Theme, domains, preferences, custom CSS | edit | edit | none | none |
| Pages, nav, blog, FAQ, legal | edit | edit | edit if "Online store" granted | none |
| View | all | all | granted | none |
All admin routes in use: [auth] group; public site routes are deliberately public and read-only. |
12. Acceptance criteria
- Given a new business, when the public URL is opened, then the home page renders with default theme and all 11 system pages are reachable.
- Given an edited theme not yet published, when a visitor loads the site, then they see the old version.
- Given a library theme, when Publish is confirmed, then it is live and the previous live theme appears in the library.
- Given a button with blank label, then it does not render.
- Given an invalid hex colour, then Save is blocked and the field shows the error.
- Given a custom page set to hidden, when its URL is visited, then 404 and it is absent from sitemap.xml.
- Given a page scheduled for tomorrow, then it is not visible today and visible after that time.
- Given a system page, then the menu offers only SEO settings and no delete.
- Given a SEO title of 66 chars, then input is limited/blocked at 65.
- Given a menu item hidden, then it is greyed in admin and absent publicly.
- Given a blog type with 3 posts, when deleting it, then the confirm states 3 posts will be deleted; after confirm, none remain.
- Given two legal docs, when doc B is set as Terms, then doc A's flag is cleared.
- Given GTM ID
abc, then Save fails with a format error; givenGTM-ABC123, the public pages include the GTM snippet. - Given a pasted
<script>in rich text, then it is stripped on save. - Given a connected custom domain with redirect on, when the platform subdomain is requested, then it 301s to the primary.
13. Dependencies & open questions
- Depends on classes/appointments/courses/packages/memberships/locations/staff data for sections and system pages; auth for login CTA; media storage; notifications for contact-form submissions (Contact us page form; staff event "new contact form submission").
- Open: draft vs live semantics when editing the live theme (original unobserved); hero gallery behaviour; eye "hide" effect; Rename/Delete nav dialogs; domain verification and TLS strategy; hosting model (shared renderer by Host header assumed); theme count at launch; plan gating of custom domain, custom CSS and extra themes (pricing tier: TBD); whether hidden parent hides children; blog comment/RSS (not observed); legal doc versioning; storefront checkout belongs to which PRD; slug-change redirects; rate/size limits for media.
- Recon gaps: sections' real output on the public site (part 07 only covers guest pages), Save/Publish network behaviour, domain wizard beyond step 1.
14. Evidence
See evidence/evidence.md.
