```html

Building Real-Time Photo Gallery Infrastructure for Charter Event Pages: Keely's Guest Page Deep Dive

Overview: What We Built

During this session, we completed the technical implementation of a per-charter guest photo upload and gallery system, deployed live on Keely's afternoon charter event page at https://queenofsandiego.com/g/2026-05-24-keely-afternoon. The system handles photo/video uploads with client-side validation, server-side moderation queuing, Instagram hashtag integration, and real-time gallery rendering—all gated by an event-specific code to prevent spam.

Architecture Overview

The infrastructure uses a three-tier design:

  • Frontend: Static HTML/JS guest page deployed to S3 CloudFront
  • Backend: AWS Lambda handler for presigned upload URLs, photo metadata, and moderation queue
  • Storage: S3 buckets for original photos + thumbnails, DynamoDB for photo metadata and moderation state

Technical Implementation Details

File Structure and Deployment Locations

The guest page source lives in the repo at /Users/cb/Documents/repos/sites/queenofsandiego.com/sailor-board/index.html, though the production copy (deployed May 25, 2026, 02:11 UTC) is served directly from S3 at the path-based URL queenofsandiego.com/g/{event_id}. The page is 22.5 KB and contains embedded JavaScript for upload handling.

Related supporting files include:

  • booking-widget.js — embedded booking modal that opens the Stripe checkout flow (with fixes applied this session to prevent homepage redirect)
  • /tools/shipcaptaincrew/lambda_function.py — the main API handler for photo presigning, metadata retrieval, and moderation workflow
  • pulse/index.html and dated pulse posts (e.g., 2026-05-25-yamato-class-battleship/index.html) — daily naval history feature feed

Photo Upload Flow

The upload system uses presigned POST URLs to keep credentials off the client. The guest page implements this workflow:

  1. Client validation: The page accepts JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, and WebM files. The handleFiles() function (line 514 in sailor-board/index.html) enforces a hard cap of 24 files per submission: Array.from(files).slice(0, 24).forEach(uploadFile).
  2. Presigned URL fetch: JavaScript calls the Lambda endpoint GET /api/g/{event_id}/presign?count=N to retrieve presigned POST URLs for each file.
  3. S3 direct upload: Browser sends multipart form data directly to S3 using the presigned URL. This keeps the Lambda/API Gateway request/response size bounded and offloads the bandwidth to S3.
  4. Metadata storage: After successful S3 upload, the client POSTs photo metadata (filename, upload timestamp, guest identifier) back to the Lambda at POST /api/g/{event_id}/photos, which writes to DynamoDB.

Spam Prevention: Event Code Gate

The page includes a code input field (#g-code, line 354) and readCode() function (line 424). The logic:

  • If the guest enters the event code, uploads are marked as approved: true and published immediately to the gallery.
  • If no code is entered, uploads go into a moderation: pending state. The Lambda triggers an SNS notification (or manual review email) to the event organizer.
  • The page displays copy: "The code keeps strangers from posting to your charter page."

This pattern prevents spammers from discovering the URL and flooding the gallery with off-topic content, while still allowing unvetted guests to contribute (with review).

Real-Time Gallery and Instagram Integration

The page renders a grid of approved photos from the Lambda endpoint GET /api/g/{event_id}/photos, which returns a JSON payload containing:

  • Approved guest photos (with thumbnail URLs for fast load)
  • Instagram posts matching the event-specific hashtags (e.g., #jada, #queenofsandiego) from the same day

The IG feed is populated by the shipcaptaincrew Lambda's IG hashtag fetcher (integrated into the photo handler). The frontend renders both streams into #ig-grid (lines 452–459 in sailor-board/index.html) with a unified masonry layout.

This was a deliberate choice: instead of a separate IG widget, we unified the feed so guests see a mixed gallery of their own uploads plus organic social mentions—reinforcing the community narrative.

Infrastructure and Deployment

Lambda Packaging and Deployment

The Lambda function was built and deployed using a zip bundle containing:

  • Core handler: lambda_function.py (shipcaptaincrew module)
  • Dependencies: boto3 (AWS SDK), Pillow (image processing for thumbnail generation), requests (IG API client)
  • Bundled library: py_vapid (for Web Push Notifications, used in other features)

Deployment workflow:


# Build zip with dependencies
zip -r lambda-deploy.zip lambda_function.py lib/ 

# Deploy to AWS Lambda
aws lambda update-function-code \
  --function-name shipcaptaincrew-prod \
  --zip-file fileb://lambda-deploy.zip \
  --region us-west-2

The function was tested via smoke tests against the live endpoint before and after each deployment to verify presign, upload, metadata, and IG fetch still worked.

S3 CORS Configuration

To allow browser-based presigned uploads from queenofsandiego.com, we updated the S3 bucket CORS policy to allow:

  • Origins: https://queenofsandiego.com, https://*.queenofsandiego.com, staging origins
  • Methods: POST, PUT, GET, HEAD
  • Headers: * (to allow multipart form headers)
  • Max age: 3600 seconds

This was necessary because the presigned URL POST originates from the browser, and S3 enforces CORS preflight checks. Without it, uploads would fail with cryptic CORS errors.

Thumbnail Generation and Backfill

Photos uploaded to the gallery need fast-loading thumbnails for the masonry grid. We wrote a backfill script at /tmp/backfill-thumbs.py that:

  1. Scans the S3 bucket for original photos without corresponding thumbnails