# AWS Static Site

> Plan, provision, deploy, or audit a guarded AWS static website using S3, ACM, Route53, and CloudFront. Use when the agent is asked to make a static site live on AWS, create or review an S3/CloudFront/ACM/Route53 setup, publish a local static site, debug a static-site AWS deployment, or prepare a safe deployment plan with rollback. Includes account-verification guardrails, read-only-by-default AWS CLI handling, and target-aware-security requirements for public cloud/DNS changes.

- Skill: `dreamers-laboratory/aws-static-site` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add dreamers-laboratory/aws-static-site`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dreamers-laboratory/aws-static-site/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: dreamers-laboratory (https://skillmd.com/u/dreamers-laboratory)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/dreamers-laboratory/aws-static-site

---


# AWS Static Site

Use this skill to get a static website live on AWS through private S3 origin, CloudFront, ACM, and Route53. Treat every AWS and DNS mutation as production-risk infrastructure work.

## Non-Negotiable Guardrails

- Also use `target-aware-security` for any AWS, DNS, public URL, certificate, bucket, CDN, IAM, or deployment change.
- Follow disciplined release practice when the request includes committing, pushing, publishing, production deploys, release prep, or source-repo setup.
- Treat local AWS CLI access as **read-only by policy** until the user explicitly approves a specific mutation plan in chat. CLI-level restraint is a behavioral policy; assume nothing is IAM-enforced.
- Select the intended account explicitly with a named profile: set `AWS_PROFILE=<profile>` or pass `--profile <profile>` on every command. Never mutate under whatever default credentials happen to be active.
- Before any AWS write, confirm you are in the intended account with `aws sts get-caller-identity` (and `aws iam list-account-aliases` when permitted). If the returned account ID or alias does not match the account the user intends, stop.
- Re-verify identity after any profile or environment change and before each mutation batch.
- If AWS identity, hosted zone, or target domain is ambiguous, stop and ask for clarification.
- Do not print secrets. Do not read or display AWS credential file contents. It is acceptable to mention credential file paths and profile names.

## Default Architecture

- Use S3 as a private CloudFront origin; avoid public S3 website hosting.
- Keep S3 Block Public Access enabled.
- Use CloudFront Origin Access Control (OAC) instead of legacy public bucket policies.
- Put ACM certificates for CloudFront in `us-east-1`.
- Use DNS validation through Route53 when the domain is in the account's Route53.
- Use Route53 alias records to CloudFront for apex and `www` where needed.
- Use HTTPS-only viewer behavior, HTTP to HTTPS redirect, and a current TLS security policy.
- Enable S3 versioning unless the user explicitly wants no rollback history.
- Keep deployment artifacts local and exclude secrets, `.env`, build caches, source maps with sensitive paths, and private files.

## Workflow

1. Inventory only:
   - Confirm current AWS identity, account ID/alias, region, Route53 hosted zone, existing DNS records, existing ACM certs in `us-east-1`, existing CloudFront distributions, and S3 bucket name availability.
   - Ask for the source repository URL before setup, unless the user already provided one.
   - If the project has no source repository, ask whether the user wants a repo created after the infrastructure is set up; do not create a repo without explicit approval.
   - Confirm repository read access and whether the deployment should publish from the repo, from the current local workspace, or after committing local changes to the repo.
   - Confirm the local static build directory and run a local preview first.
   - Record all existing DNS values that might be replaced.

2. Plan:
   - Present exact resource names, domain names, AWS account proof, commands or IaC actions, exposure state, expected public URLs, and rollback steps.
   - State whether the plan creates new resources or reuses existing resources.
   - Call out destructive or irreversible operations separately.

3. Confirm:
   - Do not mutate AWS until the user explicitly approves the exact plan.
   - If the user says "deploy" or "ship it" after seeing a plan, that can count as approval only for the listed resources and commands.

4. Mutate one class at a time:
   - Certificate and DNS validation.
   - S3 bucket and policy/OAC.
   - CloudFront distribution.
   - Route53 aliases.
   - S3 content upload and cache invalidation.

5. Verify:
   - Check HTTPS status, certificate hostname, CloudFront distribution domain, Route53 answers, HTTP-to-HTTPS redirect, and object availability.
   - Verify S3 remains private and public access is blocked.
   - Re-check the active AWS account after switching profiles or before each mutation batch.

6. Report:
   - Lead with exposure state and public URL.
   - List created/changed resources by name/ID/ARN, never secrets.
   - Include rollback commands or rollback procedure.
   - If no repo was used, ask whether the user wants a repository created or connected for future deployments.
   - Note DNS propagation and remaining risk.

## Multi-Site Monorepo Hygiene

Some repos build several hosts from one app (for example one Vite project serving `landing-a.example.com` and `landing-b.example.com`, where a build-time host variable scopes the prerendered HTML plus `robots.txt`, `sitemap.xml`, and `llms.txt` to that host's single route, and each host has its own S3 bucket and CloudFront distribution). One build targets one host; after the sync, resolve that host's distribution ID and invalidate explicitly.

**Leak gotcha: every host bucket can receive every site's assets.** A host variable typically scopes only the prerendered HTML. Vite still copies all of `public/` into `dist/` and bundles every registered site's JS, so host A ends up serving host B's images and vice versa: orphan assets, bloat, and a path for an unreleased site's WIP to publish onto a live host.

**Deploy one site cleanly:**

1. **Inspect `git status` before any per-host deploy.** Another site's uncommitted WIP (assets in `public/`, code in its source directory) in the working tree gets built and published to the host you deploy. Commit or stash unrelated WIP first, or knowingly accept and then clean the leak.
2. **Namespace per-site assets** under `public/images/<site>/` so leaks are identifiable and removable; avoid shared dumps like `public/images/landing/`.
3. **Verify after deploy** the bucket carries no other site's unreleased assets: `aws s3 ls s3://<host>/images/<other-site>/`. Remove strays only with a scoped, user-approved `aws s3 rm s3://<host>/images/<site>/ --recursive`, never a blind bucket-wide recursive delete.
4. **Commit only the changed site's files.** Shared files such as a site registry module register multiple sites; stage them only when the diff is exclusively yours, never another site's entangled edits.
5. **Real fix when the leak matters:** prune other sites' asset directories out of `dist/` before the sync (or scope the asset copy per host) so each bucket carries only its own assets.

Treat already-released shared assets as harmless cache bloat; treat an unreleased site's assets on a live host as a disclosure to clean.

## Contact Form Verification (Required When The Site Has A Contact Form)

If the static site has a contact form, that form MUST be tested end-to-end on the live host before the deployment is called complete. Never assume a shared backend already accepts a new host. Shared form backends commonly gate by hostname in more than one independent layer, and all of them are required:

- A CORS origin allowlist. Missing means the browser blocks the POST at preflight (`access-control-allow-origin: null`, same as a bogus origin).
- Per-host captcha secret routing (or an equivalent per-host config). Missing means the backend rejects the submission *even after* CORS passes. CORS alone proves nothing.

Wildcard captcha site keys registered at the apex domain usually cover subdomains, so a new subdomain often needs no captcha-console change; keys that list only a specific subdomain must have the new hostname added explicitly.

Apply backend config changes additively and surgically: fetch the full current environment, change only the required variables, preserve everything else, and save a rollback value first (handle secrets in mode-600 temp files; never print them). Prefer a targeted configuration update over redeploying a shared stack from a possibly stale template; the live configuration is the source of truth. If the backend is shared by several properties, every environment write is a production change.

Verify all three before declaring the form working:
1. A CORS preflight from the new origin echoes it in `access-control-allow-origin` (compare against a known-good host).
2. Submit the live form in a browser and confirm the UI success state.
3. Cross-check the backend log shows a sent/200 outcome for the new origin host. A tokenless POST should be rejected with a captcha error; a host missing from the per-host config should return a distinct configuration error.

## Read The Runbook

For command patterns and exact checklist detail, read `references/deployment-runbook.md` before planning or executing a static-site AWS deployment.

## SEO / GEO Baseline (Required For Every New Site)

A static site is not done when it serves 200. Before calling deployment complete, verify the build ships:

- SSG/prerendered HTML for every indexable route (content visible without JS; LLM crawlers read raw HTML).
- Self-referencing canonical per host.
- Unique title (<=60 chars) and meta description (<=160 chars).
- Open Graph + Twitter card tags with image dimensions and alt text; `og:locale`.
- JSON-LD (`WebPage` + `Organization`/`Service` as fits) with accurate per-page audience/serviceType.
- `robots.txt` allowing crawl and pointing at the sitemap.
- `sitemap.xml` scoped to the host, with `lastmod`.
- `llms.txt` carrying the actual pitch and proof entities, beyond bare URLs.
- Hero/LCP image preloads; webp/avif for heavy art.
- `www` variant 301s to the canonical host (CloudFront Function), never a TLS failure or NXDOMAIN.

## Search Console And Analytics Onboarding (Required)

After DNS resolves, in the browser:

1. Google Search Console: if a Domain property covers the apex (e.g. `example.com`), subdomains are auto-verified; submit the new host's `sitemap.xml` in that property. Expect "Couldn't fetch" minutes after DNS creation; it self-resolves.
2. Confirm the analytics object is legitimate before relying on a script tag:
   - The GA4 Measurement ID must belong to a visible, governed stream in the operator's GA property, or the deployment docs must explicitly name the external property/account that owns it.
   - For paid or actively evaluated landing pages, prefer one visible GA4 web stream per production hostname when an Analytics admin can create it. If a shared stream is used instead, the site must emit easy GUI separation signals such as hostname, page location, site-specific event names, and site slug/host parameters.
   - Record the stream name, stream URL, stream ID, Measurement ID, and reason for using that stream in the site docs.
3. Confirm live analytics beacons from the deployed host:
   - GA4 `google-analytics.com/g/collect` uses the expected `tid` and current page URL.
   - Google Ads tags load and conversion actions are connected when the page is part of a Google Ads funnel.
   - Microsoft Ads/UET tags load and event goals are connected when the page is part of a Microsoft Ads funnel.
4. Paid landing pages require attribution plumbing before spend:
   - Final URLs or final URL suffixes must preserve `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, and click IDs such as `gclid`, `gbraid`, `wbraid`, and `msclkid`.
   - Browser code must retain those values through the form submission path and include them in the backend payload or contact notes where the backend supports it.
   - Search campaigns must have focused match types and negative terms; "search terms fixed" means preventing bad-intent queries from matching rather than merely reviewing them after spend.
5. For forms, test attribution end to end before claiming ad measurement works:
   - Submit a live test only when the user has approved the side effect.
   - Verify UI success, backend logs, persisted contact row, trace ID, and attribution fields/notes agree.
   - If a conversion action says `Tag inactive`, `No recent conversions`, or equivalent after the event window, the paid funnel is not verified.
6. reCAPTCHA browser keys: classic/enterprise domain lists cover subdomains of listed domains automatically; keys listing only a specific subdomain (e.g. `portal.example.com`) must have the new hostname added explicitly.
7. Backend allowlists for form/API endpoints (CORS origin lists and per-host captcha config) must gain the new origin before live form testing.

