```html

Building the Sailor Board: From Marketing Copy to Feature — Photo Upload & Same-Day Instagram Integration

Last week, we deployed a guest experience page for the Queen of San Diego's charter events, complete with photo uploads, spam-gating, and Instagram hashtag integration. Today's work was about making "Sailor Board" — which started as marketing flavor text — into an actual, fully-featured product.

What Was Done

  • Audited Keely's guest page (https://queenofsandiego.com/g/2026-05-24-keely-afternoon) for feature completeness
  • Verified all four core capabilities: file uploads (JPEG/PNG/WebP/HEIC/HEIF/MP4/MOV/AVI/WebM), event code spam-gating, 24-at-once upload cap, and same-day Instagram hashtag scraping
  • Discovered "Sailor Board" was messaging-only; no aggregate cross-charter view existed
  • Built the Sailor Board as a new per-event photo gallery and moderation hub
  • Fixed "Book a Sail" button to open booking modal directly instead of navigating to homepage
  • Created thumbnail backfill pipeline to pre-generate optimized image sizes for gallery performance

Technical Details: The Guest Page Stack

File Structure & Deployment:

  • Source: /Users/cb/Documents/repos/sites/queenofsandiego.com/g/ (no local copy at start — recovered from S3 prod after deployment)
  • Prod URL: s3://queenofsandiego.com/g/2026-05-24-keely-afternoon/index.html (22.5 KB)
  • CloudFront: d1a2b3c4d5e6f7.cloudfront.net (prod distribution, immediate cache invalidation on deploy)
  • Staging test: Deployed to separate CloudFront alias before prod push for end-to-end smoke tests

Core Markup & JavaScript:

The guest page is a single-file progressive web app. Key elements:

  • #file-input (line 362): Accepts files via drag-and-drop or native file picker. Filtering logic in handleFiles() (line 514) enforces MIME type whitelist and 24-file max.
  • #g-code input (line 354): Guest code (event passcode). If entered, uploads bypass moderation queue and publish instantly. Missing code routes uploads to review queue with copy: "The code keeps strangers from posting to your charter page."
  • #photo-grid (lines 452–459): Renders photos from server response. Instagram feed (d.instagram) rendered above user uploads.
  • #ig-grid: Dedicated Instagram hashtag results section. Populated server-side by Lambda, displayed client-side as readonly gallery.

Upload Flow:

Client: File drag → handleFiles() → uploadFile(file, code) 
  ↓
Lambda presigned URL: POST /api/g/{event_id}/presign
  ↓
S3 direct upload: PUT {presigned_url}
  ↓
Lambda notification: POST /api/g/{event_id}/upload-complete
  ↓
DynamoDB: Insert photo record (status: "pending" or "live" based on code)
  ↓
Client: Poll GET /api/g/{event_id}/photos until photo appears

The Sailor Board Feature

"Sailor Board" started as three lines of copy in the Lambda function ("Your photo is live. Welcome to the sailor board"). We realized guests expected an actual photo gallery showing submissions in real time.

What We Built:

  • Per-event gallery: /g/{event_id}/index.html — live, searchable photo + video gallery for each charter
  • Real-time updates: Client polls GET /api/g/{event_id}/photos every 3 seconds; appends new photos to grid without page refresh
  • Instagram integration: Same API endpoint returns d.instagram array (filtered to event date and #jada or #queenofsandiego hashtags). Rendered above user uploads.
  • Moderation link: Admin email copies include "View sailor board" link pointing to the gallery, so moderators can see submissions in context

File Paths:

  • /sites/queenofsandiego.com/sailor-board/index.html — Gallery template (newly created)
  • /sites/queenofsandiego.com/booking-widget.js — Extracted booking modal code; embedded in sailor-board page to support quick "Book a Sail" action from gallery view
  • /tools/shipcaptaincrew/lambda_function.py — Handles /api/g/{event_id}/photos endpoint, returns photo metadata + Instagram feed

Infrastructure & Deployment

Lambda Function Updates:

  • Function name: shipcaptaincrew (us-west-2)
  • Endpoint: API Gateway routes POST /api/g/{event_id}/presign, POST /api/g/{event_id}/upload-complete, GET /api/g/{event_id}/photos to this function
  • Changes: Added Instagram hashtag scraping logic, refactored photo schema to include thumbnail URLs, added guest code validation
  • Deployment: Zipped function + dependencies (including py_vapid for push notifications), deployed via aws lambda update-function-code --function-name shipcaptaincrew --zip-file fileb://deploy.zip
  • Size constraint: Kept function code + bundled deps under 50 MB (Lambda direct upload limit)

S3 Structure:

  • Bucket: queenofsandiego.com
  • Photo uploads: s3://queenofsandiego.com/uploads/{event_id}/{photo_id}.{ext}
  • Thumbnails: s3://queenofsandiego.com/thumbs/{event_id}/{photo_id}_480w.jpg (backfill pipeline pre-generates these)
  • Guest pages: s3://queenofsandiego.com/g/{event_id}/index.html (deployed via CloudFormation or manual upload)
  • CORS config: Updated to allow queenofsandiego.com origins (presigned POST/PUT requests from browser)