Building the Sailor Board: From Marketing Copy to Feature — Photo Upload & Same-Day Instagram Integration
Last week, we deployed a guest experience page for the Queen of San Diego's charter events, complete with photo uploads, spam-gating, and Instagram hashtag integration. Today's work was about making "Sailor Board" — which started as marketing flavor text — into an actual, fully-featured product.
What Was Done
- Audited Keely's guest page (
https://queenofsandiego.com/g/2026-05-24-keely-afternoon) for feature completeness - Verified all four core capabilities: file uploads (JPEG/PNG/WebP/HEIC/HEIF/MP4/MOV/AVI/WebM), event code spam-gating, 24-at-once upload cap, and same-day Instagram hashtag scraping
- Discovered "Sailor Board" was messaging-only; no aggregate cross-charter view existed
- Built the Sailor Board as a new per-event photo gallery and moderation hub
- Fixed "Book a Sail" button to open booking modal directly instead of navigating to homepage
- Created thumbnail backfill pipeline to pre-generate optimized image sizes for gallery performance
Technical Details: The Guest Page Stack
File Structure & Deployment:
- Source:
/Users/cb/Documents/repos/sites/queenofsandiego.com/g/(no local copy at start — recovered from S3 prod after deployment) - Prod URL:
s3://queenofsandiego.com/g/2026-05-24-keely-afternoon/index.html(22.5 KB) - CloudFront:
d1a2b3c4d5e6f7.cloudfront.net(prod distribution, immediate cache invalidation on deploy) - Staging test: Deployed to separate CloudFront alias before prod push for end-to-end smoke tests
Core Markup & JavaScript:
The guest page is a single-file progressive web app. Key elements:
#file-input(line 362): Accepts files via drag-and-drop or native file picker. Filtering logic inhandleFiles()(line 514) enforces MIME type whitelist and 24-file max.#g-codeinput (line 354): Guest code (event passcode). If entered, uploads bypass moderation queue and publish instantly. Missing code routes uploads to review queue with copy: "The code keeps strangers from posting to your charter page."#photo-grid(lines 452–459): Renders photos from server response. Instagram feed (d.instagram) rendered above user uploads.#ig-grid: Dedicated Instagram hashtag results section. Populated server-side by Lambda, displayed client-side as readonly gallery.
Upload Flow:
Client: File drag → handleFiles() → uploadFile(file, code)
↓
Lambda presigned URL: POST /api/g/{event_id}/presign
↓
S3 direct upload: PUT {presigned_url}
↓
Lambda notification: POST /api/g/{event_id}/upload-complete
↓
DynamoDB: Insert photo record (status: "pending" or "live" based on code)
↓
Client: Poll GET /api/g/{event_id}/photos until photo appears
The Sailor Board Feature
"Sailor Board" started as three lines of copy in the Lambda function ("Your photo is live. Welcome to the sailor board"). We realized guests expected an actual photo gallery showing submissions in real time.
What We Built:
- Per-event gallery:
/g/{event_id}/index.html— live, searchable photo + video gallery for each charter - Real-time updates: Client polls
GET /api/g/{event_id}/photosevery 3 seconds; appends new photos to grid without page refresh - Instagram integration: Same API endpoint returns
d.instagramarray (filtered to event date and #jada or #queenofsandiego hashtags). Rendered above user uploads. - Moderation link: Admin email copies include "View sailor board" link pointing to the gallery, so moderators can see submissions in context
File Paths:
/sites/queenofsandiego.com/sailor-board/index.html— Gallery template (newly created)/sites/queenofsandiego.com/booking-widget.js— Extracted booking modal code; embedded in sailor-board page to support quick "Book a Sail" action from gallery view/tools/shipcaptaincrew/lambda_function.py— Handles/api/g/{event_id}/photosendpoint, returns photo metadata + Instagram feed
Infrastructure & Deployment
Lambda Function Updates:
- Function name:
shipcaptaincrew(us-west-2) - Endpoint: API Gateway routes
POST /api/g/{event_id}/presign,POST /api/g/{event_id}/upload-complete,GET /api/g/{event_id}/photosto this function - Changes: Added Instagram hashtag scraping logic, refactored photo schema to include thumbnail URLs, added guest code validation
- Deployment: Zipped function + dependencies (including
py_vapidfor push notifications), deployed viaaws lambda update-function-code --function-name shipcaptaincrew --zip-file fileb://deploy.zip - Size constraint: Kept function code + bundled deps under 50 MB (Lambda direct upload limit)
S3 Structure:
- Bucket:
queenofsandiego.com - Photo uploads:
s3://queenofsandiego.com/uploads/{event_id}/{photo_id}.{ext} - Thumbnails:
s3://queenofsandiego.com/thumbs/{event_id}/{photo_id}_480w.jpg(backfill pipeline pre-generates these) - Guest pages:
s3://queenofsandiego.com/g/{event_id}/index.html(deployed via CloudFormation or manual upload) - CORS config: Updated to allow
queenofsandiego.comorigins (presigned POST/PUT requests from browser)