Building the Sailor Board: From Marketing Copy to Feature—Adding Photo Upload, Moderation Gates, and Instagram Integration to Guest Event Pages
What Was Done
Over this session, we transformed Keely's guest event page from a static landing page into a fully functional photo-sharing gallery with spam prevention, batch uploads, and same-day Instagram post integration. The "Sailor Board" went from being flavor text in email confirmations to an actual interactive feature deployed on the `/g/{event_id}` route. We also fixed the booking modal to open inline instead of bouncing users back to the homepage.
The Starting Point: Marketing Copy Without Substance
The "Sailor Board" copy existed in three places across the Lambda function at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py:
- Line 2030: upload-success message — "Your photo is live. Welcome to the sailor board."
- Line 2066: moderation email link — "View sailor board"
- Lines 2107, 2433: moderation reply copy referencing the sailor board concept
But there was no actual board. The copy pointed to the guest's own `/g/{event_id}` page, which had no upload UI, no photo grid, no moderation flow. It was a promise with no delivery.
Feature Set Built
Photo/Video Upload with Format Validation
The guest page now accepts multiple file formats simultaneously:
- Images: JPEG, PNG, WebP, HEIC, HEIF
- Videos: MP4, MOV, AVI, WebM
- Batch limit: 24 files at once (enforced client-side with
Array.from(files).slice(0, 24))
The file input element (#file-input, line 362) triggers handleFiles() (line 514), which validates format and initiates uploads to the S3 bucket for the charter event.
Spam Prevention via Event Code
To prevent strangers from flooding charter galleries, we implemented a two-tier moderation system:
- With event code: Upload publishes immediately to the photo grid
- Without event code: Upload goes to a JADA review queue for human moderation
The code input (#g-code, line 354) is validated in readCode() (line 424). The UX copy explains: "The code keeps strangers from posting to your charter page." This code is distributed to charter guests via email and SMS before the event, creating a lightweight but effective verification gate.
Same-Day Instagram Hashtag Integration
The page fetches Instagram posts tagged with #jada or #queenofsandiego posted on the same day as the charter. The server-side handler (shipcaptaincrew Lambda) queries the Instagram Graph API (filtered by event date), and the client renders results in #ig-grid (lines 452–459).
This creates a hybrid experience: official uploads (with code) + guest uploads (moderated) + organic Instagram mentions all surface on the same page.
Technical Architecture
File Structure
The guest page is generated and deployed as:
- Source (local):
/Users/cb/Documents/repos/sites/queenofsandiego.com/sailor-board/index.html - Deployed to S3:
s3://queenofsandiego.com/g/{YYYY-MM-DD-captain-name}/index.html(public read) - CloudFront alias:
https://queenofsandiego.com/g/{YYYY-MM-DD-captain-name}
The page is a single-file application: all JavaScript, styling, and HTML are inlined. This avoids additional HTTP round trips and keeps deployment atomic.
Backend: shipcaptaincrew Lambda
The Lambda function at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py handles:
- Photo list endpoint:
GET /api/g/{event_id}/photos— returns approved uploads + same-day Instagram posts (serialized asd.instagramJSON) - Upload handler: accepts multipart file uploads, validates event code, routes to immediate publish or review queue
- Moderation workflow: queues unauthenticated uploads, sends moderator email with approve/reject links
The Lambda is deployed via CloudFormation and exposed through API Gateway at https://shipcaptaincrew.queenofsandiego.com/ (Route53 CNAME).
Mode Switching: Pre-Sail vs. Post-Sail
The page has a built-in mode toggle at FLIP_UTC (hardcoded per event). Before the sail, it shows event info and booking CTA. After the sail, it shows the upload form and photo grid. For Keely's event (2026-05-24), the flip time was May 25 00:00 UTC (5 PM PT previous day).
Key Infrastructure Changes
S3 Buckets
- Public gallery bucket:
queenofsandiego.com— stores guest pages at `/g/{event_id}/` prefix; CloudFront caches with 1-hour TTL - Upload staging bucket:
qos-upload-staging— temporary storage for unmoderated uploads pending approval - Archive bucket:
qos-archives— long-term storage for approved photos and event metadata
CloudFront Distribution
The staging distribution (used during development) has ID E... (redacted). After testing, the page was invalidated with:
aws cloudfront create-invalidation --distribution-id E... --paths "/g/2026-05-24-keely-afternoon/*"
This purged the CloudFront cache and ensured the updated page was served immediately.
Lambda Deployment
The shipcaptaincrew function was updated with new photo-handler logic and redeployed via ZIP archive:
zip -r lambda_deploy.zip lambda_function.py py_vapid/ && \
aws lambda update-function-code --function-name shipcaptaincrew --zip-file fileb://lambda_deploy.zip
The py_vapid module was bundled to support push notifications (for photo upload confirmations sent to guests).
The Booking Modal Fix
Previously, clicking "Book a Sail" on the guest page returned users to the homepage. We updated the CTA to trigger the booking modal directly by passing a deep-link parameter: