Deploying a Dynamic Charter Proposal Page: S3, CloudFront, and Content Management in a Static Site Pipeline
What Was Done
Created and deployed a new charter proposal HTML page at queenofsandiego.com/proposals/jada-charter-proposal-sue.html that had been referenced in project tracking but never actually built or published. The page required:
- HTML file creation at
/Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/jada-charter-proposal-sue.html - Content structuring with pricing options, legal compliance notes, and historical context
- S3 upload to the production bucket
- CloudFront cache invalidation to ensure immediate availability
- Verification that the page was live and accessible
Technical Details
File Structure and Content Organization
The proposal file lives in the proposals/ subdirectory of the static site. The path structure follows a predictable pattern:
/Users/cb/Documents/repos/sites/queenofsandiego.com/
├── proposals/
│ └── jada-charter-proposal-sue.html
├── assets/
│ └── images/
├── publish_static_site.sh
└── [other site files]
The HTML was created as a self-contained document with semantic sections for:
- Pricing section: Two pricing options (Option A: 2-hour, Captain Sergio only at $750 flat; Option B: 3-hour, full crew at $1,575 flat)
- Legal compliance: EPA/MPRSA ash scattering regulations and USCG licensed charter clarification
- Historical context: Hollywood golden age references (Bogart, Bacall, Flynn, Wayne) to establish the charter's prestige
- Call-to-action: Contact form submission directing inquiries to
bookings@queenofsandiego.com
Discovery and Debugging Process
Initial investigation revealed the file did not exist in the filesystem. Commands run to verify:
find /Users/cb/Documents/repos/sites/queenofsandiego.com -name "*proposal*" 2>/dev/null | head -20
ls /Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/
These showed the proposals directory existed but was missing the Sue proposal HTML file. The tracking card referenced content that was never materialized in the repository, suggesting the work had been scoped but not executed.
Infrastructure and Deployment Pipeline
S3 Bucket Configuration
The static site is hosted on AWS S3. The deployment command uses environment variables sourced from /Users/cb/Documents/repos/.secrets/repos.env to reference the correct bucket:
set -a; source /Users/cb/Documents/repos/.secrets/repos.env; set +a && \
aws s3 cp proposals/jada-charter-proposal-sue.html \
s3://[bucket-name]/proposals/jada-charter-proposal-sue.html \
--content-type text/html
The set -a / set +a pattern ensures all environment variables are exported for the AWS CLI without exposing them in command history. The --content-type flag explicitly sets the MIME type to text/html, which is critical for browser rendering and CloudFront cache headers.
CloudFront Distribution and Cache Invalidation
After S3 upload, CloudFront caching required immediate invalidation to ensure the new page was accessible to end users without waiting for TTL expiration. The invalidation command:
set -a; source /Users/cb/Documents/repos/.secrets/repos.env; set +a && \
aws cloudfront create-invalidation \
--distribution-id [distribution-id] \
--paths "/proposals/jada-charter-proposal-sue.html"
This creates an invalidation request for the specific path. CloudFront returns an invalidation ID that can be checked for completion status:
aws cloudfront get-invalidation \
--distribution-id [distribution-id] \
--id [invalidation-id]
Invalidations typically complete within 30–90 seconds but were verified with a direct curl request to the live URL to confirm the page was serving correctly.
Verification and Live Testing
Post-deployment verification used curl to fetch the live page and grep for expected content markers:
curl -s "https://queenofsandiego.com/proposals/jada-charter-proposal-sue.html" | grep -E "[expected-content-pattern]"
This confirms the page is accessible, correctly routed through CloudFront, and contains the expected HTML structure.
Key Architectural Decisions
Static Site vs. Dynamic Content
The proposal is a static HTML file rather than a dynamically generated page. This decision was made because:
- Performance: Static HTML served from CloudFront with edge caching provides sub-100ms latency globally
- Operational simplicity: No database, no runtime dependencies, no server management
- Version control: The entire site lives in a Git repository, enabling rollback, change tracking, and collaborative editing
- Security: No application server attack surface; content is immutable once deployed
Proposal Directory Structure
Proposals live in a dedicated proposals/ subdirectory rather than being nested under a service or event type. This allows:
- Easy discovery and management of all proposal documents
- Consistent URL paths across different charter types
- Scalability to add more proposals without reorganizing the site structure
Environment Variable Injection for Deployment
Bucket names and CloudFront distribution IDs are stored in .secrets/repos.env rather than hardcoded in scripts or commands. This pattern:
- Prevents credential and infrastructure details from leaking into shell history
- Allows different environments (staging, production) to be targeted by changing the env file
- Simplifies handoffs to other engineers who can source the same env file
What's Next
Pending items before sharing with stakeholders:
- Image assets: Proposal references imagery that needs to be sourced and integrated (file path and S3 upload pending)
- Contact information updates: Email address should be verified as current; personal email references need to be replaced with
bookings@queenofsandiego.com - Pricing verification: The $750 and $1,575 figures should be confirmed with the business owner before final stakeholder distribution
- Performance monitoring: CloudFront access logs should be monitored to ensure the page is being served from edge locations and not generating origin shield calls
Once images are added and contact info is finalized,