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.html is a new object key; it has no cached version yet
  • Production /index.html cache 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-*`