Deploying a Charter Service Proposal Page: From Template to Production S3/CloudFront

Overview

This post covers the end-to-end deployment of a custom HTML proposal page for a charter service, including static site generation, S3 bucket management, CloudFront cache invalidation, and content management patterns for multi-stakeholder approval workflows.

The Problem: Template Without Instance

The repository contained a proposal template structure at /Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/, but the actual proposal file jada-charter-proposal-sue.html was never instantiated. This is a common pattern in static site generators where templates exist but corresponding content files must be explicitly created.

Discovery process:

  • Searched the proposals directory: find /Users/cb/Documents/repos/sites/queenofsandiego.com -name "*proposal*"
  • Verified the directory existed but was missing the specific proposal instance
  • Located the publish script at /Users/cb/Documents/repos/sites/queenofsandiego.com/publish_static_site.sh to understand the build/deploy pipeline

Technical Architecture

Source Control Layout

/Users/cb/Documents/repos/sites/queenofsandiego.com/
├── proposals/
│   └── jada-charter-proposal-sue.html          [newly created]
├── assets/
│   └── images/
│       ├── interior/                            [charter vessel photos]
│       └── [social proof imagery]
├── publish_static_site.sh                       [build automation]
└── [other site content]

Deployment Pipeline

The site uses a three-tier static hosting pattern:

  • Local source: HTML files in the Git repository
  • S3 origin: CloudFront-backed S3 bucket (domain: queenofsandiego.com)
  • CDN edge: CloudFront distribution with cache invalidation for updates

Content Creation: Key Decisions

Proposal Structure

The proposal was built with decision-fatigue reduction as a core principle — exactly two pricing options presented:

  • Option A (recommended): 2-hour charter with Captain Sergio only — $750 flat
  • Option B: 3-hour charter with full crew — $1,575 flat

This binary choice reduces cognitive load compared to a matrix of options, increasing conversion likelihood. The "recommended" label creates a gentle default without being pushy.

Compliance & Legal Language

The page includes critical regulatory notes:

  • EPA/MPRSA ash scattering regulations: Specified because this is not a bareboat charter — the vessel operator (Captain Sergio) maintains full responsibility for EPA compliance under the Marine Protection, Research, and Sanctuaries Act
  • USCG licensing disclosure: Clarified that this is a licensed charter service, not bareboat rental, which has different insurance and liability implications

Why this matters: Charter services have different regulatory exposure than bareboat rentals. Including this language up-front prevents misunderstandings during the sales cycle.

Branding & Narrative Content

The proposal includes a "Hollywood History" section referencing the San Diego waterfront's connection to classic cinema (Bogart, Bacall, Flynn, Wayne). This serves multiple functions:

  • Differentiates the offering from commodity boat rentals
  • Builds emotional connection through storytelling
  • Justifies premium pricing through cultural positioning

Infrastructure & Deployment Commands

Publishing to S3

After creating the HTML file locally, the publish script handles credentials via environment variables:

cd /Users/cb/Documents/repos/sites/queenofsandiego.com
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 \
  --content-type "text/html" \
  --cache-control "public, max-age=3600"

Key parameters:

  • --content-type "text/html" ensures browsers render as HTML, not download
  • --cache-control "public, max-age=3600" sets a 1-hour edge cache TTL (appropriate for content that changes infrequently but needs updates within 1 hour)
  • Environment variables sourced from .secrets/repos.env contain AWS credentials (never hardcoded)

CloudFront Cache Invalidation

S3 bucket updates don't immediately flush CloudFront edge caches. Invalidation is required:

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

The distribution ID is stored in the secrets environment file and sourced at runtime. This pattern separates configuration from code.

Verification

After deployment, verify the object exists and is accessible:

aws s3api head-object \
  --bucket queenofsandiego.com \
  --key proposals/jada-charter-proposal-sue.html

Then verify it's live on the CDN:

curl -s "https://queenofsandiego.com/proposals/jada-charter-proposal-sue.html" | head -20

Content Management Patterns: Multi-Stakeholder Approval

The session generated two memory artifacts to support future iterations:

  • feedback_no_meta_copy_in_proposals.md — Tracks the constraint that proposal pages don't include meta tag descriptions (to prevent search engine indexing of internal commercial documents)
  • reference_godaddy_credentials.md — Maps domain registration details (used during CloudFront invalidation verification)

These memory files serve as a decision log, enabling other engineers to understand why certain patterns were followed without needing to reconstruct the reasoning.

Image Asset Management

The proposals directory references interior yacht photography from:

/Users/cb/Documents/repos/sites/queenofsandiego.com/assets/images/interior/

Images are committed to Git (not fetched from external sources) because:

  • Proposal pages are self-contained static documents
  • External image dependencies create availability risk (broken proposal links)
  • Git LFS or S3 direct references would be used at scale, but for proposal documents, embedded asset paths are acceptable

Key Decisions & Trade-offs

Decision Rationale