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.html returns 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