Staging a Dynamic Event Section on zunigashoals.com: Additive Deployment, Email Capture Integration, and S3 Prefix Routing

Overview

This post documents a controlled staging deployment to zunigashoals.com that introduced a new "Zuniga Days" event section, sticky ribbon, and email-capture integration—all without touching production. The approach used S3 prefix-based routing, snapshot-and-restore backup patterns, and careful separation of staging and production artifacts to enable rapid iteration with zero production risk.

What Was Done

We staged a new event marketing section on the Zuniga Shoals website by:

  • Copying the production index.html from s3://zunigashoals.com/index.html to a local working copy
  • Injecting ~212 lines of CSS, HTML, and JavaScript into the document without modifying production
  • Deploying the augmented version to s3://zunigashoals.com/_staging/index.html under a staging path prefix
  • Creating a snapshot of production before deploy: /tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot
  • Wiring email capture to an existing Google Apps Script endpoint for event signup tracking
  • Verifying the staging URL served the new content without CloudFront invalidation

Technical Architecture

S3 Bucket Structure & Prefix Routing

The deployment leveraged S3's flat namespace with strategic prefixing:

s3://zunigashoals.com/
  ├── index.html                    (production)
  ├── _staging/index.html           (staging)
  ├── _snapshots/                   (backup directory)
  │   └── index.html__2026-05-26... (timestamped prod snapshot)
  └── [other assets, CSS, JS]

Why this structure: The underscore-prefixed directories are semantically distinct from user-facing content. S3 serves by exact key match, so /_staging/ requests never collide with production. This allows multiple staging environments (e.g., /_qa/, /_preview/) to coexist without bucket duplication or complex routing logic.

Deployment Process & Artifact Lineage

The workflow maintained clear artifact separation:

  1. Snapshot production: Downloaded s3://zunigashoals.com/index.html → /tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot
  2. Augment locally: Edited the snapshot 34 times (per session log), adding:
    • CSS classes: .zd* namespace (Zuniga Days theme)
    • Sticky ribbon element with inline styles
    • New <section id="zd"> containing event details, activity tiles, and email capture
    • Event signup JavaScript posting to existing GAS endpoint
  3. Stage the result: Uploaded augmented file → s3://zunigashoals.com/_staging/index.html
  4. Verify: Confirmed https://zunigashoals.com/_staging/index.html served the new content
  5. Backup: Retained snapshot on S3 for rollback if needed

Infrastructure Details

S3 & CloudFront Configuration

The bucket zunigashoals.com sits behind a CloudFront distribution (exact dist ID withheld per security policy). Key configuration notes:

  • Origin: S3 bucket with static website hosting enabled
  • Cache behaviors: Default TTL allows staging content to be served immediately; no explicit invalidation was required for the /_staging/ prefix because CloudFront respects S3's key-based retrieval
  • Root object: Configured to serve index.html at the domain root, but explicit paths bypass this logic
  • No invalidation needed: Since staging lives at a separate path, existing cache entries for /index.html remain untouched

Why no invalidation: CloudFront cache keys include the full request path. A request to /_staging/index.html has a different cache key than /index.html, so invalidation of one doesn't affect the other.

Email Capture & Google Apps Script Integration

The new event signup form posts to an existing GAS endpoint. Relevant code pattern:

// In the injected HTML section
document.getElementById('zd-email-form').addEventListener('submit', function(e) {
  e.preventDefault();
  const email = this.querySelector('input[type="email"]').value;
  
  fetch('https://script.google.com/macros/d/[GAS_DEPLOYMENT_ID]/usercache', {
    method: 'POST',
    body: JSON.stringify({
      action: 'zuniga_days_signup',
      email: email,
      timestamp: new Date().toISOString()
    })
  })
  .then(r => r.json())
  .catch(err => console.error('Signup failed:', err));
});

The GAS endpoint logs signups to a sheet, enabling Chloe (the events coordinator referenced in notes) to track interest and coordinate date announcements. This reuses existing infrastructure rather than creating new endpoints.

Key Design Decisions

1. Additive Deployment Over In-Place Editing

Rather than modifying index.html directly, we created a parallel staging file. Benefits:

  • Production remains untouched until stakeholder sign-off
  • Easy rollback: delete /_staging/index.html if needed
  • Multiple reviewers can critique staging without affecting live users
  • Snapshot provides forensic history if something goes wrong

2. Semantic CSS Namespacing with .zd*

All new styles use a .zd prefix (Zuniga Days) to avoid colliding with existing styles. This reduces risk of unintended cascades and makes it trivial to remove the entire section later by deleting all .zd*` rules.

3. Reusing GAS Over New Infrastructure

Rather than provisioning a new backend service or Lambda, email capture posts to the existing Google Apps Script deployment. This:

  • Reduces operational surface area
  • Avoids standing up new monitoring/logging
  • Leverages a system Chloe already uses for event coordination

What's Next

The staging deployment is live at https://zunigashoals.com/_staging/index.html, but several tasks remain: