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:
- 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. - Photo metadata fetch:
GET /api/g/{event_id}/photosreturns all approved photos (and Instagram feed) for rendering the gallery. Response includesd.instagramarray 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:
- Event date extracted from request (e.g.,
2026-05-24) - Lambda calls Instagram Graph API (via internal integration, not exposed here) for posts tagged
#jadaor#queenofsandiegofrom that calendar day - Results are filtered, resized, and cached in DynamoDB to avoid repeated API calls within a rolling window
- Guest page renders Instagram grid via
#ig-grid(lines 452–459), pulling from thed.instagramarray 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