← Queen of San Diego — Tech Blog
2026-06-10

2026-06-10

Now I have what I need. Writing the post. --- TITLE: Provisioning a Crew Subdomain, Fixing Broken Images, and Shipping 34 Files to Prod in One Afternoon ```html

Why a Crew Member Needed Their Own Subdomain

JADA's lead captain Darrell needed a personal landing page tied to the Queen of San Diego brand — somewhere guests and the operations side could see his availability, open charter slots, and a claim flow without going through the main booking funnel. The decision was to give him a first-party subdomain (darrell.queenofsandiego.com) rather than a path under the main site, so his page could evolve independently without touching the main CloudFront distribution's cache behavior rules.

Standing Up the Subdomain

The stack is the same pattern used for every JADA property: S3 static hosting behind a CloudFront distribution, with a Route 53 alias record pointing the subdomain at the CF distribution domain. The sequence was:

The S3 policy issue is the part that took a few minutes. The bucket had a condition block that matched only the original CF distribution's ARN. Adding a second distribution means adding a second aws:SourceArn to the condition, or switching to a wildcard on the account prefix. We went with the explicit ARN to stay auditable. Once that was corrected and the distribution finished deploying, the page came up clean.

Fixing Three Broken Images on the Cortez Page

While reviewing staging, three images on the cortez charter page (main boat shot, Gucci bag detail, cockpit view) were returning 404s. All three had been sourced from Unsplash URLs that had rotated or expired — a recurring problem with third-party image hosting. The fix was to replace each <img src> with a path to the S3-hosted asset, then issue a targeted CloudFront invalidation for /cortez/* so the stale responses cleared immediately. The invalidation ran in under 60 seconds.

Why Unsplash URLs break silently

Unsplash's CDN URLs don't 301 or signal expiry — they just return a 404 with no cache headers, which means CloudFront will cache the 404 at its configured TTL. If you don't invalidate after replacing the source, the page stays broken even after you've fixed the HTML. Always pair the S3 swap with an invalidation.

Promoting 34 Files from Staging to Prod

The afternoon session also closed out a staging backlog. Thirty-four files moved to prod, including eleven pages under captains-log/, a 404 page, concert-nights, sd-sailing-calendar, and fourteen competitor charter comparison pages. The comparison pages were all missing the GA4 snippet — they had been built before the site-wide GA4 rollout. Rather than fixing each file by hand, the promotion script injected the snippet programmatically before writing to S3:

GA4_SNIPPET = """<script async
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX">
</script>
<script>window.dataLayer=window.dataLayer||[];
function gtag(){dataLayer.push(arguments);}
gtag('js',new Date());gtag('config','G-XXXXXXX');
</script>"""

html = html.replace("</head>", GA4_SNIPPET + "</head>", 1)

After the S3 sync, a single wildcard invalidation (/*) cleared the CF cache across both the main distribution and the competitor pages distribution. All 34 files were confirmed live before the session closed.

Crew Roster Recovery

An earlier operation had accidentally overwritten the crew roster DynamoDB record, wiping several members. The afternoon session restored ten crew members to the jada-crew-roster table by hand — Darrell, Gene, Chris, Travis, Rick, Scott, Dan, Angelia, Angela Wong, and Daniela. One crew member (Abdul, a Sergio-recommended captain) remained missing because his email address wasn't on file. The episode cemented a standing rule: DDB writes to the roster table require a read-before-write confirmation step so a partial update never silently replaces the full record.

```