Building the Sailor Board: Guest Photo Galleries, Real-Time Instagram Integration, and Deep-Linked Booking Flows

What Was Done

This session delivered a complete guest photo upload and curation system for per-charter event pages, integrated Instagram hashtag feeds for same-day content discovery, and fixed critical UX gaps in the booking flow. The work spans three layers: Lambda-based photo processing and moderation, S3-backed gallery storage with CORS configuration for browser uploads, and client-side gallery rendering with Instagram feed hydration.

Key artifacts deployed:

  • /tools/shipcaptaincrew/lambda_function.py — photo handler, IG API integration, moderation queue
  • /sailor-board/index.html — guest gallery page template
  • /booking-widget.js — extracted modal opener for deep-link auto-open
  • S3 CORS policy update on production bucket
  • CloudFront invalidations on staging distribution

Architecture: Per-Event Guest Galleries

Each charter event gets a unique guest page at /g/{YYYY-MM-DD}-{charter-slug}. The page serves two modes:

  • Pre-sail: Event info, crew bios, booking CTA (modal-triggered, not homepage redirect)
  • Post-sail (UTC midnight after charter date): Photo upload form, moderation code entry, live gallery, same-day Instagram hashtag feed

Mode flip is controlled by FLIP_UTC constant in the HTML. For Keely's May 24 afternoon sail, the flip occurred at 2026-05-25 00:00 UTC (May 24 17:00 PT).

Photo Upload Pipeline

Client-side (sailor-board/index.html, lines 362–515):

  • #file-input accepts JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, WebM
  • handleFiles() enforces 24-file-at-once cap: Array.from(files).slice(0, 24)
  • For each file, uploadFile() calls GET /api/g/{event_id}/presign to fetch S3 presigned POST URL
  • Direct browser multipart upload to S3 bucket queenofsandiego-guest-uploads, key path g/{event_id}/{uuid}.{ext}

S3 CORS Configuration:

Updated S3 bucket CORS to allow cross-origin uploads from https://queenofsandiego.com and staging origins:

AllowedOrigins:
  - https://queenofsandiego.com
  - https://d-*.cloudfront.net (staging alias)
AllowedMethods: GET, PUT, POST
AllowedHeaders: *
ExposedHeaders: ETag, x-amz-version-id

This enables the browser to negotiate CORS preflight with S3 before streaming the upload.

Lambda photo handler (lambda_function.py, route POST /api/g/{event_id}/upload):

  • Client sends metadata: filename, file size, guest email, optional g_code (event moderation code)
  • Handler writes to DynamoDB table guest-photos with keys: event_id, photo_id (UUID), status PENDING or PUBLISHED
  • If g_code matches event code in DynamoDB table events, status = PUBLISHED immediately; else status = PENDING (moderation queue)
  • Sends confirmation email via SNS; if pending, also triggers internal moderation queue email to captain@queenofsandiego.com

The moderation code logic: "The code keeps strangers from posting to your charter page." Guests who receive the event code in pre-sail email get instant publish; others go to review queue.

Instagram Feed Integration

Lambda route GET /api/g/{event_id}/photos:

  • Returns JSON: { published: [...], instagram: [...] }
  • published array: all PUBLISHED photos from DynamoDB, sorted by upload timestamp DESC
  • instagram array: results from hashtag search on same charter date

Instagram API handler (lambda_function.py, internal function fetch_instagram_posts()):

  • Searches hashtags #jada and #queenofsandiego via Instagram Graph API
  • Filters results to posts created on the charter date (24-hour UTC window)
  • Extracts: post URL, caption, media_type (IMAGE or VIDEO), timestamp, username
  • Caches results in DynamoDB table instagram-cache with 12-hour TTL to avoid quota exhaustion
  • Instagram API credentials stored in AWS Secrets Manager (not in code)

Client-side rendering (sailor-board/index.html, lines 452–459):

Gallery grid hydrates from d.instagram array returned by the Lambda endpoint. Each post renders as a card with thumbnail, caption, link to original Instagram post.

Booking Flow Fix: Modal Auto-Open

Previous UX: "Book a Sail" button returned user to homepage. New UX: clicking "Book a Sail" immediately opens the Stripe Checkout modal without page redirect.

Implementation:

  • Extracted booking widget logic into /booking-widget.js
  • Widget exports function openBookingModal(eventDate, eventSlug)
  • Guest page calls openBookingModal('2026-05-24', 'keely-afternoon') on button click
  • Modal fetches Stripe session data via GET /api/checkout-session, initializes Stripe.js
  • User completes booking in modal overlay; on success, gallery page updates to show booked status

This removes friction and keeps guests on the event context page during checkout.

Staging & Deployment

CloudFront staging distribution:

  • Distribution ID: E... (alias: staging.queenofsandiego.com)
  • Deployed new HTML files to S3 staging bucket queenofsandiego-site-staging at paths:
    • index.html (updated homepage nav + booking widget injection)
    • sailor-board/index.html (new gallery page)
  • Invalidated CloudFront cache: