# Host a static site or a hybrid site

> Deploy a site built to plain files that the edge serves itself, set its redirects and headers, and add server routes with a hybrid site.

Source: https://www.coritan.com/docs/managed-containers/static-sites/

A *static site* is a site whose build writes plain files, such as HTML, CSS, JavaScript and images, to one directory. App Deployment serves those files from the edge itself: there is no instance and no server to size, and the site answers from every location. A *hybrid site* adds server routes: the edge serves the files the build wrote, and sends every other request to the site's instances, which run where you choose.

A static site counts as one of your plan's websites, and costs nothing within the plan's allowances ([How App Deployment is billed](/docs/managed-containers/how-billing-works/)). A hybrid site's instances are billed like any other instance.

## Before you begin

- Keep the site in a git repository whose build writes its files to one directory, such as `dist`, `build` or `out` ([Deploy from a git repository](/docs/managed-containers/deploy-from-git/)). Once the site is deployed, you can also upload a directory with `coritan deploy` ([Use the Coritan CLI](/docs/managed-containers/cli/)).
- Order it from the dashboard's **New deployment**, when its order page offers **What to deploy**. Without it, static sites and hybrid sites are not open yet, and the API answers `404` for them too.

## Deploy a static site {#deploy-a-static-site}

1. In the [dashboard](https://www.coritan.com/dashboard/deployments), go to **App Deployment** and select **New deployment**. Under **What to deploy**, select **Static site**, or **Starter** to begin from a starter such as Astro or Vite.
2. Under **Repository**, choose **From GitHub** and select the repository, or choose **Paste an address** and paste its `https` address.
3. Under **Build and run**, check the **Build command** and the **Output directory** the build writes the files to. Left blank, the output directory is the framework's own ([Frameworks we build](/docs/managed-containers/deploy-from-git/#frameworks)), or we look for `dist`, `build`, `out`, `public` or `_site`. A static site has no size or locations to choose.
4. Under **Networking**, keep the **Deployment name** or type your own, and add any **Custom domains**. Under **Git**, check the **Production branch**.
5. Select the button under the summary. A static site on its own has nothing to pay when you order it ([Place the order](/docs/managed-containers/order-a-server/#place-the-order)).
6. If you pasted the address of a repository on GitHub, connect GitHub on the deployment's **Git** tab once it exists, so that each push builds ([Connect GitHub](/docs/managed-containers/deploy-from-git/#connect-github)).

With the API, write the order's body, as [With the API](#with-the-api) shows: `"kind": "static"`, the repository in `source.git`, and the build settings in `build`, with the output directory the build writes the files to in `output_dir`. To begin from a starter, put its name in `source.starter` instead ([Deploy from a git repository](/docs/managed-containers/deploy-from-git/#with-the-api)). Send it to `POST /checkout/quote` to see what it costs, then to `POST /services/order` with one of the plan's prices as `pricing_id`. A static site is priced at nothing on either an hourly or a monthly price, and an hourly price needs a minimum total of deposits on your account ([How hourly billing works](/docs/billing/hourly-billing/)).

Each push to the production branch builds a release, and the edge serves its files once they are all uploaded. The site answers at its platform address, such as `web-shop.apps.example.net`, and at your own domains ([Add a domain and protection](/docs/managed-containers/domains-and-protection/)).

To run server code as well, such as the server routes of Astro, Nuxt or Next.js, order a hybrid site instead: select **Git repository**, then **Hybrid** under **How it is served** (`"kind": "hybrid"` with the API), and choose the size and the locations as for any other code ([Choose locations and scaling](/docs/managed-containers/locations-and-scaling/)).

## How the edge answers a request {#how-requests-are-answered}

For each request, the edge tries these in order and stops at the first that answers:

1. The redirects, whether or not a file exists at that path.
2. The canonical address, when clean URLs or a trailing slash rule is on: a `308` to the address of a file the output has, keeping the query.
3. The file: the path itself, then `path/index.html`, then `path.html`.
4. The rewrites, only where no file is, so a fallback such as `/* /index.html 200` leaves your assets alone.
5. For a single page app, `/index.html`. Otherwise the not found page with the status `404` (`/404.html` unless you name another), or a plain `404`. A hybrid site sends the request to its instances instead.

Header rules then apply to every answer. The edge never serves `/_redirects` or `/_headers`.

## Configure the site with coritan.json {#coritan-json}

Put a `coritan.json` in the output directory. It takes the keys `vercel.json` uses for the same things, in either spelling:

`spa`
: `true` serves `/index.html` for any path that has no file. The output needs an `/index.html`.

`not_found` (or `notFound`)
: The page to serve with `404`, such as `/errors/missing.html`.

`clean_urls` (or `cleanUrls`)
: `true` serves `/about.html` at `/about`, and redirects `/about.html` there.

`trailing_slash` (or `trailingSlash`)
: `true` adds a trailing slash to every address, `false` removes it.

`redirects`
: Each with a `source`, a `destination`, and either `statusCode` or `permanent` (`true` answers `308`, `false` answers `307`).

`rewrites`
: Each with a `source` and a `destination`, a file of the output to serve in its place.

`headers`
: Each with a `source` and its `headers`, a list of `key` and `value`.

```json
{
  "cleanUrls": true,
  "redirects": [
    {"source": "/blog/:slug", "destination": "/articles/:slug", "permanent": true}
  ],
  "rewrites": [
    {"source": "/app/:path*", "destination": "/app/index.html"}
  ],
  "headers": [
    {"source": "/(.*)", "headers": [{"key": "X-Frame-Options", "value": "DENY"}]}
  ]
}
```

A source matches its path exactly, except for placeholders: `:name` matches one part of the path, `:name*` the rest of it (or nothing), `:name+` the rest of it (at least one character), `*` the rest as `:splat`, and `(.*)` a group the destination names as `$1`. A mistake in `coritan.json` fails the build, and the build log names it.

## Redirects in a _redirects file {#redirects-file}

A `_redirects` file in the output directory works as it does on Cloudflare Pages: one rule a line, the source, the destination and an optional status.

```text
/old-page   /new-page         301
/docs/*     /guides/:splat
/app/*      /app/index.html   200
```

A rule without a status redirects with `302`. `301`, `302`, `303`, `307` and `308` redirect whether or not a file exists at the source. `200` serves the destination in place, and `404` serves it as the not found page, both only where no file is. A line the edge cannot take, such as a rule for another domain or one with conditions, is skipped, and the build log lists it as a warning. The rules in `coritan.json` come first, then those in `_redirects`, and the first rule that matches wins.

## Headers in a _headers file {#headers-file}

A `_headers` file sets response headers by path: a path on its own line, then each header on an indented line below it. `! Name` removes a header an earlier rule set.

```text
/*
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
/embed/*
  ! X-Frame-Options
```

Every rule that matches applies, in order. The first to set a header replaces the value the edge would send, and a later rule adds its value after a comma. Headers that frame the answer, such as `Content-Length`, `Content-Encoding` and `ETag`, cannot be set or removed.

## Caching and compression {#caching}

A file whose name carries its content hash, such as `app.3f9a1c.js`, or anything under `/_next/static/` or `/_app/immutable/`, is sent with `Cache-Control: public, max-age=31536000, immutable`. Every other file, and HTML always, is sent with `public, max-age=0, must-revalidate`, so browsers check for a new release each time. A header rule can set either. Text files from 1 KiB up are compressed with gzip or brotli for the browsers that take them. To turn compression off for a path, send `Cache-Control: no-transform` from `_headers`.

## Limits {#limits}

- A file can be up to 25 MiB, and a release up to 2 GiB in up to 20,000 files.
- `coritan.json`, `_redirects` and `_headers` can each be up to 256 KiB.
- A release can have up to 2,100 redirects and rewrites together, and up to 100 header rules, each of up to 2,000 characters.
- A release keeps its files for at least 30 days after it is built, so a static site can roll back to a release of the last 30 days without building it again ([Roll back](/docs/managed-containers/releases/#roll-back)).

## Result

The **Releases** tab shows the release as ready once its files are uploaded, and the site answers with them at every address. The release serving before keeps serving until then.

## Troubleshooting

Every path answers `404`
: Check the output directory in the build settings: the edge serves only what the build wrote there. For a single page app, set `"spa": true` in `coritan.json`, or add `/* /index.html 200` to `_redirects`.

The build failed on `coritan.json`
: The build log names the rule, such as `coritan.json redirects[0]: ...`. Fix the rule and push again.

A rule in `_redirects` or `_headers` does nothing
: The build log lists every line that was skipped, and why. A redirect in `coritan.json` that matches first wins.

A page still shows the old version
: HTML is sent with `max-age=0`, so the browser checks each time. If you set a longer `Cache-Control` on HTML in `_headers`, visitors keep the old page until it expires.

The order answers `402` with `"entitlement": "websites_max"`
: A static site is one of your plan's websites, beside your DNS zones and web proxies, and your account has as many as the plan allows. Delete one you no longer need, add a websites pack, or choose a plan with more ([Add capacity to a service](/docs/billing/add-ons/), [How account plans work](/docs/billing/account-plans/#what-each-plan-includes)).

## Related

- [Deploy from a git repository](/docs/managed-containers/deploy-from-git/) sets up the repository, the production branch and previews.
- [Add a domain and protection](/docs/managed-containers/domains-and-protection/) puts the site on your own domain.
- [How App Deployment is billed](/docs/managed-containers/how-billing-works/) explains the allowances a static site uses.

## With the API

A static site is ordered with `POST /api/v1/services/order`, as in [Order any kind](/docs/managed-containers/order-a-server/#order-any-kind), with `"kind": "static"` in `config` and no `placement`. The build settings are in `build`:

```json
{
  "kind": "static",
  "name": "docs-site",
  "source": {"git": {"repo_url": "https://github.com/example/docs-site", "branch": "main"}},
  "build": {"preset": "astro", "build_command": "npm run build", "output_dir": "dist"}
}
```

A hybrid site takes `"kind": "hybrid"`, with `placement` and `runtime` as for a web service. Its releases, domains and variables use the same routes as any deployment, under `/api/v1/client/deployments/{uuid}`.
