Building the Sailor Board: Guest Page Photo Gallery + Instagram Integration for Queen of San Diego

Overview: What Was Built

During this session, we completed the implementation and deployment of the Sailor Board feature for Queen of San Diego's guest experience. The Sailor Board is a post-charter photo gallery that allows guests to upload photos/videos immediately after their sail, with optional same-day Instagram hashtag integration and a spam-prevention gate. The feature went live on Keely's afternoon charter page (https://queenofsandiego.com/g/2026-05-24-keely-afternoon) and is now production-ready for all future charters.

Technical Architecture

The Sailor Board is a three-tier system:

  • Frontend (Guest Page): Static HTML deployed to S3, served via CloudFront. Located at /sites/queenofsandiego.com/sailor-board/index.html (template) and S3 at s3://queenofsandiego.com/g/{event_id}/
  • Backend (Lambda): AWS Lambda function at /tools/shipcaptaincrew/lambda_function.py handling photo uploads, metadata storage, and Instagram feed aggregation
  • Photo Storage & Metadata: S3 bucket with structured photo metadata, CloudFront distribution for CDN delivery

Core Features Implemented

1. Multi-Format Photo/Video Upload (24 Max)

The guest page accepts up to 24 files per submission across formats: JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, and WebM. The upload handler is in lambda_function.py at the handleFiles() function (line 514):

Array.from(files).slice(0, 24).forEach(uploadFile)

This client-side cap prevents overload and guides UX ("up to 24 at once" copy on line 360). The Lambda backend enforces the same cap server-side, ensuring no way around the limit.

2. Event Code Spam Gate

To prevent strangers from posting to a charter's page, Keely (or any event host) receives a unique event code. Guests entering the code get instant publication; those without bypass to a JADA review queue. The code input is wired at line 354 (#g-code), with validation at readCode() (line 424). Copy on the page reads: "The code keeps strangers from posting to your charter page."

Why this design: Low friction for known attendees, manual gating for randos. No authentication required — just a shared secret.

3. Same-Day Instagram Hashtag Aggregation

The Lambda endpoint GET /api/g/{event_id}/photos returns a JSON response with both uploaded photos and Instagram posts matching hashtags #jada or #queenofsandiego from the same calendar day. The frontend renders this into the #ig-grid (lines 452–459):

d.instagram.forEach(post => {
  // render Instagram embed
  photosContainer.appendChild(igPostElement);
});

The IG fetcher is implemented in the Lambda's Instagram integration module (imported from the shipcaptaincrew tooling) and queries the Instagram Graph API using an app token scoped to the Queen of San Diego business account.

4. Time-Gated UI Transitions

The page behavior changes at a FLIP_UTC timestamp. Before the charter sail time, guests see a "Coming Soon" message. After the event, the upload UI activates. For Keely's afternoon charter (2026-05-24, flip at May 25 00:00 UTC), the upload widget became live post-event.

Infrastructure & Deployment

S3 Bucket Structure

Guest pages are stored in the primary S3 bucket queenofsandiego.com under the /g/{date}-{name}/ prefix. Each charter's page is a standalone HTML file with embedded JSON data:

s3://queenofsandiego.com/g/2026-05-24-keely-afternoon/index.html

This allows each charter to have its own unique URL and metadata without needing a routing layer or parameter-driven template.

Lambda Deployment Pipeline

The shipcaptaincrew Lambda function was updated to support the new /api/g/{event_id}/photos endpoint. The deployment process:

  1. Edit /tools/shipcaptaincrew/lambda_function.py (multiple iterations in this session)
  2. Snapshot current production code for safety
  3. Validate syntax with Python AST parser
  4. Bundle dependencies (e.g., py_vapid for push notifications) into deploy zip
  5. Upload zip to Lambda using AWS CLI
  6. Smoke test endpoints to confirm deployment success

Why bundling at deploy time: Lambda's default environment lacks some packages; bundling ensures consistent behavior across invocations and avoids Lambda Layers complexity for this particular flow.

CloudFront & Staging

Two CloudFront distributions serve the Queen of San Diego domain:

  • Production: Points to queenofsandiego.com S3 bucket origin
  • Staging: Separate distribution for testing new content before prod push

During this session, the sailor-board template and homepage were deployed to staging first, cache invalidated, smoke tested, then promoted to production:

aws cloudfront create-invalidation --distribution-id <staging-dist-id> --paths "/*"
aws cloudfront create-invalidation --distribution-id <prod-dist-id> --paths "/g/*" "/index.html"

Homepage Integration

The main homepage at /sites/queenofsandiego.com/index.html was updated to reference the Sailor Board in marketing copy. The phrase "Thanks for sailing with us on JADA. Upload your photos and we'll add the best ones to our sailor board!" now directs guests to their post-sail experience via the unique /g/{event_id} URL.

Additionally, a critical UX fix was made: the "Book a Sail" button now opens the scheduling/payment modal immediately instead of returning the user to the homepage. This was implemented via a deep-link mechanism in the booking modal's initialization script, triggered when the page loads with a specific hash parameter.

Key Design Decisions

  • Per-Event Static Pages over Dynamic Routes: Each charter gets its own S3 object. This avoids query-string routing complexity, enables full CloudFront caching, and isolates each event's content.
  • Client-Side IG Rendering: Instagram feed is fetched server-side by Lambda but rendered on the client. This keeps the page fast (no server-side rendering overhead) while centralizing IG API logic in one Lambda endpoint.
  • Event Code Instead of Auth: A simple shared secret is easier to distribute (email, text, verbal) than issuing credentials to dozens of trans