Building the Sailor Board: Guest Page Photo Gallery + Instagram Integration for Queen of San Diego
Overview: What Was Built
During this session, we completed the implementation and deployment of the Sailor Board feature for Queen of San Diego's guest experience. The Sailor Board is a post-charter photo gallery that allows guests to upload photos/videos immediately after their sail, with optional same-day Instagram hashtag integration and a spam-prevention gate. The feature went live on Keely's afternoon charter page (https://queenofsandiego.com/g/2026-05-24-keely-afternoon) and is now production-ready for all future charters.
Technical Architecture
The Sailor Board is a three-tier system:
- Frontend (Guest Page): Static HTML deployed to S3, served via CloudFront. Located at
/sites/queenofsandiego.com/sailor-board/index.html(template) and S3 ats3://queenofsandiego.com/g/{event_id}/ - Backend (Lambda): AWS Lambda function at
/tools/shipcaptaincrew/lambda_function.pyhandling photo uploads, metadata storage, and Instagram feed aggregation - Photo Storage & Metadata: S3 bucket with structured photo metadata, CloudFront distribution for CDN delivery
Core Features Implemented
1. Multi-Format Photo/Video Upload (24 Max)
The guest page accepts up to 24 files per submission across formats: JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, and WebM. The upload handler is in lambda_function.py at the handleFiles() function (line 514):
Array.from(files).slice(0, 24).forEach(uploadFile)
This client-side cap prevents overload and guides UX ("up to 24 at once" copy on line 360). The Lambda backend enforces the same cap server-side, ensuring no way around the limit.
2. Event Code Spam Gate
To prevent strangers from posting to a charter's page, Keely (or any event host) receives a unique event code. Guests entering the code get instant publication; those without bypass to a JADA review queue. The code input is wired at line 354 (#g-code), with validation at readCode() (line 424). Copy on the page reads: "The code keeps strangers from posting to your charter page."
Why this design: Low friction for known attendees, manual gating for randos. No authentication required — just a shared secret.
3. Same-Day Instagram Hashtag Aggregation
The Lambda endpoint GET /api/g/{event_id}/photos returns a JSON response with both uploaded photos and Instagram posts matching hashtags #jada or #queenofsandiego from the same calendar day. The frontend renders this into the #ig-grid (lines 452–459):
d.instagram.forEach(post => {
// render Instagram embed
photosContainer.appendChild(igPostElement);
});
The IG fetcher is implemented in the Lambda's Instagram integration module (imported from the shipcaptaincrew tooling) and queries the Instagram Graph API using an app token scoped to the Queen of San Diego business account.
4. Time-Gated UI Transitions
The page behavior changes at a FLIP_UTC timestamp. Before the charter sail time, guests see a "Coming Soon" message. After the event, the upload UI activates. For Keely's afternoon charter (2026-05-24, flip at May 25 00:00 UTC), the upload widget became live post-event.
Infrastructure & Deployment
S3 Bucket Structure
Guest pages are stored in the primary S3 bucket queenofsandiego.com under the /g/{date}-{name}/ prefix. Each charter's page is a standalone HTML file with embedded JSON data:
s3://queenofsandiego.com/g/2026-05-24-keely-afternoon/index.html
This allows each charter to have its own unique URL and metadata without needing a routing layer or parameter-driven template.
Lambda Deployment Pipeline
The shipcaptaincrew Lambda function was updated to support the new /api/g/{event_id}/photos endpoint. The deployment process:
- Edit
/tools/shipcaptaincrew/lambda_function.py(multiple iterations in this session) - Snapshot current production code for safety
- Validate syntax with Python AST parser
- Bundle dependencies (e.g.,
py_vapidfor push notifications) into deploy zip - Upload zip to Lambda using AWS CLI
- Smoke test endpoints to confirm deployment success
Why bundling at deploy time: Lambda's default environment lacks some packages; bundling ensures consistent behavior across invocations and avoids Lambda Layers complexity for this particular flow.
CloudFront & Staging
Two CloudFront distributions serve the Queen of San Diego domain:
- Production: Points to
queenofsandiego.comS3 bucket origin - Staging: Separate distribution for testing new content before prod push
During this session, the sailor-board template and homepage were deployed to staging first, cache invalidated, smoke tested, then promoted to production:
aws cloudfront create-invalidation --distribution-id <staging-dist-id> --paths "/*"
aws cloudfront create-invalidation --distribution-id <prod-dist-id> --paths "/g/*" "/index.html"
Homepage Integration
The main homepage at /sites/queenofsandiego.com/index.html was updated to reference the Sailor Board in marketing copy. The phrase "Thanks for sailing with us on JADA. Upload your photos and we'll add the best ones to our sailor board!" now directs guests to their post-sail experience via the unique /g/{event_id} URL.
Additionally, a critical UX fix was made: the "Book a Sail" button now opens the scheduling/payment modal immediately instead of returning the user to the homepage. This was implemented via a deep-link mechanism in the booking modal's initialization script, triggered when the page loads with a specific hash parameter.
Key Design Decisions
- Per-Event Static Pages over Dynamic Routes: Each charter gets its own S3 object. This avoids query-string routing complexity, enables full CloudFront caching, and isolates each event's content.
- Client-Side IG Rendering: Instagram feed is fetched server-side by Lambda but rendered on the client. This keeps the page fast (no server-side rendering overhead) while centralizing IG API logic in one Lambda endpoint.
- Event Code Instead of Auth: A simple shared secret is easier to distribute (email, text, verbal) than issuing credentials to dozens of trans