Community Sign-Up Page SOP
Every step to stand up a branded community sign-up page such as
sunlakes.yourpremiervacation.com. One tab per phase, in the order you do them. Last verified against live code and the database on 09/22/2026.The Short Answer
| Question | Answer |
|---|---|
| Are the pages in this repo? | There is no page per community. There is one page, ep-trip-signup.html, which renders whichever community the hostname names. |
| Why in platform-mockup? | Because everything that fills it is already here: the community record, the trip, the itinerary, the photos, the pricing grid, the Stripe endpoints and the admin screens that edit them. A separate repo would need a copy of all of it. |
| How does the domain reach it? | Both hostnames are custom domains on the same Cloudflare Pages project (platform-mockup). They are not separate sites and not separate deploys. |
| What turns the root into the form? | functions/index.js rewrites the root of a known hostname to /ep-trip-signup. A rewrite, not a redirect, so the clean address stays in the bar. |
| Does a new community need new code? | Only two lines, both hostname maps (tab 2). Content, branding, pricing and photos are all data you type in the admin. |
The Request Path, End To End
| # | Step | What Happens |
|---|---|---|
| 1 | Resident opens sunlakes.yourpremiervacation.com | DNS points the hostname at Cloudflare Pages. |
| 2 | Cloudflare serves the platform-mockup project | The hostname is attached to this project as a custom domain. |
| 3 | functions/index.js matches the host | HOST_TO_GROUP recognises it and fetches /ep-trip-signup in place. |
| 4 | ep-trip-signup.html loads | No query string arrived, so its own HOST_GROUP map derives the slug sun-lakes from the hostname. |
| 5 | Page calls /api/ep-trip-signup-init?group=sun-lakes | Returns the community branding plus every active, non-draft trip whose signup_group is that slug. |
| 6 | Template renders | One trip renders Template 1, two or more render Template 2. |
| 7 | Resident pays | ep-trip-signup-intent charges the deposit, ep-trip-signup-confirm schedules the balance and mirrors to Airtable Purple. |
What Is Live Today
| Community | Slug | Public Domain | Live |
|---|---|---|---|
| Quail Creek | quail-creek | quailcreek.yourpremiervacation.com | Live |
| Sun Lakes | sun-lakes | sunlakes.yourpremiervacation.com | Live |
Create The Community Record
| # | Where | Do This |
|---|---|---|
| 1 | Event Planning › Neighborhoods | Click + Add Neighborhood. Type the Name only, then save. |
| 2 | Neighborhoods list | Click the new row. It opens the Community detail wizard. |
| 3 | Step 1 · Community | Fill Name, Slug, Departure Address, City, State, Zip. Save. |
| 4 | Step 2 · Sign-Up Page | Fill the four sub-tabs below. Save. |
| 5 | Step 3 · Status | Leave Go Live OFF for now. Turn it on in tab 7, after the page is tested. |
Step 1 · Community Fields
| Field | Rule |
|---|---|
| Name | Required. Appears in every picker and inside the merged trip name (Sun Lakes - Las Vegas and The Sphere). |
| Slug | Lower case, hyphenated, permanent (sun-lakes). This is the join key the whole stack uses. Changing it re-homes the page and breaks any trip still pointed at the old one. |
| Departure Address / City / State / Zip | Where the coach leaves from. Used by the trip, not by the public page header. |
Step 2 · Header Sub-Tab
| Field | Where It Shows |
|---|---|
| Brand Pill | Small badge top-left of the navy header band. Example: Sun Lakes Community. |
| Headline | Centre of the header band. Example: Make It a Season to Remember. |
| Intro Paragraph | The "About" band on a multi-trip page. A single-trip page uses the trip's own About text instead. |
Step 2 · Concierge Sub-Tab
| Field | Rule |
|---|---|
| Phone | Displayed exactly as typed, including the parentheses. |
| Dial Number | What tapping it dials. Digits with country code, no spaces: +16022161168. |
| Note | The line under the phone number in the hero and on the confirmation. |
Step 2 · Footer Sub-Tab
| Field | Rule |
|---|---|
| Disclaimers | One per block, blank line between them. Type the leading * or ** yourself so it matches the badge it explains. |
| Logo Row Caption | Example: Presented in cooperation with. |
| Community Logo | URL of a transparent PNG. Your Premier Vacation and Club Travelo sit either side automatically. A square logo auto-bumps to 76px; a wide one stays 50px. |
| Logo Alt Text | Screen-reader text. |
| Discovery Discount | Tick if this community gets the Discovery pricing tier. Untick and the discount column never renders. |
⚠ Known Gap
| Gap | What To Do Today |
|---|---|
| There is no logo uploader on the community editor, only a Logo URL text box. | Either paste an external URL the community already hosts (Quail Creek does this), or hand Claude the file and it gets committed to assets/ and the path pasted in (Sun Lakes does this: /assets/logo-sunlakes.png). |
Five Places, All Required
| # | Where | Do This | Who |
|---|---|---|---|
| 1 | DNS for yourpremiervacation.com | Add a CNAME. Host = the subdomain (sunlakes). Value = platform-mockup.pages.dev. | Mia |
| 2 | Cloudflare › Pages › platform-mockup › Custom domains | Set up a domain › type the full hostname › Activate. Wait for the certificate to read Active. | Mia |
| 3 | functions/index.js › HOST_TO_GROUP | Add 'sunlakes.yourpremiervacation.com': 'sun-lakes'. This is what rewrites the root to the form. | Claude |
| 4 | ep-trip-signup.html › HOST_GROUP (~line 592) | Add the same pair. The rewrite drops the query string, so the page derives its own slug from the hostname. | Claude |
| 5 | Community detail › Step 3 › Public Domain | Type the host only. No https://, no trailing slash. Save. | Mia |
What Each One Actually Does
| Place | Breaks If Missing |
|---|---|
| DNS CNAME | The hostname does not resolve at all. |
| Cloudflare custom domain | Resolves but Cloudflare answers 404 or a certificate error. |
functions/index.js | The root serves the internal Command Central home page instead of the sign-up form. |
ep-trip-signup.html | The form loads blank because it has no slug to ask the API for. |
public_domain column | Everything still works for residents, but every "View Live Page" link in the admin points at the in-app URL instead of the branded one. |
⚠ Known Gap
| Gap | Consequence |
|---|---|
Steps 3 and 4 keep their own hardcoded copy of the host map. The public_domain column does not drive routing. | Typing a domain in the admin does not create it. Steps 1 to 4 must all be done as well, and steps 3 and 4 need a deploy (tab 8). |
| The admin editor says this on screen, under the Public Domain box. | Believe it. This is the single most common way a new community half-launches. |
Optional · Vanity Paths
| File | Do This |
|---|---|
_redirects | Add /sun-lakes /ep-trip-signup?group=sun-lakes 302 if you want a short path on the main site too. Useful for a flyer or an email before the subdomain exists. |
The Event Comes First
| Why | Detail |
|---|---|
| The Trip wizard picks from existing events. | You cannot create a trip for an event that does not exist yet. Build the event, then the trip. |
| The event carries the seats. | capacity and booked_count live on the event, not the trip. The sign-up page reads them through the trip's linked event. |
| The event carries the itinerary rows. | You edit them from the Trip, but they are stored against the event. |
| The event carries the community. | neighborhood_id is what builds the merged name Sun Lakes - Las Vegas and The Sphere on both the event and the trip. |
Create The Event
| # | Where | Do This |
|---|---|---|
| 1 | Event Planning › Events | Click + Add Event. |
| 2 | Step 1 · Details | Set Event Type = Neighborhood Trip. The Neighborhood dropdown only appears for this type. |
| 3 | Step 1 · Details | Pick the Neighborhood you created in tab 1. |
| 4 | Step 1 · Details | Type the Event Name without the community in it. The platform prefixes it: Sun Lakes - Las Vegas and The Sphere. The live Calc Name preview shows you the result. |
| 5 | Step 1 · Details | Set Start Date and End Date. |
| 6 | Step 1 · Details | Set Capacity. Blank means unlimited. Capacity is reservations, not headcount — a double room is one seat, not two. |
| 7 | Save | Create the event. Note its id if you will need it later. |
What You Do NOT Set Here
| Field | Why Not |
|---|---|
| Active for Forms | That flag drives the call-centre RSVP seminar forms, not the trip sign-up page. The sign-up page never reads it. Leaving it off changes nothing for residents. |
| Itinerary | The Event editor's itinerary step is deliberately hidden for trip-linked events. You edit it from the Trip, in one place (tab 5). |
Create The Trip
| # | Where | Do This |
|---|---|---|
| 1 | Event Planning › Trips | Click + New Trip. |
| 2 | Step 1 · Pick Events | Tick the Neighborhood Trip event you just made. City, State and the date range fill themselves from it. |
| 3 | Step 2 · Trip Details | Set Company and a Trip Nickname. Trip Name is generated, not typed. |
| 4 | Step 2 · Trip Details | Click Create Trip, not Save Draft. A draft is invisible to the public page. |
The Four Conditions A Trip Must Meet To Appear
| Condition | Where It Is Set |
|---|---|
signup_group = the community slug | Trip detail › Step 5 › Details › Community dropdown. |
active = true | Trip detail › Step 6 › Status › Active. |
is_draft = false | Set by clicking Create Trip at the end of the New Trip wizard. |
discarded_at is empty | Not discarded. Nothing to do unless someone discarded it. |
If The Page Renders But The Trip Is Missing
| # | Check, In This Order |
|---|---|
| 1 | Step 5 › Details › Community is set to the right community, not "None". |
| 2 | Step 6 › Active is ticked. |
| 3 | The trip was finished with Create Trip, not left as a draft. |
| 4 | Open /api/ep-trip-signup-init?group=<slug> in a browser. If the trip is not in that JSON, it is a data problem. If it is, it is a rendering problem. |
Optional · Staff
| Where | Do This |
|---|---|
| Step 4 · Staff | Tick who from the team is travelling. Internal only. Nothing on the public page reads it. |
Trip Detail · Step 5 · Details
| # | Field | What To Enter |
|---|---|---|
| 1 | Sign-Up Template | Template 1 - Single Trip or Template 2 - Multiple Trips. Leave on Auto and it picks by trip count. A template is a reusable layout a trip selects, like a WordPress template — it is not owned by a community. |
| 2 | Community | The community whose page this trip appears on. This is the single field that publishes the trip to a page. |
| 3 | Sign-Up Title | The customer-facing trip title. Falls back to Trip Name if blank. Example: Las Vegas and The Sphere. |
| 4 | About This Trip | The marketing band on a single-trip page. Blank line between paragraphs. First paragraph renders as the lead, last as an italic closer. |
| 5 | Highlights | One bullet per line. This is the "what's included" list. |
| 6 | Transport Note | Example: Round-trip transport to and from Sun Lakes Country Club included. |
| 7 | Test Charge Amount | Whole dollars. Used only by ?test and ?livetest. Set it to 10 before testing (tab 6). Harmless to leave set. |
Gotcha
| Symptom | Cause |
|---|---|
| Two trips on one page each pick a different template. | Unresolved. Keep both trips on the same template, or leave both on Auto. |
Trip Detail · Step 5 · Photos
| # | Where | Do This |
|---|---|---|
| 1 | Photos tab | Click Upload and pick any normal photo. It does not need to be pre-cropped. |
| 2 | Crop window | Drag and zoom to frame it. The tool outputs the banner shape for you. |
| 3 | Thumbnail strip | Hover a photo to reorder or remove. The first one is the first slide. |
| 4 | Save | Save Changes on the wizard. The carousel updates on the live page immediately. |
Image Rules
| Rule | Detail |
|---|---|
| Correct upload size | 2400 × 800, a 3:1 ratio. The banner box is a fixed 3:1, so a 3:1 image fills it with zero crop at every screen width. |
| Old 2:1 images | A 2000 × 1000 image cover-crops about a third in a 3:1 box. Re-run it through the crop tool rather than living with it. |
| How many | The carousel comfortably holds 7. |
| Empty gallery | The carousel hides itself entirely. No broken box. |
| Partner logos | Transparent PNG, about 160px tall. Those go on the community record (tab 1), not here. |
Trip Detail · Step 5 · Itinerary
| # | Where | Do This |
|---|---|---|
| 1 | Itinerary tab | Click + Add Itinerary Line, one per day. |
| 2 | Each line | Set the real date, a short title (Travel Day, Free Day, Departure), and the detail. Times belong inside the detail text. |
| 3 | Order | Sort order drives the display. Keep it in day order. |
Rules
| Rule | Detail |
|---|---|
| Edit it here, only here | The Event editor's itinerary step redirects trip-linked events back to the trip on purpose. The whole sign-up page is built in one place. |
| Where it really lives | ep_event_itinerary, against the trip's single event. That is plumbing, not something you manage. |
| ⚠ Drift is invisible | The table has no updated_at, so nobody can tell later whether a line was edited. When the wording matters, diff it against the owner's source text rather than trusting that it looks detailed. |
| Departure point | Name the real place in day 1 (Sun Lakes Country Club), and match it in the Transport Note. A placeholder left in either one ships to residents. |
Trip Detail · Step 5 · Pricing
| # | Where | Do This |
|---|---|---|
| 1 | Pricing tab | Click + Add Pricing Option, one row per occupancy type. |
| 2 | The row | Type every value directly in the row. There is no control above or outside the grid. |
| 3 | Save | Save the row. The Balance Due date recomputes as you type the days. |
The Grid, Column By Column
| Column | What It Means |
|---|---|
| Occupancy Label | What the resident reads. Example: Double Occupancy. |
| Occupancy Code | The internal key. Example: double. If no row is coded double, the page pre-selects the first row instead. |
| Retail Price | Whole dollars, per the occupancy, not per person. |
| Deposit | Whole dollars charged today. Typed, never computed. There is no deposit percentage anywhere in this platform. |
| Discovery Discount | Whole dollars off, per occupancy. Shows only when it is above zero and the community has Discovery ticked. |
| Balance Due | Type the number of days before the trip. The due date is computed from the trip start date and shown to you and to the resident. |
| Active | Untick to retire a tier without deleting it. |
⚠ Do Not
| Never | Why |
|---|---|
| Reintroduce a trip-level deposit percent or a header control above the grid. | The whole point of the grid is flexibility per occupancy type. Trip-level signup_deposit_percent and signup_balance_days_before still exist as dead columns; nothing edits or reads them. |
| Assume a price is right because it looks right. | Prices have gone out of sync with the flyer more than once. Check the row against the flyer before the page goes live. |
The Four URL Switches
| Add To The URL | What It Does | Money |
|---|---|---|
?preview | Shows the page before Go Live is switched on. The only way the team reviews a page pre-launch. | n/a |
?test | Full checkout on the YPV test Stripe keys. Card 4242 4242 4242 4242, any future expiry, any CVC. | Fake |
?livetest | A real card on the live Stripe keys, shrunk to the trip's Test Charge Amount. Red banner on screen. This is the only way to prove the live keys, the balance schedule and the Airtable mirror all work. | Real |
?override | Team only, unpublished. Registers someone past a sold-out cap and converts their wait-list row in place. Never put this on a resident-facing link. | Real |
Test Sequence
| # | Do This | Looking For |
|---|---|---|
| 1 | Open https://<domain>/?preview | Header, photos, About, itinerary, phone and pricing all render with real content and no placeholders. |
| 2 | Open it on a phone | Nothing overflows sideways, the hero pins on scroll. |
| 3 | Open ?preview&test and book with card 4242 | You reach the confirmation screen with the right amounts. |
| 4 | Check Supabase ep_trip_signups | A new row, status=paid, is_test=true. |
| 5 | Set Test Charge Amount to 10, then open ?preview&livetest and pay with a real card | $10 charged. |
| 6 | Check the same row | stripe_mode='live', stripe_subscription_schedule_id populated, final_status='scheduled'. |
| 7 | Check Airtable Purple | A Jotform Submits record and a Check In Manifest record, with the discrete address fields filled. |
| 8 | Refund the $10 in Stripe | And cancel the scheduled balance subscription so it never charges. |
⚠ Test Rules
| Rule | Why |
|---|---|
| Never run an unflagged paid test. | A plain booking charges the full retail price on live keys. |
| Test bookings never take a seat. | ?test and ?livetest rows are recorded but not counted, so testing cannot sell out a real trip. |
| A confirmation screen is not proof. | The deposit and the balance schedule are two different calls. Check the database row, not the screen. |
Pre-Flight, Every Box Ticked
| # | Check |
|---|---|
| 1 | DNS, Cloudflare custom domain, both host maps and Public Domain are all done (tab 2), and the code half is deployed (tab 8). |
| 2 | Brand pill, headline, concierge phone, disclaimers and logo are the community's real ones. |
| 3 | Every price in the grid matches the flyer, tier by tier. |
| 4 | Deposit and Balance Due days are typed on every active tier. |
| 5 | Capacity is set on the event, or deliberately left blank for uncapped. |
| 6 | No placeholder text survives in the itinerary, the Transport Note or the About band. |
| 7 | Photos are 3:1 and in the order you want. |
| 8 | A ?livetest booking produced a paid row with a balance schedule and reached Airtable. |
Publish
| # | Where | Do This | Who |
|---|---|---|---|
| 1 | Community detail › Step 3 › Status | Tick Go Live. Save. | Admin only |
| 2 | Neighborhoods list | The Page badge flips from Not Live to Live. | — |
| 3 | A private browser window | Open the bare domain with no query string. The form renders. | Mia |
What Go Live Does And Does Not Do
| Does | Does Not |
|---|---|
| Removes the "not open yet" gate so anyone can see the page. | Touch DNS, create a domain, or deploy anything. |
| Is enforced on the server as well as in the UI. | Publish a trip. A trip appears because of its Community field, Active and not-draft. |
| Is locked to admins. Everyone else sees the toggle disabled. | Take the page down cleanly if a resident already has the link. Untick it and they see "not open yet". |
Taking A Page Down
| Goal | Do This |
|---|---|
| Close the whole community | Untick Go Live. |
| Pull one trip, leave the page up | Untick that trip's Active on Step 6, or clear its Community on Step 5. |
| Stop sales but keep interest | Fill the capacity. The page flips to Sold Out and offers the wait list. |
Deploy, When Code Changed
| # | Command | Notes |
|---|---|---|
| 1 | Set-Location "C:\Users\apps\Claude Projects\command-central\Platform\platform-mockup" | One command per block. Never chained. |
| 2 | npm run deploy | Stamps the page dates, then pushes to Cloudflare Pages project platform-mockup, branch main. |
| 3 | git push origin main | Repo is clubtravelo/platform-mockup. Claude does this, not Mia. |
When A Deploy Is Actually Needed
| Change | Deploy? |
|---|---|
| Anything typed in the admin: branding, prices, photos, itinerary, Go Live, Public Domain | No. The page reads the database live. Refresh and it is there. |
The two host maps, _redirects, or any page or function file | Yes. |
| A new database column | Yes, plus the column must be added to the write allowlist or the save silently drops it. |
Verify After Deploy
| # | Check |
|---|---|
| 1 | Open https://<domain>/. The sign-up form renders, not the Command Central home page. If it is the home page, the functions/index.js map did not ship. |
| 2 | The address bar still shows the clean root. It is a rewrite; a redirect would mean the router changed. |
| 3 | Open https://<domain>/api/ep-trip-signup-init?group=<slug>. The community and every expected trip are in the JSON. |
| 4 | Console is clean. |
End Of Session
| Step | Command |
|---|---|
| Back everything up | powershell -NoProfile -ExecutionPolicy Bypass -File "C:\Users\apps\claude-workspace\scripts\sync-backup.ps1" |
Database
| Table | Holds |
|---|---|
ep_neighborhoods | The community. Slug, departure address, all branding, is_live, public_domain, has_discovery, disclaimers. |
ep_events | The event. neighborhood_id, capacity, booked_count, trip_id. |
ep_event_itinerary | The day-by-day lines. No updated_at, so drift cannot be detected after the fact. |
ep_trips | The trip. signup_group, signup_template, signup_title, signup_description, signup_highlights, signup_gallery, signup_transport_note, signup_test_amount, active, is_draft. |
ep_trip_signup_options | One row per occupancy tier. occupancy, occupancy_label, full_price, deposit_price, discovery_discount, balance_days_before, active. |
ep_signup_templates | The template registry the Step 5 dropdown reads. |
ep_trip_signups | Every booking and wait-list entry. status, is_test, stripe_mode, stripe_subscription_schedule_id, final_status. |
Dead Columns · Leave Alone
| Column | Status |
|---|---|
ep_trips.signup_deposit_percent | Dead. Deposits are typed per occupancy. |
ep_trips.signup_balance_days_before | Dead. Balance days are typed per occupancy. |
ep_trips.signup_discovery_discount | Trip-level fallback only. Not edited in the UI. |
ep_signup_communities (whole table) | Merged into ep_neighborhoods. Unread. Not yet dropped. |
Files That Matter
| File | Role |
|---|---|
functions/index.js | The root router. HOST_TO_GROUP rewrites a known hostname's root to /ep-trip-signup. |
ep-trip-signup.html | The one public page. Holds both templates as <template id="tpl-single"> and tpl-multi, its own HOST_GROUP map (~line 592), the Stripe checkout and the confirmation view. |
_redirects | Vanity paths, plus the standing redirect that rescues the mockup URL Quail Creek published by mistake. |
style.css | The contract. Find the existing class before writing any new UI. |
signup-design/ | Self-contained design mockups with dead payment forms. Reference only. Publicly reachable, so never hand one to a resident. |
Pages Functions
| Endpoint | Does |
|---|---|
/api/ep-trip-signup-init | Public read. Community branding plus every active, non-draft trip on that slug, with options, itinerary and seat counts. |
/api/ep-trip-signup-intent | Creates the Stripe PaymentIntent, resolves remaining seats server-side, writes the pending row, and bridges a wait-list entry to Airtable Purple. |
/api/ep-trip-signup-confirm | Verifies the charge, schedules the balance subscription, counts the seat, mirrors the paid booking to Purple. |
/api/ep-trip-update | Saves Step 5. Fields must be on its allowlist or they are silently dropped. |
/api/ep-trip-signup-option-save · -delete | The pricing grid rows. |
/api/ep-trip-gallery-upload | The cropped banner upload into the signup-gallery bucket. |
/api/lookup-create · lookup-update | The community record. Also allowlisted, including public_domain and the admin-only is_live. |
Admin Screens
| Screen | Edits |
|---|---|
| Neighborhoods | The community list, with the Live badge. |
| Community detail | Three steps: Community, Sign-Up Page (Header / Concierge / Footer / Trips On This Page), Status (Active, Go Live, Public Domain). |
| Events | The event, its type, neighborhood and capacity. |
| Trips | Six steps: Basics, Location & Dates, Events, Staff, Sign-Up Page (Details / Photos / Itinerary / Pricing), Status. |
Live URLs
| Purpose | URL |
|---|---|
| Quail Creek | https://quailcreek.yourpremiervacation.com/ |
| Sun Lakes | https://sunlakes.yourpremiervacation.com/ |
| In-app fallback | /ep-trip-signup?group=<slug> |
| Data check | /api/ep-trip-signup-init?group=<slug> |