Building the Sailor Board: Post-Event Photo Gallery & Real-Time Instagram Integration for Charter Guest Pages
What Was Done
This session delivered a complete photo upload and curation system for Queen of San Diego's per-charter guest pages. The "Sailor Board" is a dynamic gallery that appears on each event's guest page (e.g., /g/2026-05-24-keely-afternoon) immediately after a charter concludes. It combines three data sources: guest-uploaded photos/videos, moderated submissions from a Lambda review queue, and real-time Instagram posts matching charter-specific hashtags.
Key features shipped:
- Client-side multi-file upload: Accept up to 24 photos/videos per submission; validate file types (JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, WebM)
- Guest code spam gate: Event-specific code enables instant publish; missing code routes to moderation queue
- Server-side moderation flow: Lambda review queue with email notifications to event organizer
- Same-day Instagram hashtag integration: Fetch and render #jada and #queenofsandiego posts from the charter date
- Booking widget modal: "Book a Sail" link now opens checkout in-modal instead of navigating away
Technical Architecture
File Paths & Structure
Guest page template:
/Users/cb/Documents/repos/sites/queenofsandiego.com/g/{event_id}/index.html
Example: Keely's post-sail guest page lives at queenofsandiego.com/g/2026-05-24-keely-afternoon and is stored in S3 at that path. The local repository does not currently track guest pages as checked-in sources; they are generated and deployed directly to S3 during event setup.
Lambda function handling photo operations:
/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py
This single Lambda handles multiple routes:
POST /api/g/{event_id}/photos— Accept file uploads, validate code, enqueue for moderation or publish directlyGET /api/g/{event_id}/photos— Retrieve approved photos + Instagram feed for the event dateGET /api/g/{event_id}/moderate— Admin review queuePOST /api/g/{event_id}/moderate— Approve/reject submissions
Booking widget (newly extracted):
/Users/cb/Documents/repos/sites/queenofsandiego.com/booking-widget.js
This ~15 KB standalone module contains the Stripe checkout modal initialization and payment flow. It's embedded in guest pages via a <script> tag and triggered by "Book a Sail" click handlers.
Upload Flow
Client side (guest page):
- File input element (
#file-input) accepts multiple files; HTML5acceptattribute constrains to image/video MIME types handleFiles()function (line 514 in Keely's page) retrieves files, slices to first 24, and callsuploadFile()for each- Each
uploadFile()request is a multipart POST to/api/g/{event_id}/photoswith fields:file,code(optional event code),uploader_name,uploader_email - Guest enters event code in
#g-codeinput if they have it; code determines publish vs. queue behavior
Server side (Lambda):
- Extract multipart body and validate MIME type against allowlist
- If code matches event's
codefield in DynamoDB: bypass moderation, write to S3 photo bucket and DynamoDB gallery table, return success - If no code or code mismatch: store in
queueDynamoDB table, send email to event organizer with moderation link - Generate thumbnail on write via Pillow (Lambda layer) for fast gallery rendering
- Store metadata (uploader name/email, timestamp, original file size) in DynamoDB for audit and display
Instagram Integration
The Lambda has an embedded Instagram API client (using requests library) that fires during the GET /api/g/{event_id}/photos call:
- Query Instagram for posts with hashtags
#jadaand#queenofsandiegofrom the event's date (e.g., May 24, 2026) - Filter results to only posts within the 24-hour window of the charter
- Download media URLs and thumbnails; store pointers in the same response payload as guest photos
- Client renders Instagram posts in a dedicated section (
#ig-grid) using the same grid styling as guest uploads
This approach avoids client-side API calls (which would require exposing Instagram credentials) and centralizes filtering logic on the backend where rate limits and authentication are managed securely.
S3 & CloudFront Deployment
Photo bucket: s3://qos-guest-uploads/ (event-scoped prefix: 2026-05-24-keely-afternoon/)
Photo metadata DynamoDB table: qos_guest_photos (partition key: event_id, sort key: photo_id)
Moderation queue table: qos_photo_queue (partition key: event_id, sort key: submission_ts)
Guest pages are published to the main S3 website bucket and fronted by CloudFront distribution E2ABCD1234EF5` (staging: E5XYZ9876ABC3`). The Lambda is exposed via API Gateway with base path shipcaptaincrew.queenofsandiego.com and routed through a separate CloudFront distribution for API caching (TTL: 0 for photo POST, 60s for GET).
S3 CORS policy update: To allow guest pages to issue XHR requests to the presigned upload endpoint, CORS was updated to allow Origin: https://queenofsandiego.com and https://www-staging.queenofsandiego.com with methods GET, POST, PUT.
Booking Modal Integration
Previously, "Book a Sail" links on the homepage and guest pages navigated to /book