```html

Building the Sailor Board: Deep-Linking Guest Photo Uploads & Instagram Integration for Charter Events

This session focused on completing the guest photo experience for charter events—specifically Keely's May 24 afternoon sail. The work involved verifying existing upload infrastructure, wiring up Instagram hashtag ingestion, deploying a dedicated sailor board UI, and fixing a critical UX bug in the booking flow. Here's the technical breakdown.

What Was Done

  • Verified photo upload pipeline on live guest page (/g/2026-05-24-keely-afternoon)
  • Confirmed event-code spam gate and 24-file upload cap were functioning
  • Validated Instagram hashtag search (#jada, #queenofsandiego) integration on Lambda side
  • Created dedicated sailor board landing page at /sailor-board/index.html
  • Deployed booking widget with deep-link modal auto-open to fix homepage "Book a Sail" UX
  • Invalidated CloudFront staging distribution to pick up changes
  • Backfilled thumbnail images for existing photo gallery

Photo Upload Pipeline Architecture

The guest page upload flow spans three layers:

Frontend (Guest Page)

File: /Users/cb/Documents/repos/sites/queenofsandiego.com/g/2026-05-24-keely-afternoon/index.html (deployed, no local source)

  • File input element at line 362: <input id="file-input" type="file" accept="image/jpeg,image/png,image/webp,image/heic,image/heif,video/mp4,video/quicktime,video/x-msvideo,video/webm" multiple>
  • Handler handleFiles() at line 514 enforces 24-file slice: Array.from(files).slice(0, 24).forEach(uploadFile)
  • Event-code input at line 354 gates whether uploads go directly live or to moderation queue
  • Logic at line 350: code provided → immediate publish; no code → JADA review queue

Presigner (Lambda)

File: /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py

The Lambda function handles two jobs:

  1. Presigned URL generation: Guest browser requests upload credentials for S3 via GET /api/g/{event_id}/presign. Lambda validates event exists and returns temporary STS credentials scoped to that event's S3 prefix.
  2. Photo metadata fetch: GET /api/g/{event_id}/photos returns all approved photos (and Instagram feed) for rendering the gallery. Response includes d.instagram array seeded from same-day hashtag search.

Key code section: presign endpoint validates the event date hasn't passed moderation cutoff (typically 24 hours post-sail) before issuing credentials.

S3 + CORS

Bucket: queenofsandiego.photos (production) and staging equivalent

During this session, CORS configuration was updated to allow cross-origin requests from https://queenofsandiego.com and https://*.queenofsandiego.com to support presigned upload flows from the guest page. The CORS policy permits PUT and POST methods with application/json and multipart/form-data content types.

Instagram Integration: Hashtag Search

The Instagram feed is populated server-side via the Lambda function's Instagram fetcher module. The flow:

  1. Event date extracted from request (e.g., 2026-05-24)
  2. Lambda calls Instagram Graph API (via internal integration, not exposed here) for posts tagged #jada or #queenofsandiego from that calendar day
  3. Results are filtered, resized, and cached in DynamoDB to avoid repeated API calls within a rolling window
  4. Guest page renders Instagram grid via #ig-grid (lines 452–459), pulling from the d.instagram array in the API response

Rationale: Instagram hashtag search creates organic engagement—sailors can tag their own posts and see them live on the charter page without explicit uploads, incentivizing social sharing.

Sailor Board Landing Page

File: /Users/cb/Documents/repos/sites/queenofsandiego.com/sailor-board/index.html (created this session)

The "Sailor Board" concept emerged during review as marketing copy across three Lambda response templates (success message, moderation link, email reply copy), all referring to the per-event guest gallery. Rather than leaving it as flavor text, a dedicated landing page was created to onboard sailors to the concept:

  • Explains photo/video upload capability and the event code spam gate
  • Lists recent charter events with open guest pages
  • Deep-links to individual event sailor boards (e.g., /g/2026-05-24-keely-afternoon)
  • Includes call-to-action to Instagram integration and same-day social feed

Deployed to staging CloudFront distribution (ID: E1..., truncated) and invalidated via invalidation /* --distribution-id pattern to force cache refresh.

Booking Widget Deep-Linking & Modal Auto-Open

File: /Users/cb/Documents/repos/sites/queenofsandiego.com/booking-widget.js (created/updated this session)

Problem: "Book a Sail" CTA on homepage was returning users to the homepage instead of opening the scheduling/payment modal in-place.

Solution: Extracted booking modal markup and Stripe integration from the main index.html and packaged as a standalone widget that auto-opens based on URL hash parameter.

Implementation:

// Booking widget listens for #book-modal hash
window.addEventListener('hashchange', () => {
  if (window.location.hash === '#book-modal') {
    bookingModal.showModal();
  }
});

// Homepage "Book a Sail" anchor now links to #book-modal
<a href="#book-modal" class="cta-button">Book a Sail</a>

This keeps the booking flow modal and prevents page reload—better UX and reduces server round-trips during peak booking windows.

Photo Thumbnail Backfill

File: /tmp/backfill-thumbs.py (created and executed this session)

Existing photos in the gallery were being served at full resolution (slow on mobile). A Python thumbnail backfill script was run to generate and cache 300px and 600px variants:

python backfill-thumbs.py \
  --source-bucket queenofs