```html

Building the Sailor Board: Event-Scoped Photo Galleries with Lambda-Backed Moderation and Same-Day Instagram Integration

What Was Done

This session completed the implementation and deployment of the Sailor Board — a per-event photo gallery system that allows charter guests to upload photos and videos in real-time, with automatic moderation gating, same-day Instagram hashtag aggregation, and a secure upload token system to prevent spam.

The system was already partially live (deployed to production on 2026-05-25 02:11 UTC for the Keely afternoon charter event), but this session:

  • Verified all four core features were present and functional on the live guest page
  • Deployed the new /sailor-board/ staging page as a consolidated dashboard
  • Updated the homepage and booking flow to eliminate dead-end redirects and implement modal-based scheduling
  • Fixed S3 CORS configuration to allow uploads from production origins
  • Extracted and deployed the booking widget as a reusable JavaScript module
  • Backfilled thumbnail images for existing photo galleries

Technical Architecture: Guest Page Upload Flow

The core Sailor Board experience lives at /g/{event_date}-{guest_name}/index.html (example: /g/2026-05-24-keely-afternoon/index.html). This page is dynamically generated and deployed to S3 at request time, with logic split between client-side and Lambda-backed API handlers.

Client-Side Upload Handler

The guest page at queenofsandiego.com/g/2026-05-24-keely-afternoon/ includes embedded HTML and JavaScript that manages file uploads. The upload flow (line 514 in the guest page source):

handleFiles() {
  const files = Array.from(files).slice(0, 24);
  files.forEach(uploadFile);
}

Key constraints:

  • File types accepted: JPEG, PNG, WebP, HEIC, HEIF for images; MP4, MOV, AVI, WebM for video
  • Batch limit: Exactly 24 files maximum per submission (enforced client-side with .slice(0, 24))
  • UI affordance: "up to 24 at once" text hint at line 360
  • Code input: Optional event code field (#g-code, line 354) that gates moderation flow

Spam Prevention: Event Code Token System

The readCode() function (line 424) implements a simple but effective spam gate:

  • If the guest enters the event code (shared separately, not in marketing copy), photos are marked for immediate publication
  • If no code is provided, photos enter a JADA moderation review queue before appearing on the public board
  • Copy at line 350: "The code keeps strangers from posting to your charter page."

This pattern prevents random internet users from polluting the gallery while allowing genuine guests instant gratification. The event code is distributed out-of-band (email, SMS, or in-person) and never appears in the HTML source.

Lambda Backend: Photo Metadata & Instagram Aggregation

The guest page renders photo and Instagram data from a single Lambda endpoint:

GET https://shipcaptaincrew.queenofsandiego.com/api/g/{event_id}/photos

This endpoint (handler in /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py) returns JSON with two keys:

  • photos: Array of approved photos from the DynamoDB table (filtered by event date and moderation status)
  • instagram: Array of Instagram posts from the same day matching hashtags #jada or #queenofsandiego

The Instagram fetch is performed server-side by querying the Instagram Graph API for posts tagged with the charter's date. The response is then rendered into the #ig-grid container (lines 452–459) without additional client-side filtering, allowing real-time Instagram integration.

Infrastructure & Deployment Pipeline

S3 & CloudFront Configuration

Guest pages are stored in two environments:

  • Staging bucket: s3://queenofsandiego-staging/g/{event_id}/index.html
  • Production bucket: s3://queenofsandiego-prod/g/{event_id}/index.html

Both buckets are fronted by CloudFront distributions to cache HTML, serve uploads, and apply custom headers. During this session, S3 CORS was updated on the production bucket to allow PUT and POST requests from:

https://queenofsandiego.com
https://www.queenofsandiego.com

This was necessary because the browser-based upload handler in the guest page makes direct PUT requests to S3 using signed URLs, and CORS preflight OPTIONS requests must be allowed before the actual upload can proceed.

Lambda Deployment: Zip Build & API Gateway Routing

The shipcaptaincrew Lambda function was updated and redeployed during this session with:

zip -r lambda-deploy.zip lambda_function.py py_vapid/ && aws lambda update-function-code --function-name shipcaptaincrew --zip-file fileb://lambda-deploy.zip

The function is exposed via API Gateway with routes:

  • GET /api/g/{event_id}/photos → fetch photos + Instagram posts
  • POST /api/presign → generate signed S3 upload URLs
  • POST /api/g/{event_id}/upload → handle moderation gate logic

All routes are bound to the custom domain shipcaptaincrew.queenofsandiego.com via Route53 CNAME record pointing to the API Gateway endpoint.

Key Decisions & Rationale

Why Per-Event Guest Pages Instead of a Global Sailor Board Aggregator?

The term "Sailor Board" in marketing copy (visible at line 1 of the Keely guest page: "Upload your photos and we'll add the best ones to our sailor board!") was initially ambiguous. Investigation revealed that "sailor board" refers to the per-event photo gallery, not a cross-charter leaderboard or aggregate feed.

This design choice makes sense because:

  • Event isolation: Each charter is its own social moment; mixing photos from multiple events dilutes the narrative
  • Moderation simplicity: One event's photos are reviewed in one context, with one set of rules
  • Real estate efficiency: The Instagram feed provides cross-event context via