Wiring Up Destination Sites to The Queen's Fleet Hub: CloudFront Aliases and Route53 DNS at Scale
What Was Done
The Queen's Fleet hub (hosted at thequeensfleet.com) displays cards for 16 destination cities. Each card pulls its hero image from a subdomain-specific origin: the Pacific destination pulls from thepacific.queenofsandiego.com/assets/0.jpg, the Atlantic from theatlantic.queenofsandiego.com/assets/0.jpg, and so on. During initial launch, three destination hostnames (thepacific, theatlantic, mazatlan) were deployed to S3 and served by the CloudFront distribution, but their DNS aliases were never created. The result: card images rendered broken on the hub, and clicking "Explore" on those cards returned 404s because the hostnames didn't resolve.
The fix involved three steps: (1) adding the missing subdomain aliases to the existing CloudFront distribution E176JRMB92GPAJ, (2) creating their Route53 A and AAAA records to point at the distribution, and (3) adding a regression test to catch this class of bug automatically on future deployments.
Technical Details: Hub Architecture and the Card Image Pipeline
The hub's card rendering logic (in build/build_hub.py) generates a static page containing 16 destination cards. Each card's markup includes an image reference like:
<img src="https://thepacific.queenofsandiego.com/assets/0.jpg" alt="...">
The browser makes an HTTPS request to that subdomain. If the subdomain doesn't resolve via DNS, the image fails silently (broken image placeholder). If it resolves but the path isn't served, the image fails with a 404 in the network tab.
Each destination site is deployed to an S3 bucket (qos-city-sites) with a prefix structure like s3://qos-city-sites/thepacific/index.html and s3://qos-city-sites/thepacific/assets/0.jpg. A single CloudFront distribution (E176JRMB92GPAJ) serves all 18 destination origins (two regional sites plus 16 destinations) using an S3 origin at qos-city-sites.s3.us-west-2.amazonaws.com and a path-based cache behavior that strips the subdomain prefix before forwarding to S3.
The router function (deployed as a CloudFront function in build/router.js) handles the subdomain-to-path translation on every request. A request to thepacific.queenofsandiego.com/assets/0.jpg arrives at CloudFront, the function rewrites the URI to /thepacific/assets/0.jpg, and CloudFront fetches that key from S3.
Infrastructure Changes
CloudFront Distribution E176JRMB92GPAJ (qos-city-router):
- Added three subdomain aliases:
thepacific.queenofsandiego.com,theatlantic.queenofsandiego.com,mazatlan.queenofsandiego.com - Total aliases increased from 15 to 18 (the distribution now serves: two regional hubs, 16 destinations)
- The wildcard ACM certificate (
*.queenofsandiego.com+ apex) already covered all three new subdomains — no certificate changes needed - Router function (version 3) correctly strips each alias to its path prefix — no function changes required
Route53 Hosted Zone (queenofsandiego.com):
- Created alias records for each missing subdomain:
thepacific.queenofsandiego.com→ A and AAAA records aliasing to distribution E176JRMB92GPAJtheatlantic.queenofsandiego.com→ A and AAAA records aliasing to distribution E176JRMB92GPAJmazatlan.queenofsandiego.com→ A and AAAA records aliasing to distribution E176JRMB92GPAJ
- Each alias record uses the CloudFront distribution's DNS name as its target, with
EvaluateTargetHealth: false(CloudFront requires this) - AAAA records added for all three to ensure IPv6 clients resolve correctly
Regression Testing (tests/test_hub.py):
Added test test_every_hub_card_host_serves_its_image() which:
- Parses the hub HTML to extract all 16 card image URLs
- Issues HTTP HEAD requests to each image URL from the Lightsail build box (as an external client would)
- Asserts HTTP 200 for every image URL
- Distinguishes real misconfigurations (test fails, alerts triggered) from transient network drops (test skipped, no false positive)
- Runs nightly in the deployment pipeline
Key Decisions
Why CloudFront aliases instead of separate distributions? Creating a new CloudFront distribution per destination would multiply management overhead, cache misses, and cost. A single distribution with many aliases keeps cache locality high (popular images stay hot), reduces operational surface area, and leverages one wildcard certificate for all subdomains. The router function acts as a "virtual host" layer, mapping each alias to its S3 prefix in one place.
Why Route53 alias records? AWS alias records eliminate DNS lookups for AWS targets — Route53 resolves alias records directly to the CloudFront distribution's address without a CNAME indirection. This improves DNS resolution latency for clients and avoids the CNAME flattening complexity at the apex.
Why automatic regression tests? When deploying a new destination site, engineers must remember to (1) upload S3 content, (2) deploy the build, (3) add CloudFront aliases, and (4) create Route53 records. Missing step 3 or 4 silently breaks the hub until a user reports it. The regression test turns this into a hard-fail in CI: the deployment pipeline won't complete if any card image is unreachable. This catches configuration drift immediately.
What's Next
The hub now serves all 16 destination cards with their hero images. The checklist for adding new destination sites has been updated in sites/queenof-cities/CLAUDE.md and infra.md to include the CloudFront alias and Route53 steps as explicit requirements. A decision document at /Users/cb/icloud-jada-ops/decisions/2026-07-05-hub-broken-cards-dns-fix.md records the root cause (missing DNS/CDN wiring) and the fix pattern.
Future enhancements: monitoring could attach a CloudWatch alarm to the regression test, paging oncall if card images go 404 unexpectedly. Cache headers could be tuned per destination (hero images are stable; index.html changes per deployment). And as the destination roster grows, the hub performance should be tracked via CloudFront request metrics and cache hit ratio to ensure the router function scales linearly.
```