Staging a Dynamic Event Section: Additive Deployment Pattern for Zuniga Shoals
What Was Done
We deployed a new event marketing section ("Zuniga Days") to the Zuniga Shoals website using an additive staging strategy that left production untouched. The implementation added ~212 lines of CSS, HTML, and JavaScript to the existing homepage, introduced a persistent sticky ribbon, wired email capture to an existing Google Apps Script endpoint, and staged everything under a /_staging/ prefix on S3 before production merge.
Technical Details: The Additive Deployment Pattern
Rather than modifying the production index.html directly, we:
- Snapshotted production: Before any changes, we captured the current prod state to
/tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot. This gave us a rollback point and a clean diff baseline. - Created a staging working copy: Pulled the current production file from
s3://zunigashoals.com/index.htmlto/tmp/zuniga-staging.htmllocally. - Made all changes in staging: Added CSS classes (`.zd*` namespace for Zuniga Days, `.ribbon` for the sticky header), inserted the new
<section id="zd">between hero and shop sections, and embedded email-capture JavaScript. - Deployed to a prefixed path: Published the modified file to
s3://zunigashoals.com/_staging/index.html—not the root production path. This allows live testing without affecting end users.
The key insight: CloudFront doesn't cache the /_staging/ prefix separately from / by default, so we bypassed cache invalidation entirely. The staging URL serves fresh from S3 without CloudFront interference, and production remains untouched until we're ready to merge.
Section Architecture: What Got Built
The new section includes:
- Sticky ribbon header: Fixed position, top of viewport. Class:
.ribbon. Text: "Zuniga Days · the last free open anchorage in San Diego." This persists across all page scrolling. - Hero callout: Pull quote: "Who's in charge here?" — NO ONE. Sets the tone for an unsanctioned, community-driven event.
- Three-pillar row: PERMIT (none required) · COST (free to anchor) · RULES (see above). Each pillar is a
<div class="zd-pillar">with centered text and consistent spacing. - Activity tiles grid: Six activities displayed in a responsive grid:
- Unsanctioned Dinghy Races
- Zuniga Shoals T-Shirt Contest
- Boat-Diving Contest
- Raftup & Rage
- Anchor & Anthem
- Flag Off Your Stern
Each tile uses the class
.zd-activity-tilewith a semi-transparent background, consistent padding, and hover effects. - Value callouts: Two supporting sections: "What you get for free" (Hotel Del / Coronado / Point Loma views) and "No boat? No problem" (finding a dinghy partner).
- Email capture form: Labeled "Tell me the day. I'll be there." The form submits to the existing Google Apps Script endpoint, posting an action object with
action: "zuniga_days_signup". - CTA back to shop: Links to the shop section with messaging around annual event tee drops.
Email Capture Integration
Rather than creating a new backend endpoint, we leveraged the existing Google Apps Script handler already in use on the site. The form submission runs this JavaScript:
document.getElementById('zd-email-form').addEventListener('submit', function(e) {
e.preventDefault();
const email = document.getElementById('zd-email-input').value;
const payload = {
action: 'zuniga_days_signup',
email: email,
timestamp: new Date().toISOString()
};
fetch('[GAS_ENDPOINT_URL]', {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify(payload)
})
.then(() => {
document.getElementById('zd-email-form').innerHTML = '<p>Thanks. You\'re in.</p>';
})
.catch(err => console.error('Signup error:', err));
});
The GAS endpoint logs the action and email to a Sheets backend. This reuses existing infrastructure rather than spinning up a Lambda or new API Gateway endpoint.
CSS Organization
All new styles use the .zd prefix (Zuniga Days) and the .ribbon class to avoid collisions with existing styles. The CSS is embedded inline in the HTML file (no external stylesheet needed for this first pass). Key classes:
.ribbon—position: fixed; top: 0; width: 100%; background: [brand color]; z-index: 999;.zd— wraps the entire section; padding and margin consistent with existing section styles.zd-pillar— flex container for the three-pillar row;flex: 1; text-align: center;.zd-activity-tile—display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));for responsive layout.zd-email-form— standard form styling with input + submit button
Infrastructure & Deployment
S3 Structure:
- Production:
s3://zunigashoals.com/index.html(unchanged) - Staging:
s3://zunigashoals.com/_staging/index.html(new, for QA) - Backups:
/tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot(local snapshot)
CloudFront Distribution: The existing CloudFront distribution serves zunigashoals.com. The staging path /_staging/* is intentionally not cached (or cached with very short TTL) to enable rapid iteration. No distribution ID changes were made; the existing config already supports path-based routing.
Deployment Command (example):
aws s3 cp /tmp/zuniga-staging.html s3://zunigashoals.com/_staging/index.html \
--content-type text/html \
--cache-control "max-age=60" \
--region us-west-2
Verification: After deploy, we confirmed the