# Static Sites

A Keel static site uses a private S3 bucket behind CloudFront. Declare it in `keel.yml`, provision it with `keel up`, and publish it with `keel sites deploy`. CloudFront accesses the bucket through an origin access control.

`sites` sits alongside `services`. An app can contain services, sites, or both, allowing a frontend and its API to share one config and deployment workflow.

```yaml
sites:
  frontend:
    root: ./dist              # required — where the built site lives
    build: npm run build      # optional — a site of committed files needs none
    # build_in: codebuild     # default: codebuild when pipeline.source is set, else local
    spa: true                 # single-page app: client-side routing works on refresh
    cdn:
      domain: www.example.com # must sit inside the app's domain zone
      # price_class: PriceClass_100
      # default_ttl: 86400
```

```bash
keel sites list               # configured and live state (--json)
keel sites deploy             # build, publish, invalidate the CDN (--local, --skip-build, --no-delete, --dry-run)
keel sites verify frontend    # check the certificate in ACM and record that it is issued
```

## The essentials

- **`root`** is required and identifies the directory to publish.
- **`build_in`** controls whether the build runs in CodeBuild or locally. It defaults to CodeBuild when `pipeline.source` is set and to local otherwise.
- **`spa: true`** maps S3 403 and 404 responses to `index.html`, allowing client-side routes such as `/orders/42` to load directly.
- **`cdn`** is required because Keel does not make the bucket public. Without `cdn.domain`, the site uses its assigned `*.cloudfront.net` domain.
- **Certificates** are created in us-east-1 as required by CloudFront.
- **`default_ttl`** defaults to 86400 seconds. Keel invalidates CloudFront on deployment, but other caches may continue to honor this TTL.
- **`price_class`** defaults to `PriceClass_100` for North America and Europe. `PriceClass_All` adds more regions at a higher per-GB rate.

## Where the build runs

`build_in` is `codebuild` or `local`.

When omitted, `build_in` defaults to `codebuild` if `pipeline.source` names a repository and to `local` otherwise. `keel up`, `keel deploy`, and `keel sites deploy` print the selected location before building:

```
frontend: building in codebuild — pipeline.source is github
```

**In CodeBuild**, the build runs from a clone of the selected commit and uploads the output from AWS. The local machine does not need the build toolchain or upload bandwidth, and the deployment can run in CI. `root` must point to a directory inside the repository.

**Locally**, the build runs in your project root and the files are uploaded from your disk, comparing each one's content hash so only what changed is sent.

You can set `build_in: local` even when a source is configured, or pass `keel sites deploy --local` for one deployment. A site configured for local builds has no CodeBuild project, so there is no flag to force the reverse override.

### What a CodeBuild site build is

Each site has its own CodeBuild project, separate from service image builds. It uses a site-specific buildspec without a privileged Docker daemon, and IAM access is scoped to the site's buckets. Keel retains responsibility for CloudFront invalidation and distribution changes.

Local and CodeBuild uploads apply the same `Content-Type` and `Cache-Control` rules. Assets are uploaded before HTML, and old files are deleted only after the new files are present. This keeps the previous build readable until the new entry points are published.

CodeBuild deployments always invalidate CloudFront because the remote build does not compare output hashes with the previous deployment.

### Preflight

Git preflight checks apply only to sites built in CodeBuild, which clones a specific commit. Local-only site deployments do not require a repository or run these checks.

## Sites and services together

An API on ECS and a frontend on CloudFront can share one `keel.yml`, hosted zone, and deployment:

```yaml
name: shopfront
region: eu-west-1

services:
  api:
    type: web
    port: 3000
    health_check: /healthz

sites:
  frontend:
    root: ./dist
    build: npm run build
    spa: true
    cdn:
      domain: www.example.com

domain:
  name: app.example.com     # the load balancer
  zone: example.com
  dns: route53

pipeline:
  source: github
  repo: myorg/shopfront
```

`keel deploy` rolls out services before publishing sites, inside the same deploy lock. CodeBuild uses the same commit for both. Use `keel deploy frontend` or `keel deploy api` to deploy one target.

The load balancer and site require separate hostnames, such as `app.example.com` and `www.example.com`. Keel rejects a site that reuses the application's hostname because the DNS alias would conflict. Routing both through one CloudFront distribution is not yet supported.

## Sites-only infrastructure

When an app declares no services, Keel omits ECS, the load balancer, VPC, NAT gateway, and ECR resources. `keel cost` excludes those resources from the estimate.

Keel rejects a `database`, `cache`, or `waf` in a sites-only app because those features require service networking and, for WAF, a load balancer.

CodeBuild runs in an AWS-managed network and does not require a VPC or NAT gateway. A sites-only app can therefore build in AWS without provisioning service infrastructure:

```yaml
name: docsite
region: us-east-1

sites:
  www:
    root: ./dist
    build: npm run build
    spa: true
    cdn: {}

pipeline:
  source: github
  repo: myorg/docsite
```

`keel cost` shows a site build as its own **CodeBuild (sites)** line — separate from the image builds, which may run on a larger (and twice as expensive) instance.

## Custom domains for sites

`cdn.domain` must sit inside the zone the top-level `domain:` block declares — Keel creates no second zone and derives none by stripping labels. With `domain.dns: external`, run `keel domains records` for the DNS entries to create by hand, and `keel sites verify` once the certificate validates. See [Custom Domains & WAF](./domains-and-waf).

## Limits

- **No build cache in CodeBuild.** Every build reinstalls its dependencies, which for a Node front end is most of the wall clock.
- **No build-time environment variables** for a CodeBuild site build, so a build-time API URL has to come from the repository.
- **The site build runs on `BUILD_GENERAL1_SMALL`** (3 GB), and there's no key to raise it. A very large front end can exhaust it — the failure reads as `JavaScript heap out of memory`.
- **No per-pattern `Cache-Control`.** Hashed asset filenames deserve `immutable`, but guessing which files are hashed would cache a non-hashed one for a year.
