Staging a Dynamic Event Section: Additive Deployment Pattern for zunigashoals.com

This post documents a zero-downtime staging deployment for a new event marketing section on zunigashoals.com. The approach prioritized safety, auditability, and preserving production stability while validating UX and email capture integration before a full prod rollout.

What Was Done

We deployed a new "Zuniga Days" event section to a staging prefix without touching the production index.html. The section includes:

  • Sticky ribbon banner with event branding
  • Multi-tile activity display (dinghy races, contests, raftups)
  • Email capture form posting to an existing Google Apps Script endpoint
  • ~212 lines of CSS and HTML

The entire production asset remained untouched, and a snapshot was captured before any staging deployment.

Technical Details: The Additive Deployment Pattern

File Structure and S3 Staging Prefix

Rather than modifying s3://zunigashoals.com/index.html directly, we deployed to s3://zunigashoals.com/_staging/index.html. This pattern allows:

  • Parallel testing of the new version without cache invalidation
  • Easy rollback (just delete the staging object or revert CloudFront routing)
  • Isolation from production traffic
  • A clear audit trail via S3 object versioning and access logs

The staging file is served at https://zunigashoals.com/_staging/index.html via the same CloudFront distribution.

Content Assembly Pipeline

The deployment process followed this sequence:


1. Download prod snapshot → /tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot
2. Parse and validate HTML structure
3. Inject Pirate Days CSS (new .zd* and .ribbon classes)
4. Insert ribbon element at top of <body>
5. Insert new <section id="zd"> between hero and shop sections
6. Attach email-capture handler to GAS endpoint
7. Write assembled file → /tmp/zuniga-staging.html
8. Upload to s3://zunigashoals.com/_staging/index.html

CSS Architecture

All new styles use a .zd class prefix (for "Zuniga Days") to prevent collisions with existing component styles. The ribbon uses position: sticky; top: 0; with appropriate z-indexing to layer above the hero without breaking responsive behavior. Key CSS decisions:

  • BEM-adjacent naming: .zd__pillar, .zd__tile, .zd__cta for clear scoping
  • CSS Grid for activity tiles: 6-column grid on desktop, responsive down to single column on mobile
  • Flexbox for pillar row: Equal-width distribution with gutters
  • Custom property overrides: Font sizes and spacing inherit from existing site theme but can be overridden per breakpoint

Email Capture Integration

The form posts to the existing Google Apps Script endpoint (referenced in agent notes as the JADA handler). The payload structure:


POST /google-apps-script-url
Content-Type: application/x-www-form-urlencoded

action=zuniga_days_signup
&email=[user-supplied]
&timestamp=[ISO-8601]
&source=staging

This reuses the existing GAS infrastructure rather than creating a new backend endpoint, reducing operational overhead and keeping email routing centralized.

Infrastructure: S3, CloudFront, and Snapshots

S3 Bucket Configuration

Both production and staging assets live in s3://zunigashoals.com (a single bucket with prefix isolation). Relevant bucket policies:

  • Public read access via CloudFront origin access identity (OAI) — direct S3 URLs are blocked
  • Versioning enabled for automatic rollback capability
  • Access logging directed to s3://zunigashoals-logs/s3-access/ for audit trails

CloudFront Distribution

The staging URL is served by the same CloudFront distribution as production (no separate distribution needed). The distribution:

  • Origin: zunigashoals.com.s3.us-west-2.amazonaws.com
  • Default root object: index.html
  • Behavior 1: Path pattern /_staging/* → TTL 60 seconds (short, for rapid iteration)
  • Behavior 2: Path pattern /* → TTL 86400 seconds (prod, longer cache)

No CloudFront invalidation was required because we're not modifying any existing paths — only adding a new prefix.

Production Snapshot

Before any staging deployment, we captured the production file:


aws s3 cp s3://zunigashoals.com/index.html \
  /tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot

This snapshot is stored locally and can be diffed against the staging version to verify exactly what changed. The filename includes a timestamp (`2026-05-26T17-31-10Z`) for easy tracking across multiple deployments.

Key Decisions and Trade-offs

Why Additive Rather Than In-Place?

Modifying production directly would have required:

  • CloudFront cache invalidation (costs money, takes 1–2 minutes to propagate globally)
  • Higher risk of a botched deploy affecting live traffic immediately
  • Harder to A/B test or roll back quickly

By using a staging prefix, we can validate everything before touching production. The tradeoff is that staging and prod serve slightly different HTML trees until promotion — but this is acceptable for a short validation window.

Why Not a Separate Staging Domain?

We could have created staging.zunigashoals.com as a separate Route53 record pointing to a different CloudFront distribution. However:

  • Shared distribution simplifies DNS and certificate management
  • Staging prefix mirrors prod exactly (same CDN, same regional edge caches)
  • Email forms and analytics can route based on referrer/source parameter (already done here)

If staging testing expands beyond HTML/CSS to backend services, a separate subdomain may be warranted.

Why GAS for Email Capture Instead of a Lambda?

The existing GAS endpoint already receives form submissions from other site sections. Reusing it:

  • Avoids spinning up new serverless infrastructure
  • Keeps email routing in one place (easier