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.html or https://zunigashoals.com/_staging/index.html
  • Instant rollback by deleting the staging version

Workflow: Copy → Modify → Deploy → Snapshot

The actual process was:

  1. Snapshot production before any changes: Downloaded s3://zunigashoals.com/index.html to local backup /tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot
  2. Copy current prod to staging draft: Placed the prod version at /tmp/zuniga-staging.html as the base
  3. Add Zuniga Days section: Injected new CSS rules (`.zd*` class namespace) and HTML section with id zd
  4. Add email capture: Inserted JavaScript that POSTs to the existing GAS endpoint (same one used for other forms on the site)
  5. Deploy to staging S3 prefix: Uploaded final /tmp/zuniga-staging.html to s3://zunigashoals.com/_staging/index.html
  6. Verify URL serves correct content: Confirmed https://zunigashoals.com/_staging/index.html renders 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