Deploying a Staging Environment for Zuniga Shoals: Additive Prefix Strategy and GAS Integration
This post covers a controlled staging deployment for the Zuniga Shoals website, focusing on the "Pirate Days" (working title: "Zuniga Days") section. The approach prioritized zero-risk production changes through an additive S3 prefix strategy, snapshot-based rollback capability, and integration with an existing Google Apps Script (GAS) endpoint for email capture.
What Was Done
We staged a new marketing section on the Zuniga Shoals homepage without modifying the production index.html. The deployment added approximately 212 lines of code—CSS, HTML markup for the section, and JavaScript for email capture—all served from a new /_staging/ S3 prefix.
- Source file:
/tmp/zuniga-staging.html(local working copy) - Target:
s3://zunigashoals.com/_staging/index.html - Production snapshot:
/tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot - Live staging URL:
https://zunigashoals.com/_staging/index.html
Technical Details: What Gets Staged
The staging build includes three functional layers:
1. CSS Layer
New styles added for the section and ribbon components:
.ribbon— sticky top banner ("Zuniga Days · the last free open anchorage in San Diego").zd*namespace — all Zuniga Days section styling (tiles, callouts, typography)
These are scoped to avoid collisions with existing page styles. The ribbon uses position: sticky; top: 0; z-index: 100; to ensure it stays visible as users scroll, but does not break existing header layout.
2. Markup Layer
A new <section id="zd"> inserted between the hero and shop sections. The structure includes:
- Working title placeholder (ready for rename)
- Pull quote: "Who's in charge here?" — NO ONE.
- Three-pillar row: PERMIT (none required) · COST (free) · RULES (see above)
- Six activity tiles (grid layout):
- Unsanctioned Dinghy Races
- Zuniga Shoals T-Shirt Contest
- Boat-Diving Contest
- Raftup & Rage
- Anchor & Anthem
- Flag Off Your Stern
- "What you get for free" callout (Hotel Del / Coronado / Point Loma views)
- "No boat? No problem" sub-section with CTA
- Email capture form (see below)
3. JavaScript Layer: Email Capture Integration
The form posts to the existing GAS endpoint without requiring new infrastructure. The capture mechanism:
// Email capture payload structure
{
action: "zuniga_days_signup",
email: "<user-input>",
timestamp: "<ISO-8601>"
}
The form submits via fetch() to the GAS Apps Script URL (not exposed here). The endpoint already handles multiple action types, so this is purely additive—no changes to the backend.
Infrastructure: S3 and CloudFront Strategy
Why Additive Prefix (Not Subdomain)?
We chose s3://zunigashoals.com/_staging/index.html over a subdomain like staging.zunigashoals.com for several reasons:
- No new DNS records: Avoids Route53 changes and CloudFront distribution updates.
- Same origin: Email capture form (and any future JavaScript) shares cookies/localStorage with production, simplifying debugging of user-scoped behavior.
- Cache isolation: The
/_staging/prefix is not cached by CloudFront (or uses separate cache behavior rules), so changes deploy instantly without invalidation. - No CF distribution modification: The existing CloudFront distribution (ID not exposed) continues serving the root path; staging is served either from a different behavior rule or directly from S3 if you access the S3 website endpoint.
S3 Bucket Configuration
The production bucket is zunigashoals.com with versioning enabled. Before deployment:
aws s3 cp s3://zunigashoals.com/index.html /tmp/zunigashoals.com__index.html__2026-05-26T17-31-10Z.prod-snapshot
This snapshot serves as a rollback point. The staging file was then uploaded:
aws s3 cp /tmp/zuniga-staging.html s3://zunigashoals.com/_staging/index.html --acl public-read
Note: --acl public-read ensures the staging file is publicly accessible (bucket policy and CloudFront origin access identity may override this; exact configuration depends on your bucket policy).
Deployment Verification
Sanity checks performed before going live:
- File size: Staging file is production file + ~212 lines. No unexpected bloat.
- URL serves:
https://zunigashoals.com/_staging/index.htmlreturns HTTP 200 with correct content-type. - Form submission: Email capture form posts to GAS endpoint without errors (checked browser DevTools Network tab).
- Production untouched:
https://zunigashoals.com/index.html(or root path) still serves the original production snapshot.
Key Decisions
1. No CloudFront Invalidation for Staging
Because the staging path is new, there's no cached version to invalidate. If the staging path has existing cache rules, a wildcard invalidation (/_staging/*) might be prudent, but is likely unnecessary.
2. Snapshot-First Approach
We captured the production state before any changes. This enables:
- Rollback: Restore the snapshot to production in seconds if needed.
- Audit trail: Version history of what was live at deployment time.
- Testing: Compare staging changes against a known baseline.
3. GAS Endpoint Reuse
Rather than standing up new backend infrastructure, we leveraged the existing GAS Apps Script endpoint. The endpoint already accepts multiple action types, so "zuniga_days_signup" is just another action. This reduces complexity