Building a Real-Time Event Photo Gallery: Keely's Guest Page Architecture & the Sailor Board Feature
What Was Done
This session focused on shipping Keely's guest charter page — a single-event photo upload and gallery system — and laying groundwork for an aggregate "Sailor Board" feature. The guest page went live on production S3 post-event (2026-05-25 02:11 UTC) and includes four core capabilities: photo/video uploads with a 24-at-once cap, event-code gating to prevent spam, same-day Instagram hashtag integration, and moderation-queue handling. The session also involved shipping a booking-widget refactor to fix a critical UX bug: "Book a Sail" now opens the scheduling modal inline instead of bouncing users back to the homepage.
Technical Architecture: Guest Page Stack
Keely's page lives at a predictable S3 path:
/queenofsandiego.com/g/2026-05-24-keely-afternoon/index.html
The architecture layers are:
- Static HTML shell (22.5 KB, served from CloudFront / S3)
- Feature toggle:
FLIP_UTC = "2026-05-25T00:00:00Z"controls pre-sail (upload hidden) vs. post-sail (upload visible) - Time-based mode switch handles the 5 PM PT event boundary
- Feature toggle:
- Client-side upload handler (lines 514–550 in Keely's page)
- Accepts: JPEG, PNG, WebP, HEIC/HEIF, MP4, MOV, AVI, WebM
- Validates file count:
Array.from(files).slice(0, 24).forEach(uploadFile) - Presign request flow: POST to
/api/g/{event_id}/presign(Lambda endpoint)
- Lambda photo handler (
/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py, function:shipcaptaincrew)- Presign endpoint: Validates event code, generates signed S3 URLs for client-side direct upload
- Photo listing endpoint:
GET /api/g/{event_id}/photosreturns approved photo metadata + Instagram feed - Metadata store: DynamoDB table (implicit in Lambda code, likely
QueenofsandiegoPhotosor similar)
- S3 bucket hierarchy
s3://qos-guest-uploads/{event_id}/{filename}— raw uploadss3://qos-guest-uploads/{event_id}/thumbs/{filename}.webp— 300×300 thumbnails (backfilled post-event)- CORS policy: Updated to allow
queenofsandiego.comandstaging.queenofsandiego.comorigins for presign requests
- CloudFront distribution (staging alias:
staging.queenofsandiego.com)- Invalidation after page deploy:
/*pattern to bust cache
- Invalidation after page deploy:
Spam Prevention: Event Code Gating
The page includes a passive but critical security mechanism. When guests upload without an event code, their submissions enter a moderation queue instead of auto-publishing:
// From Keely's page (line 350–354)
const isCodeVerified = code.length > 0;
if (!isCodeVerified) {
// Submit to moderation queue instead of direct publish
metadata.status = 'pending_review';
}
The event code is provided to charter attendees via email (e.g., Keely's pre-sail email body in proposals/keely-email-body.html). This pattern accomplishes two goals:
- Reduces spam: Random internet users can't auto-publish to the gallery
- Increases social proof: Only guests who attended the event (and have the code) bypass moderation
Copy on the page makes this transparent: "The code keeps strangers from posting to your charter page."
Instagram Hashtag Integration
The page renders an IG feed grid (lines 452–459) pulled from the Lambda endpoint. The endpoint accepts two hashtags — #jada and #queenofsandiego — and searches Instagram's public API for posts from the event day:
GET /api/g/2026-05-24/photos
→ Returns: { photos: [...], instagram: [...] }
The IG feed is server-side fetched by Lambda (details in shipcaptaincrew/lambda_function.py, likely around the Instagram integration logic) and baked into the response. This keeps the client-side logic simple: just render whatever d.instagram contains. Why server-side? Lambda can cache IG results, avoid exposing client-side IG tokens, and batch-fetch across events without hammering the API on every page load.
Photo Thumbnail Backfill
Post-event, the session ran a backfill script (/tmp/backfill-thumbs.py) to generate 300×300 WebP thumbnails for gallery rendering. This happened after all uploads had concluded, keeping the presign/upload hot path lightweight. Thumbnails are stored alongside originals in S3 under a /thumbs/ prefix, and the Lambda endpoint's photo listing handler references them when available.
Booking Widget Refactor: Modal Instead of Redirect
The "Book a Sail" button had a critical flaw: it redirected to the homepage, losing context and creating a disjointed UX. The fix involved:
- Extracted booking logic from homepage to standalone
booking-widget.js - Embedded modal markup + CSS into Keely's page (and presumably the main homepage)
- Wired click handler to open modal inline instead of navigating away
- Kept Stripe checkout integration (JS dependencies) to maintain payment flow
The widget is now reusable across multiple pages — any page that includes booking-widget.js and the modal HTML can offer seamless bookings without redirects.
Infrastructure & Deployment
Lambda function lifecycle:
- Function name:
shipcaptaincrew - Runtime: Python (version extracted from Lambda config during session)
- Source:
/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py