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.htmlfroms3://zunigashoals.com/index.htmlto 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.htmlunder 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:
- Snapshot production: Downloaded
s3://zunigashoals.com/index.html→/tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot - 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
- CSS classes:
- Stage the result: Uploaded augmented file →
s3://zunigashoals.com/_staging/index.html - Verify: Confirmed
https://zunigashoals.com/_staging/index.htmlserved the new content - 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.htmlat the domain root, but explicit paths bypass this logic - No invalidation needed: Since staging lives at a separate path, existing cache entries for
/index.htmlremain 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.htmlif 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: