Deploying Zuniga Days: A Staging-First Approach to Non-Destructive Site Evolution
This post covers a deployment pattern we used to add a major new section to zunigashoals.com without touching production, and the infrastructure decisions that made it safe and reversible.
What We Built
We added a full "Zuniga Days" marketing section to the Zuniga Shoals homepage—sticky ribbon, six activity tiles, email capture, and CTAs—without modifying the production index.html. The entire payload (212 lines of CSS, HTML, and JavaScript) lives in a staging prefix that serves independently from prod.
- Sticky ribbon: "Zuniga Days · the last free open anchorage in San Diego"
- Hero section: Pull quote + three-pillar grid (PERMIT, COST, RULES)
- Activity tiles: Dinghy races, t-shirt contest, boat diving, raftup, anthem, stern flags
- Email capture: Posts
action:"zuniga_days_signup"to existing Google Apps Script endpoint - Shop CTA: Annual event tee merchandising link
Technical Details: The Staging Pipeline
File Structure & S3 Bucket Layout
Production and staging share the same CloudFront distribution (d2x...cloudfront.net) but serve from different S3 prefixes:
s3://zunigashoals.com/index.html # Production (never modified)
s3://zunigashoals.com/_staging/index.html # Staging (new version)
This prefix-based isolation means:
- No risk of overwriting production during deployment
- CloudFront cache invalidation only needed on prod paths (staging can be freely refreshed)
- Easy A/B testing by routing users to either
https://zunigashoals.com/index.htmlorhttps://zunigashoals.com/_staging/index.html - Instant rollback by deleting the staging version
Workflow: Copy → Modify → Deploy → Snapshot
The actual process was:
- Snapshot production before any changes: Downloaded
s3://zunigashoals.com/index.htmlto local backup/tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot - Copy current prod to staging draft: Placed the prod version at
/tmp/zuniga-staging.htmlas the base - Add Zuniga Days section: Injected new CSS rules (`.zd*` class namespace) and HTML section with id
zd - Add email capture: Inserted JavaScript that POSTs to the existing GAS endpoint (same one used for other forms on the site)
- Deploy to staging S3 prefix: Uploaded final
/tmp/zuniga-staging.htmltos3://zunigashoals.com/_staging/index.html - Verify URL serves correct content: Confirmed
https://zunigashoals.com/_staging/index.htmlrenders and ribbon/tiles/email form appear
CSS Namespacing Strategy
All new styles use a .zd (Zuniga Days) prefix to prevent collisions with existing classes:
.zd { /* Zuniga Days container */ }
.zd-ribbon { /* Sticky top banner */ }
.zd-pillar { /* Permit/Cost/Rules grid */ }
.zd-tiles { /* Activity card grid */ }
.zd-tile { /* Individual activity card */ }
.zd-email-input { /* Email capture field */ }
This isolation means the new section can be developed, tested, and deployed independently. If we need to modify or remove it, there's no risk of breaking existing site styles.
Infrastructure & Deployment
S3 Bucket Configuration
The zunigashoals.com bucket is configured with:
- Static website hosting: Enabled on the S3 bucket
- Index document:
index.html - Versioning: Enabled (allows point-in-time rollback if needed)
- Public access: Blocked at bucket level; access flows through CloudFront only
CloudFront Distribution
The distribution (d2x...cloudfront.net) routes all requests to the S3 origin:
- Origin:
zunigashoals.com.s3.amazonaws.com - Behavior: Default cache TTL 300s for HTML (short to avoid stale content)
- Compression: Enabled for text/HTML/JS
- Invalidation: Not needed for staging paths (staging URLs are not cached aggressively during development)
Because we deployed to the /_staging/ prefix (not the root), no cache invalidation was required on production paths.
Email Capture Integration
The new email signup form POSTs to an existing Google Apps Script endpoint that was already handling other site forms. The JavaScript snippet:
// Posts { action: "zuniga_days_signup", email: user_email }
// to existing GAS handler
fetch(gasEndpointUrl, {
method: "POST",
body: JSON.stringify({
action: "zuniga_days_signup",
email: formInput.value
})
})
This reuses infrastructure already in place, reducing operational surface area.
Key Decisions & Rationale
Why Staging Prefix Instead of Branch Domain?
We considered creating a separate domain (e.g., staging.zunigashoals.com) but chose the /_staging/ prefix because:
- Single certificate: Avoids needing a separate HTTPS cert or wildcard configuration
- Single CloudFront distribution: Reduces infrastructure complexity
- Same origin for testing: Email forms and analytics use the same domain, so no cross-origin complications
- Easier for stakeholders: One memorable URL structure; no need to manage DNS aliases
Why Not Deploy Directly to Production?
The staging approach buys us:
- Reversibility: If the section breaks something, delete the staging file and the site is unchanged
- Stakeholder review: Customers can see the section live before we flip production traffic to it
- Analytics baseline: We can measure email signup volume and user engagement in staging before deciding on a