```html

Multi-Site Static Hosting with Dynamic Vendor Integration: Deploying Across Three Dangerouscentaur Properties

This session involved a significant infrastructure undertaking: deploying vendor pages to three independent Dangerouscentaur sites (sailjada.com, queenofsandiego.com, bssd.org), registering a new domain (morenoferrolaw.com), and building out a professional subdomain with correct legal credentialing. Here's how we handled the technical complexity while keeping deployments safe and reproducible.

The Problem: Vendor Data Across Multiple Sites

The three sites share a vendor database but serve different audiences — wedding referrals, maritime services, and BSSD partner organizations. Each site had its own vendor pages, but the underlying data was scattered:

  • /Users/cb/icloud-jada-ops/vendors/vendors.json — canonical vendor data
  • /Users/cb/icloud-repos/sites/queenofsandiego.com/tests/test_vendor_pages.py — URL/phone validation tests
  • Three separate S3 buckets with outdated vendor HTML

The core issue: vendor URLs were stale, tests were failing against live sites due to timeout and DNS issues, and there was no deterministic way to rebuild and redeploy all three sites from a single source of truth.

Technical Solution: Centralized Build + Multi-Site Deploy

Step 1: Consolidate vendor data

We curated the master vendors.json file with cleaned URLs and phone numbers, then built HTML pages using a templated approach:

# Hypothetical structure (actual filenames redacted)
vendors.json
├── name
├── description
├── serviceType (wedding | maritime | nonprofit)
├── url (with timeout-safe verification)
├── phone (validated format)
└── tags (filtering for site-specific display)

Step 2: Deterministic test suite

We wrote test_vendor_pages.py with retry logic and higher timeouts (15s instead of 5s) because some partner sites (notably bssd.org) serve slowly. The tests verify:

  • HTTP 200 or 301 (redirect-safe)
  • Phone numbers match regex format
  • No DNS resolution failures
  • Content-Type headers are sensible
# Example test structure
def test_vendor_url_with_retry(url, max_retries=3, timeout=15):
    for attempt in range(max_retries):
        try:
            response = requests.head(url, timeout=timeout, allow_redirects=True)
            assert response.status_code in [200, 301]
            return True
        except requests.exceptions.Timeout:
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # exponential backoff
    raise AssertionError(f"Failed after {max_retries} retries")

Step 3: Single deploy command

Rather than manual S3 uploads to three buckets, we created a jada-deploy wrapper that:

  1. Runs build.py to generate vendor pages from vendors.json
  2. Deploys to three S3 buckets: dc-sites (primary), sailjada-web, queenofsandiego-web
  3. Invalidates CloudFront cache for affected distributions
  4. Verifies live endpoints match the local build

Infrastructure Details

S3 Bucket Structure:

  • s3://dc-sites/vendors/ — canonical vendor HTML pages
  • s3://dc-sites/danielaferro/ — Daniela's professional subdomain content
  • Cross-bucket sync via AWS CLI with --sse AES256 for compliance

CloudFront Distributions:

Three distributions handle traffic:

  • dc-sites distribution — covers *.dangerouscentaur.com (includes danielaferro. subdomain via wildcard cert)
  • sailjada-distribution — independent domain, separate origin
  • queenofsandiego-distribution — independent domain, separate origin

Routing was handled by dc-sites-router, a CloudFront function that maps incoming hostnames to S3 prefixes:

// dc-sites-router function logic (pseudocode)
if (host === "danielaferro.dangerouscentaur.com") {
  return "/danielaferro/index.html"
} else if (host.includes("dangerouscentaur.com")) {
  return "/vendors/" + path
}

Domain Registration:

morenoferrolaw.com was registered via Namecheap API from a Lightsail instance (for secure credential handling outside local dev machines). The domain was then CNAME'd to the CloudFront distribution. ACM certificate was requested for the domain and validated via DNS CNAME records in Route53.

Key Decisions & Why

Why CloudFront routing instead of virtual hosts in Apache/nginx?

Static hosting on S3 + CloudFront scales without server overhead. The dc-sites-router function is edge-computed, so latency is minimal. Adding a new subdomain is now a JSON config change, not a server redeployment.

Why retry logic in vendor tests?

Partner sites are not always fast or stable. A test that fails on transient network jitter creates false alerts. Exponential backoff matches real-world DNS resolution delays and temporary service degradation without creating flaky CI/CD.

Why morenoferrolaw.com as a separate domain?

dangerouscentaur.com is the organization brand; Daniela's professional presence needed its own domain for credibility (law firms, NGOs, and academic institutions won't link to a dangerouscentaur.com subdomain). The CNAME approach lets us maintain one CloudFront distribution and one S3 bucket while serving multiple domains.

Why centralize vendor data in vendors.json?

Single source of truth prevents divergence. Each site rebuild is deterministic. If a phone number changes, one edit updates all three sites in the next deploy cycle.

Deployment Process

# Build locally
python build.py --output dist/ --vendors vendors.json

# Run tests
pytest tests/test_vendor_pages.py -v

# Deploy to all three sites (mocked command)
jada-deploy --targets dc-sites,sailjada,queenofsandiego --invalidate

# Verify live
curl -I https://morenoferrolaw.com/
curl -I https://vendors.sailjada.com/

What's Next

  • Handoff documentation at /Users/cb/icloud-jada-ops/HANDOFF-2026-07-02.md covers credential inventory and infra ownership
  • Vendor data sync can now be automated via Lambda + EventBridge if vendors.json is updated via API
  • ACM certificate auto-renewal is managed by AWS; no manual action needed
  • CloudFront cache TTLs are set to 3600s for vendor pages (balance between freshness and edge cache efficiency)

The infrastructure now supports adding new partner sites or domains with minimal friction — just add an S3 prefix, update the router function, and redeploy.

```