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-inputaccepts JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, WebMhandleFiles()enforces 24-file-at-once cap:Array.from(files).slice(0, 24)- For each file,
uploadFile()callsGET /api/g/{event_id}/presignto fetch S3 presigned POST URL - Direct browser multipart upload to S3 bucket
queenofsandiego-guest-uploads, key pathg/{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-photoswith keys:event_id,photo_id(UUID), statusPENDINGorPUBLISHED - If
g_codematches event code in DynamoDB tableevents, status =PUBLISHEDimmediately; 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: [...] } publishedarray: allPUBLISHEDphotos from DynamoDB, sorted by upload timestamp DESCinstagramarray: results from hashtag search on same charter date
Instagram API handler (lambda_function.py, internal function fetch_instagram_posts()):
- Searches hashtags
#jadaand#queenofsandiegovia 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-cachewith 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-stagingat paths:index.html(updated homepage nav + booking widget injection)sailor-board/index.html(new gallery page)
- Invalidated CloudFront cache: