Building the Sailor Board: Event Photo Galleries with Deep-Link Modal Flows and Guest Code Validation
During this development session, we completed the Sailor Board feature for queenofsandiego.com—a per-event guest photo gallery system with real-time uploads, Instagram hashtag integration, and strict spam prevention. The implementation spans three layers: a serverless Lambda backend handling photo ingestion and moderation, an S3-backed CDN delivery system, and a client-side Vue-like component framework that gates uploads behind event-specific codes.
What Was Done
We deployed a complete event guest photo platform for the Queen of San Diego charter experience:
- Per-event guest pages at
/g/{event-slug}/with upload and gallery UI - Event code validation to prevent spam uploads (code → instant publish; no code → moderation queue)
- 24-photo batch upload with JPEG/PNG/WebP/HEIC and MP4/MOV/AVI/WebM support
- Same-day Instagram hashtag fetching (#jada, #queenofsandiego) served alongside user uploads
- Deep-link modal auto-open for the booking flow so "Book a Sail" CTAs launch the scheduling/payment modal inline instead of navigating away
- S3 CORS hardening to allow presigned upload requests from production origins
- Thumbnail backfill for existing photos using Pillow to generate responsive image sizes
Technical Architecture
Backend: Lambda Photo Handler
The core upload logic lives in /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py. The handler:
- Accepts presigned POST uploads from the guest page (
handleFiles()on the client) - Validates the event code (line ~424 in the guest page) against a DynamoDB table keyed by event_id
- Routes valid uploads to instant publish; missing codes go to a moderation queue
- Triggers image processing: stores originals in S3, generates thumbnails via Pillow (540px, 900px widths)
- Fetches same-day Instagram posts with hashtag filters via the Instagram Graph API
- Returns a JSON response with gallery state and moderation status
Key function signatures:
def handler(event, context):
# Route: POST /api/g/{event_id}/upload
# Validates guest_code from form data
# Returns presigned POST URL + response metadata
def fetch_instagram_posts(event_id, event_date):
# Called at gallery render time
# Searches #jada and #queenofsandiego for posts from event_date
# Returns array of {caption, media_url, timestamp}
The Lambda is deployed via a zip bundle that includes the py_vapid library (for web push, future use) and is triggered by API Gateway routes under the CloudFront distribution shipcaptaincrew.queenofsandiego.com.
Storage & CDN
S3 Buckets:
queenofsandiego-photos-prod— user-uploaded photos and generated thumbnails (path:events/{event_id}/{photo_id}/*)queenofsandiego-site-prod— HTML/CSS/JS assets and guest page templates
CloudFront:
- Primary distribution (
d1234example.cloudfront.net) — caches site HTML and API responses with 300s TTL - Staging distribution — allows pre-deploy validation before pushing to prod
CORS Configuration: Updated S3 CORS policy on queenofsandiego-photos-prod to allow presigned POST from origins:
https://queenofsandiego.comhttps://www.queenofsandiego.comhttps://staging.queenofsandiego.com(for staging uploads)
Guest Page Architecture
The guest page at sites/queenofsandiego.com/sailor-board/index.html is a single-page component with:
- Upload UI (lines 354–370): file input accepting up to 24 files, event code field, "Sail with us" copy
- Validation layer (line 424
readCode()): Validates code format and makesGET /api/g/{event_id}/validate-codecall - Batch upload handler (line 514
handleFiles()): Slices files to 24-max, calls presign endpoint for each, streams to S3 - Gallery render (lines 452–459): Renders user photos from DynamoDB + Instagram posts from
d.instagramarray - Mode switching (line ~60
FLIP_UTC): Pre-sail shows upload prompt; post-sail shows full gallery
Booking Modal Deep-Link Fix
The "Book a Sail" CTA originally navigated to the homepage. We refactored booking-widget.js to:
- Export a global
openBookingModal()function - Parse
?book=truequery param on page load and auto-open the modal - Allow internal links (e.g., from guest pages) to call
window.openBookingModal()directly without navigation - Updated homepage nav markup to use
onclick="openBookingModal()"instead of href
This eliminates the redirect and keeps users on their current page while the Stripe checkout flow opens modally.
Key Decisions & Why
Event codes over universal auth: We chose per-event codes (stored in DynamoDB, keyed by event_id) rather than OAuth/JWTs because guests are typically one-time users who sail once. A simple alphanumeric code is frictionless and fits the charter use case.
Presigned S3 POST over Lambda streaming: Rather than proxying file bytes through Lambda (which has 10 GB memory limit and 15-minute timeout), we generate presigned POST URLs client-side and let browsers upload directly to S3. This scales horizontally and offloads bandwidth from Lambda.
DynamoDB for metadata, S3 for photos: We store photo references (URLs, timestamps, uploader names) in DynamoDB for fast queries; the actual binary objects live in S3. This decouples compute from storage and allows efficient pagination/filtering of galleries.
Instagram hashtag fetch at render time: Rather than polling Instagram in the background, we fetch same-day posts when the guest