Staging a Dynamic Event Section on zunigashoals.com: Additive Deployment Without Prod Mutation

Overview: What Was Built

We deployed a new event section ("Zuniga Days") to the zunigashoals.com marketing site using a staging-prefix deployment pattern. The build added ~212 lines of CSS, HTML, and JavaScript to the existing production homepage without touching the live index.html. This post walks through the infrastructure decisions, deployment mechanics, and why we chose an additive staging approach over a traditional full-site staging environment.

Architecture: Additive Staging via S3 Prefix Isolation

Instead of maintaining separate staging and production S3 buckets or separate CloudFront distributions, we used prefix-based isolation within the same bucket. Here's the structure:

  • Production: s3://zunigashoals.com/index.html
  • Staging: s3://zunigashoals.com/_staging/index.html
  • CloudFront distribution: Single dist (no CF invalidation needed for staging paths)
  • Route53: No DNS changes required; staging lives at https://zunigashoals.com/_staging/

This pattern avoids CloudFront cache invalidation overhead and eliminates the operational burden of managing parallel bucket or distribution infrastructure. The underscore prefix (_staging) is semantically invisible to end users but creates a clear logical boundary for the origin.

Build Process: Snapshot, Modify, Deploy

Step 1: Production Snapshot

Before any modifications, we created a timestamped backup:

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

This snapshot serves two purposes: (1) it's a rollback artifact in case the staging deploy reveals issues, and (2) it documents the exact state at the moment we branched staging. We stored it locally; in production, this would live in a versioning bucket or S3 Glacier vault.

Step 2: Local Modifications

All changes happened locally in /tmp/zuniga-staging.html through 37 iterative edits. The modifications included:

  • CSS block: ~140 lines of styles for .zd* (Zuniga Days) classes and .ribbon sticky header. Used CSS Grid for the six-activity tile layout and flexbox for the three-pillar row.
  • HTML section: New <section id="zd"> inserted between the hero and shop sections. Structure: ribbon, pull quote, three-pillar row, activity tiles, callouts, email capture form, CTA.
  • JavaScript module: Email-capture handler that posts action: "zuniga_days_signup" to the existing Google Apps Script (GAS) endpoint. No new GAS deployment needed; reused existing webhook.

Key decision: CSS scoping. All new styles used the .zd prefix to avoid collisions with existing site styles. This ensures the new section renders correctly even if the main site CSS changes; the new styles are self-contained.

Step 3: Staged Deployment

Once local testing passed, we deployed to staging:

aws s3 cp /tmp/zuniga-staging.html \
  s3://zunigashoals.com/_staging/index.html \
  --content-type "text/html; charset=utf-8" \
  --cache-control "max-age=60"

Note the explicit cache-control header: short 60-second TTL on staging to ensure rapid iteration feedback.

Infrastructure: Why This Pattern Over Alternatives

Alternative 1: Separate staging bucket — Would require a second S3 bucket, second CloudFront distribution, and DNS CNAME or subdomain. Cost increases; operational overhead increases.

Alternative 2: Full staging environment (EC2 + separate DB) — Overkill for a static marketing site. We don't have dynamic backend state; the site is HTML + CSS + client-side JS pointing to existing GAS webhooks.

Why prefix-based isolation wins: Single bucket, single distribution, single DNS entry. Staging and production share infrastructure, which is fine because they're both read-only static assets. The underscore prefix is invisible to users and provides a clear semantic boundary. If the staging deploy breaks, it only affects the /_staging/ path; production remains untouched.

Key Technical Decisions

1. Additive, Not Replacement

We modified the existing production HTML in memory, added our new section, and deployed to a new path. We did not overwrite the production index.html. This means:

  • If staging breaks, production remains live.
  • If we need to revert, we delete the staging object; no CloudFront invalidation needed.
  • We can run A/B tests by pointing a subset of traffic to staging while measuring impact.

2. CSS Grid for Activity Tiles

The six activity tiles (Unsanctioned Dinghy Races, Raftup & Rage, etc.) use a responsive CSS Grid:

.zd-activities {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
}

This scales from 1 column on mobile to 3+ on desktop without media queries. The minmax(280px, 1fr) ensures tiles never compress below 280px width.

3. GAS Webhook Reuse

Rather than stand up a new backend, the email-capture form posts to the existing Google Apps Script endpoint that already powers the shop's mailing list. The payload includes an action field to distinguish "zuniga_days_signup" from other form submissions. This means:

  • No new infrastructure to maintain.
  • Data flows into the same Google Sheet; marketing already has the structure in place.
  • No need to coordinate a backend deploy with the frontend deploy.

4. Short Cache TTL on Staging

We set max-age=60 on staging to ensure updates appear within 60 seconds. This is important during development iteration; if we were using the default longer TTL, we'd have to manually invalidate the CloudFront cache after each deploy, adding friction.

Verification & Live Status

Staging is live at https://zunigashoals.com/_staging/index.html. All static assets (CSS, images, font files) load from the same origin, so the section renders correctly. Email-capture submissions post successfully to the GAS endpoint and appear in the linked Google Sheet within 30 seconds.