Staging the Zuniga Shoals "Ocean Man" Event Section: S3 Deployment, Email Capture Integration, and Multi-Environment Strategy
This post documents the technical implementation of the first-pass "Ocean Man" event marketing section for zunigashoals.com, including staging deployment, Google Apps Script integration, and the infrastructure decisions that kept production untouched while validating new content.
What Was Done
We built and deployed an additive marketing section to the Zuniga Shoals website without touching production. The work included:
- Created a new "Zuniga Days" (working title) section with activity tiles, three-pillar value proposition, and email capture
- Implemented a sticky ribbon header announcing the event
- Wired email signup to the existing Google Apps Script endpoint
- Deployed to S3 staging prefix (
/_staging/index.html) with full production snapshot before any changes - Verified staging deployment and content integrity
Technical Architecture
File Structure and Modifications
All work was done in a local staging file, /tmp/zuniga-staging.html, which became the source of truth for this deployment:
/tmp/zuniga-staging.html
├─ Original prod snapshot: /tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot
├─ New CSS blocks (~150 lines)
│ ├─ .ribbon (sticky positioning, z-index management)
│ ├─ .zd-section (grid layout for activity tiles)
│ ├─ .zd-tile (card styling with hover states)
│ └─ .zd-email-capture (form styling)
├─ New HTML sections (~62 lines)
│ ├─ <div class="ribbon"> (sticky header)
│ └─ <section id="zd"> (main event section)
└─ New JS function (email-capture handler)
The CSS uses a .zd* namespace to avoid collisions with existing Zuniga Shoals styles. This is critical in a site with years of accumulated CSS; prefixing reduces the blast radius if a selector has to be refactored.
Email Capture Integration
The email signup form posts to the existing Google Apps Script endpoint (GAS). The JavaScript handler:
// Captures form submission
document.querySelector('.zd-email-capture form').addEventListener('submit', async (e) => {
e.preventDefault();
const email = e.target.querySelector('input[type="email"]').value;
const payload = {
action: 'zuniga_days_signup',
email: email,
timestamp: new Date().toISOString()
};
// POST to existing GAS endpoint (no hardcoded URL in blog post)
// Response logged; form cleared on success
});
This approach reuses the existing GAS infrastructure instead of spinning up a new backend service. The action field allows the GAS script to route signup data into a separate sheet tab (or webhook) for event management.
S3 and CloudFront Infrastructure
Deployment Strategy: Staging Prefix
Rather than deploy directly to the production index, we used an S3 prefix strategy:
- Production:
s3://zunigashoals.com/index.html(unchanged) - Staging:
s3://zunigashoals.com/_staging/index.html(new deployment) - Live URL:
https://zunigashoals.com/_staging/index.html(accessible via existing CloudFront distribution)
This prefix-based staging avoids the need for a separate S3 bucket, separate CloudFront distribution, or Route53 CNAME. The existing CloudFront distribution already serves the root domain and any prefix path; S3 origin routing handles the path automatically.
Production Snapshot
Before any modifications, we captured a production snapshot:
aws s3 cp s3://zunigashoals.com/index.html \
/tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot
Why: If staging validation reveals a critical issue, rollback is a single S3 copy command. The timestamp in the filename prevents accidental overwrites if multiple snapshots are taken in the same session. This is a lightweight disaster-recovery pattern suitable for static sites.
CloudFront Considerations
The CloudFront distribution already caches both /index.html and /_staging/index.html because:
- The origin is configured to serve S3 at
zunigashoals.com.s3.amazonaws.com - All paths under that origin are cached by default
- No CF invalidation was needed for the staging path because it's a new object, so no stale cache exists
If we later update /_staging/index.html again, an explicit invalidation may be needed:
aws cloudfront create-invalidation \
--distribution-id [DIST_ID] \
--paths "/_staging/index.html"
Key Technical Decisions
1. Additive-Only Changes
The new section is inserted between the hero and the shop, but no existing HTML is removed or restructured. This reduces the risk of breaking existing functionality (navigation, footer scripts, analytics) and makes the changeset easier to review.
2. Namespace Isolation
All new CSS classes use the .zd- prefix (for "Zuniga Days"). If a future redesign needs to remove the event section, a simple search-and-delete removes all its styles without worrying about cascading side effects.
3. GAS Reuse Over New Microservice
The email capture could have been sent to a Lambda function or external API. Instead, we routed it to the existing Google Apps Script endpoint because:
- No new infrastructure to provision or monitor
- Existing endpoint already logs to Google Sheets (a free, queryable database for event signups)
- GAS auto-scales and has no cold-start latency for low-traffic signups
4. Staging Path, Not Subdomain
We could have requested staging.zunigashoals.com or stood up a separate bucket. A prefix path was chosen because:
- No DNS changes (no Route53 modifications)
- Same CloudFront distribution, same SSL certificate (already valid for
*.zunigashoals.com) - Easier to promote to production: rename
/_staging/index.htmlto `/index.html` - No separate cache invalidation strategy
Deployment Verification
After uploading to S3, we verified:
- URL accessibility: