```html

Deploying a Dynamic Charter Proposal Page: From Template to Production with S3 + CloudFront

What Was Done

Created and deployed a new HTML proposal page for charter services at queenofsandiego.com/proposals/jada-charter-proposal-sue.html. The page was initially missing from production despite a template existing in the repository—this post covers the discovery, creation, testing, and CloudFront invalidation process that got it live.

The Problem: Template Exists, Page Doesn't

During development, we discovered that while the proposal template was checked into the repository at:

/Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/jada-charter-proposal-sue.html

The file had never been written to the S3 bucket serving queenofsandiego.com. This is a common pattern in static site deployments: local development files don't automatically sync to production without an explicit publish step.

Technical Details: Static Site Architecture

Repository Structure

The site uses a conventional static site layout:

queenofsandiego.com/
├── proposals/
│   ├── jada-charter-proposal-sue.html  (newly published)
│   └── [other proposal files]
├── assets/
│   ├── images/
│   │   ├── interior/
│   │   └── [other images]
│   └── [css, js, etc.]
├── publish_static_site.sh
└── [other pages]

Content Delivery Pipeline

The deployment flow follows this sequence:

  • Source: Files in the local repository at /Users/cb/Documents/repos/sites/queenofsandiego.com/
  • S3 Bucket: queenofsandiego.com (standard naming convention matching the domain)
  • CloudFront Distribution: Configured to use the S3 bucket as origin, with HTTPS enforcement and cache headers
  • Cache Invalidation: Manual invalidation required after S3 upload to ensure fresh content serves immediately

Deployment Process

Step 1: Verify the File Exists Locally

$ find /Users/cb/Documents/repos/sites/queenofsandiego.com -name "*proposal*" 2>/dev/null | head -20
$ ls /Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/

This confirmed the HTML file was present in the repository but not yet published.

Step 2: S3 Upload via AWS CLI

$ set -a; source /Users/cb/Documents/repos/.secrets/repos.env; set +a
$ aws s3 cp proposals/jada-charter-proposal-sue.html s3://queenofsandiego.com/proposals/jada-charter-proposal-sue.html

Why this approach: Direct S3 upload via AWS CLI is faster than running the full publish_static_site.sh script when deploying a single file. The environment variables (stored securely in a separate .secrets directory) provide AWS credentials without hardcoding them.

Step 3: CloudFront Cache Invalidation

$ aws cloudfront create-invalidation --distribution-id [DISTRIBUTION_ID] --paths "/proposals/jada-charter-proposal-sue.html"

Why invalidation is necessary: CloudFront caches content at edge locations globally. Even though the S3 object updated, cached versions would persist until the TTL (time-to-live) expires. Explicit invalidation forces all edge locations to fetch fresh content immediately, ensuring users see the deployed version within seconds rather than minutes or hours.

Step 4: Verification

$ curl -s "https://queenofsandiego.com/proposals/jada-charter-proposal-sue.html" | grep -E "[search-term]"

This confirms the live URL serves the expected content with correct HTML elements and data.

Infrastructure: Key Components

S3 Bucket Configuration

The bucket queenofsandiego.com is configured for static website hosting:

  • Public Read ACL: Objects must be readable by CloudFront and the public internet
  • Index Document: index.html for directory-level requests
  • Error Document: Custom error page handling
  • Versioning: Typically disabled for cost; rollback happens via re-upload if needed

CloudFront Distribution

The distribution (ID referenced in invalidation commands) provides:

  • HTTPS/TLS: Enforced via ACM certificate; HTTP redirects to HTTPS
  • Origin: S3 bucket origin with Origin Access Identity (OAI) for security
  • Caching Behavior: Default TTL and max TTL configured; typically 86400s (1 day) for HTML
  • Geographic Restrictions: None (available globally)
  • Custom Headers: May include cache-control, security headers depending on policy

DNS: Route 53 (assumed)

Domain queenofsandiego.com likely uses Route 53 with an alias record pointing to the CloudFront distribution domain (e.g., d123abc.cloudfront.net). This setup decouples infrastructure changes from DNS updates—we can modify CloudFront without touching Route 53.

Key Decisions Made

Why Not Run the Full Publish Script?

The publish_static_site.sh script likely syncs the entire /proposals/ directory (or the whole site). For a single file, direct S3 upload is:

  • Faster (no script overhead)
  • Safer (no risk of accidentally removing other files)
  • More transparent (explicit command shows exactly what's uploaded)

Invalidation Scope: Path vs. Wildcard

We invalidated only /proposals/jada-charter-proposal-sue.html rather than /proposals/* or /*. This is more cost-effective: CloudFront charges per invalidation path, and precise paths avoid unnecessary edge-location refreshes of unmodified files.

Environment Variable Pattern

The command set -a; source /Users/cb/Documents/repos/.secrets/repos.env; set +a loads credentials into the shell session temporarily:

  • set -a: Export all variables declared
  • source .../repos.env: Load the file (contains AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, etc.)
  • set +a: Stop auto-exporting

This avoids committing secrets to the repository or