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.com
  • https://www.queenofsandiego.com
  • https://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 makes GET /api/g/{event_id}/validate-code call
  • 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.instagram array
  • 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=true query 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