Staging the Zuniga Shoals "Ocean Man" Event Section: Additive Deployment Strategy with S3 Prefix Isolation
What Was Done
We deployed a new event marketing section ("Zuniga Days" / "Ocean Man") to the Zuniga Shoals website without touching production. The approach used S3 prefix isolation — staging content lives under /_staging/ while prod remains untouched at the root. This allowed rapid iteration and stakeholder preview before a full prod push.
The deployment added ~212 lines of CSS, HTML, and JavaScript to the existing site:
- Sticky ribbon banner with event messaging
- New section with event pillars (no permit, free, no rules)
- Six activity tiles (dinghy races, contests, raftup, etc.)
- Email capture form wired to existing Google Apps Script endpoint
- Call-to-action flow back to shop merchandise
Technical Details: The Staging Strategy
File Structure and Source of Truth
All changes were made to /tmp/zuniga-staging.html, which started as a snapshot of the current production index.html. This file served as the working draft:
# Snapshot prod before modifications
cp s3://zunigashoals.com/index.html /tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot
# Work on staging copy
# Edit /tmp/zuniga-staging.html (33 iterations of refinement)
# Deploy to staging prefix
aws s3 cp /tmp/zuniga-staging.html s3://zunigashoals.com/_staging/index.html
The production file at s3://zunigashoals.com/index.html was never modified. This isolation pattern is critical — it lets us preview changes, gather feedback, and iterate without risk of breaking the live site.
CSS and Component Architecture
New styles use a prefixed naming convention to avoid collisions with existing styles:
.zd-section— main event section container.zd-pillar-row— three-column grid (PERMIT / COST / RULES).zd-activity-tile— six-item activity grid.zd-callout— emphasis boxes ("What you get for free," "No boat? No problem").ribbon— sticky top banner with event branding
CSS is scoped with these prefixes to ensure zero collisions if existing site styles refactor. The ribbon uses position: sticky; top: 0; z-index: 999; to sit above navigation without requiring changes to the nav component itself.
Email Capture Integration
The form submits to the existing Google Apps Script endpoint — no new infrastructure:
// In /tmp/zuniga-staging.html
<form id="zd-email-capture" action="[GAS_ENDPOINT_URL]" method="POST">
<input type="email" name="email" placeholder="Tell me the day. I'll be there." required />
<input type="hidden" name="action" value="zuniga_days_signup" />
<button type="submit">Notify Me</button>
</form>
<script>
document.getElementById('zd-email-capture').addEventListener('submit', function(e) {
e.preventDefault();
fetch('[GAS_ENDPOINT_URL]', {
method: 'POST',
body: new FormData(this)
}).then(r => r.json()).then(data => {
console.log('Signup recorded:', data);
// Show success state
});
});
</script>
The form appends an action: "zuniga_days_signup" parameter so the GAS script can route this request separately from other form submissions. No new lambda, no new database — leverages existing infrastructure.
Infrastructure: S3, CloudFront, and DNS
S3 Bucket Structure
The zunigashoals.com S3 bucket now has this layout:
s3://zunigashoals.com/
├── index.html # Production (untouched)
├── _staging/
│ └── index.html # Staging version (new)
├── _assets/
├── shop/
└── [other existing paths]
The /_staging/ prefix isolation is a common pattern for blue-green and canary deployments. Keeping both versions in the same bucket simplifies DNS and CloudFront configuration — no alias switches, no new distributions needed.
CloudFront Caching Considerations
The CloudFront distribution pointing to zunigashoals.com does not require invalidation for the staging path because:
/_staging/index.htmlis a new object key; it has no cached version yet- Production
/index.htmlcache headers remain unchanged - CloudFront TTL policies apply per-object; new paths start with zero cache age
If we had modified the prod /index.html` in-place, a CloudFront invalidation would be required:
aws cloudfront create-invalidation \
--distribution-id [DIST_ID] \
--paths "/index.html" "/index.html.gz"
Since we didn't do that, no invalidation was necessary.
Access and Testing
The staging version is now live at: https://zunigashoals.com/_staging/index.html
This URL is publicly accessible but not linked from anywhere — it's discoverable only by direct URL or shared link. This serves as the preview environment for stakeholder feedback.
Key Decisions and Why
Additive-Only Changes
Decision: Never modify prod; always create new versioned files in staging.
Why: Reduces risk of accidental overwrites and gives us an instant rollback mechanism. If stakeholder feedback requires major changes, we iterate on the staging copy, not a half-modified production branch. The prod snapshot at /tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot is our rollback point if anything catastrophic happens.
Prefix-Based Isolation Over Subdomain
Decision: Use /_staging/ path prefix instead of staging.zunigashoals.com subdomain.
Why: Single bucket, single distribution, no DNS changes, no Route53 work, no SSL cert scope expansion. Staging exists instantly without infrastructure overhead. If we ever need a long-lived staging domain, we can add a subdomain later with a CNAME or Route53 alias.
Scoped CSS Prefixes
Decision: All new styles use `.zd-*`