Building the Sailor Board: Deep-Linking Photo Upload, Instagram Integration, and Widget Extraction for Queen of San Diego

What Was Done

This session consolidated three major technical initiatives across the Queen of San Diego booking and event infrastructure:

  • Fixed the Book a Sail CTA to open the booking modal in-place instead of navigating back to the homepage
  • Built and deployed the Sailor Board feature—a per-event photo gallery that pulls guest uploads and same-day Instagram posts (hashtag: #jada, #queenofsandiego)
  • Extracted the booking widget as a standalone reusable component for embedded checkout flows
  • Deployed updates to staging and verified end-to-end photo upload, Instagram integration, and CORS preflight handling

All changes shipped to production with CloudFront invalidation and Lambda version updates.

Technical Details

The Modal Deep-Link Problem

The homepage CTA Book a Sail was set up as a simple anchor link that returned users to /index.html instead of opening the booking flow in-place. The solution was two-part:

  • Homepage HTML anchor structure: Changed from <a href="/"> to an anchor with data-modal-target="booking" and an onclick handler that prevents default navigation
  • Booking modal auto-open mechanism: Added JavaScript initialization logic in the landing zone that inspects the DOM for modal anchor attributes and triggers the Stripe checkout modal without page navigation

File: /Users/cb/Documents/repos/sites/queenofsandiego.com/index.html

// Pseudo-code example (actual implementation in HTML/JS)
const bookingLink = document.querySelector('[data-modal-target="booking"]');
if (bookingLink) {
  bookingLink.addEventListener('click', (e) => {
    e.preventDefault();
    openCheckoutModal();
  });
}

This pattern keeps users on the homepage, reduces friction, and provides immediate visual feedback on the booking flow—critical for conversion on mobile where navigation feels heavy.

The Sailor Board Architecture

The "Sailor Board" is not a separate cross-charter leaderboard. It is a per-event photo gallery that combines three data sources:

  • Guest uploads: Photos/videos submitted via the event's guest page (/g/{event_id})
  • Same-day Instagram posts: Public posts tagged with #jada or #queenofsandiego, fetched from Instagram's API
  • Moderated content: Uploads without the event code go to a review queue; approved content appears in the gallery

The gallery lives directly on the guest page, not on a separate endpoint. For Keely's May 24 afternoon charter, the gallery URL is:

https://queenofsandiego.com/g/2026-05-24-keely-afternoon

File structure:

  • /Users/cb/Documents/repos/sites/queenofsandiego.com/sailor-board/index.html — gallery template (created this session)
  • /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py — backend Lambda that aggregates uploads and Instagram data
  • API endpoint: GET https://shipcaptaincrew.queenofsandiego.com/api/g/{event_id}/photos

Instagram Integration in the Lambda

The Lambda handler in lambda_function.py (around line 1800+, in the /api/g/{event_id}/photos route) does the heavy lifting:

  • Queries the S3 bucket for uploads matching the event date
  • Filters for approved content (code-submitted or manually reviewed)
  • Calls Instagram's Graph API to search for posts with #jada and #queenofsandiego created on the event date
  • Merges both datasets and returns as JSON with metadata (uploader name, timestamp, media type)

Why this design? Instagram posts are ephemeral; by pulling them server-side on every gallery load, we ensure the feed always reflects today's posts without stale data. Guest uploads are persisted in S3, so they live permanently on the page.

Key Lambda parameters:

event_id = "2026-05-24-keely-afternoon"
event_date = "2026-05-24"
ig_hashtags = ["jada", "queenofsandiego"]
approval_mode = "instant_if_code | review_if_no_code"

Infrastructure & Deployment

S3 & CloudFront

The sailor-board gallery template was deployed to:

  • Bucket: queenofsandiego.com (primary site bucket)
  • Key: sailor-board/index.html
  • CloudFront distribution: Production distribution (ID: E***, masked for security)
  • Cache invalidation: /* (full distribution purge after deploy)

The staging CloudFront distribution was used for pre-flight validation before prod deploy. Invalidation commands:

aws cloudfront create-invalidation \
  --distribution-id E*** \
  --paths "/*"

Lambda Deployment

The shipcaptaincrew Lambda was updated multiple times this session to finalize Instagram integration and photo handling. Deployment workflow:

  • Local edits to /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py
  • Syntax validation via Python import check
  • Zip package creation: booking-widget.js + updated lambda_function.py (with py_vapid bundled for push notifications)
  • Deployment via AWS Lambda update-function-code
  • Smoke test: GET /api/photos endpoint to verify existing photos still load

Why py_vapid? The Lambda handles Web Push subscriptions for photo notifications; py_vapid signs the JWT required by the push service. Bundling it ensures the Lambda has no external dependencies at runtime.

CORS Configuration

Guest uploads failed initially because S3 CORS wasn't configured to allow preflight requests from queenofsandiego.com. We updated the bucket CORS policy to allow:

  • AllowedOrigins: https://queenofsandiego.com,