# Hosting and delivery

> Hosting on every plan (siteon deploy, rollback, subdomains, propagation time, suspension; the Free plan shows a "Made with Siteon" badge), and how to keep your current hosting (Cloudflare Pages relay, data-embed).

URL: https://siteon.dev/docs/hosting

Siteon is infrastructure for AI-built websites: it adds hosting, a CMS your client can edit, contact forms, analytics and control from AI agents (CLI and MCP) around static HTML built with AI or by hand, so it is ready to ship.

## Three ways to host

There are three ways to put CMS values into your HTML. Forms and traffic-source recording work with the script tag alone in all three.

| | Siteon hosting | Cloudflare Pages relay | `data-embed` (other hosting) |
|---|---|---|---|
| Where files live | Siteon (published with `siteon deploy`) | Your Cloudflare Pages | Your hosting |
| When values are inserted | Before delivery (server side) | Before delivery (server side) | After the page renders (browser side) |
| Search engines and visitors without JavaScript | Receive the page with values filled in | Receive the page with values filled in | May pick up the static text |
| Flicker on load | None | None | Static text shows for a moment |
| Per-article pages, tag pages, article `url` | Available | Available | Not available |
| GTM ID injection from the dashboard | Available | Available | Not available (use `data-gtm`) |
| Time for edits to go live | Within 15 seconds | Up to 1 minute | Up to 1 minute |

## Hosting (siteon deploy)

Publish a built folder (`dist/` or similar) with a single command. You can publish on the Free plan; paid plans (Lite, Standard, Business) add more storage, custom domains (coming soon) and badge removal. There's no extra charge for hosting.

- Sites are published at `https://<subdomain>.siteun.com/`
- CMS values are embedded into HTML before delivery. You need neither relay files nor `data-embed`. Keep the script tag in place for forms and traffic-source recording
- Everything is done from the [CLI and MCP server](https://siteon.dev/docs/cli-mcp). The API key needs the `deploy:write` scope
- Free sites can also go live at `https://<subdomain>.siteun.com/` and are indexed by search engines. A small "Made with Siteon" badge appears in a corner of the page (not on paid plans). Free sites can deploy up to 20 times a day, and every deploy is scanned automatically (a suspicious version is held instead of going live)
- Custom domains and badge removal come with paid plans (from $4/mo)

### Your first deploy

```sh
siteon hosting enable --site hanaya     # First time only. Picks the subdomain
siteon deploy ./dist --site hanaya      # Publishes. Prints the URL
```

- If you don't specify one, the subdomain is the site ID
- Subdomains use 3 to 63 lowercase letters, digits, and hyphens (no hyphen at the start or end). Reserved words such as `www`, `admin`, and `mail`, and names containing banking, payment, or major brand names, are not allowed
- If the site ID doesn't meet these rules, the command stops with `subdomain_required`. Run it again with `--subdomain another-name`
- A subdomain that was once in use cannot be taken by another account for 90 days after it is released (to prevent link hijacking)
- You can change a site's subdomain up to 3 times a month. You can also change it in the dashboard under "Hosting" → "Subdomain"
- If you omit `--site`, the `id` in `sitekit/site.json` is used

### What happens on deploy

1. The CLI reads the files in the folder and computes a sha256 for each one
2. It uploads only the files the server doesn't have yet (6 in parallel). Files unchanged since the last deploy are not sent
3. Once everything has arrived, the live version is switched over in one step. If anything fails midway, the live site stays on the previous version

If the connection drops, the CLI retries the same operation, so nothing is published twice. On failure, the CLI cancels the reservation for anything it had already uploaded before exiting.

### Files you can deploy

| Item | Rule |
|---|---|
| Extensions | Only `html` `htm` `css` `js` `mjs` `json` `txt` `xml` `svg` `png` `jpg` `jpeg` `webp` `avif` `gif` `ico` `woff` `woff2` `ttf` `mp4` `webm` `pdf` `webmanifest` `map`. Anything else (`exe` `zip` `apk`, etc.) is stopped with `file_type_not_allowed` before upload |
| Reserved paths | Paths starting with `admin/`, `v1/`, or `_sk/` cannot be used (`reserved_path`; case-insensitive) |
| Dotfiles | Names starting with a dot, such as `.git`, `.env`, and `.DS_Store`, are not uploaded. Only `.well-known/` is published |
| Single file | Up to 25MiB (`file_too_large`) |
| Storage, file count, retained versions | Depends on the plan (table below) |

| | Free | Lite | Standard | Business |
|---|---|---|---|---|
| Storage (total of retained versions) | 20MB | 200MB | 1GB | 5GB |
| Files per deploy | 200 | 1,000 | 5,000 | 20,000 |
| Retained versions (including the live one) | 2 | 5 | 20 | 50 |

Storage is the total size of the files used by retained versions. Files with identical content count once across versions. Photos uploaded from the dashboard are counted separately.

### How URLs map to files

| Request | Returns |
|---|---|
| `/` | `index.html` |
| `/about` | `about.html` (or an extensionless file named `about`, if present) |
| `/about/` | `about/index.html` |
| `/about.html`, `/index.html`, `/about/index.html` | 301 to the form without the extension or `index.html` (`/about`, `/`, `/about/`), keeping the query string |
| `/about/` (no `about/index.html`, but `about.html` exists) | 301 to `/about` |
| `/about` (`about/index.html` exists) | 301 to `/about/` |
| `/sitemap.xml`, `/robots.txt` | Your file if you deployed one; otherwise generated automatically (see below) |
| Not found | The site's `404.html` with status 404 if it exists, otherwise a shared 404 page |

Variants are redirected with a 301 so each page has exactly one URL. Links with `.html` still reach the page, but we recommend writing links without the extension.

**Automatic sitemap.xml and robots.txt:** if you don't deploy your own, we build sitemap.xml from your published HTML (excluding `404.html`, templates whose path has a segment starting with `_`, and noindex pages) plus per-article pages. robots.txt allows everything and points to the sitemap. Files you deploy yourself are never overwritten.

- Per-article pages, tag pages, and per-language paths work the same way as with the [relay](#cloudflare-pages-relay) ([A page per article](https://siteon.dev/docs/cms#a-page-per-article))
- Language prefixes come from `locales` in schema.json. The first language is served at `/`, the others at `/<language code>` (for example, `["ja", "en"]` gives `ja:/,en:/en`). The article page path is `news`. Ways to change either are in preparation (coming soon)

### Propagation time and caching

Switching the live version, `rollback`, saving in the dashboard, and suspension all reach every page **within 15 seconds**. Files already cached in a visitor's browser may look stale for the periods below.

| File | Browser cache |
|---|---|
| HTML | Revalidated every time (`no-cache`) |
| Files with a hash in the name (`app.3f9a8b7c.js`, etc.), and anything under `_astro/` or `assets/` | 1 year (`immutable`) |
| Images, fonts, video, PDF | 1 day |
| Everything else (CSS, JS, JSON, etc.) | 5 minutes |

If you turn on your build tool's "hash file names" option, new files are served the moment you publish. To check how a page looks right after saving, use the dashboard preview.

### Rolling back to an earlier version

```sh
siteon deploys --site hanaya              # List versions (* marks the live one)
siteon rollback --site hanaya             # Roll back one version
siteon rollback <deploy_id> --site hanaya # Roll back to a specific version
```

You can only roll back to retained versions (see the table above for how many are kept). Older versions are removed as new deploys push them out (`deploy_not_retained`).

### Search engines

| Site state | Search engines | Page |
|---|---|---|
| Paid plan | Indexed | As is |
| Free, or a site treated like Free after cancellation or 31 days of unpaid invoices | Indexed | A small "Made with Siteon" badge appears in a corner of the page |
| Shared subdomain while a custom domain is in use (coming soon) | Not indexed (canonical points to the custom domain) | As is |

### When a site goes offline and when it doesn't

**Delivery does not stop for late payment, cancellation, or exceeding limits.** A site stops being served only in these cases:

| State | Response |
|---|---|
| Suspended after an abuse report (phishing, etc.) or an inspection | Every page returns a `451` suspension page |
| Site deleted or account closed | `404` |
| Never published | `404` "Not published yet" |
| Siteon cannot check the site's state because of an outage on our side | `503` (retry in a few seconds) |

- `deploy` is still accepted while a site is suspended (so you can fix it before appealing). The new version goes live once the suspension is lifted
- From day 8 to day 30 of an unpaid invoice, new `deploy`s are refused (`402`). After cancellation or from day 31 unpaid, the site is treated like Free and can deploy within the Free plan's limits. In every case the live version keeps being served (for behavior by payment state, see [Plans and limits](https://siteon.dev/docs/plans#when-a-payment-fails))

### Custom domains (coming soon)

You can publish on your own domain (`www.example.com`); requests are not open yet.

- `www` is the primary host: point `www.example.com` at Siteon with a `CNAME`. You also add a `TXT` record to verify ownership
- `example.com` (without www) redirects to `www`
- Lite allows 1, Standard 3, and Business 10 (a `www` host and its non-www form count as one)
- Once a custom domain is active, the shared subdomain is set to stay out of search, and the custom domain is declared as the canonical URL

The DNS records to add, examples by DNS provider, and what each status and error means are in [Custom domains and allowed origins](https://siteon.dev/docs/domains#custom-domain).

### Pre-publish inspection and abuse reports

Every `deploy` inspects the HTML automatically. If it finds a password field, a form that submits to another site, or a brand name next to sign-in wording, the Free plan does not publish and stops with `422` `check_failed` (the live version keeps serving). On the Free plan the URL is also scanned before it goes live. Paid plans publish first and send the site for inspection.

A site being abused (phishing and the like) can be reported through the form at `https://siteun.com/report`.

## Cloudflare Pages relay

### Files to add

Add two relay files to your site's `functions/` directory.

```text
functions/_middleware.ts
functions/_cms/render.ts
```

- `_cms/render.ts` is generated. Don't edit it (edits will conflict with future updates)
- A public way to distribute these files (download, CLI) is in preparation (coming soon)

### Environment variables

Set these in the Pages project's `vars` (in `wrangler.jsonc` or the Cloudflare dashboard).

| Variable | Required | Description |
|---|---|---|
| `CMS_SITE` | Required | Site ID |
| `CMS_ORIGIN` | Required | `https://cdn.siteon.dev` (no trailing slash) |
| `CMS_LOCALES` | | Comma-separated `language:prefix` pairs. Defaults to `ja:/` |
| `CMS_NEWS_PATH` | | Article page path, also used as the collection key. Defaults to `news` |
| `CMS_INDEXABLE_HOSTS` | | Comma-separated host names allowed in search. Does nothing if omitted |

If `CMS_SITE` or `CMS_ORIGIN` is missing, the relay does nothing and returns the static HTML as is.

```jsonc
{
  "name": "hanaya",
  "compatibility_date": "2026-08-01",
  "pages_build_output_dir": "public",
  "vars": {
    "CMS_SITE": "hanaya",
    "CMS_ORIGIN": "https://cdn.siteon.dev",
    "CMS_LOCALES": "ja:/,en:/en",
    "CMS_NEWS_PATH": "news"
  }
}
```

### Language detection

Write `CMS_LOCALES` like `ja:/,en:/en,zh-tw:/zh-tw`. The first entry is the primary language. The language is chosen by the **longest matching** path prefix.

| Path | Language |
|---|---|
| `/`, `/about/` | `ja` (unknown paths also fall here) |
| `/en`, `/en/`, `/en/about`, `/en.html` | `en` |
| `/english` | `ja` (a different page that merely starts with the prefix doesn't match) |
| `/zh-tw/news/x` | `zh-tw` |

`Accept-Language` is ignored. The same URL shows the same language to everyone.

### Keeping out of search (`CMS_INDEXABLE_HOSTS`)

If a preview host (such as `*.pages.dev`) and your production host serve the same content, they are treated as duplicate pages, and the preview can end up in search results. List **only your production hosts** in `CMS_INDEXABLE_HOSTS`, and HTML served from any other host gets `X-Robots-Tag: noindex`.

```jsonc
"CMS_INDEXABLE_HOSTS": "www.example.com,example.com"
```

| Value | Meaning |
|---|---|
| Unset or empty | Does nothing |
| `www.example.com,example.com` | `noindex` on HTML from any host other than these two |
| `-` | No host is indexed (when you don't have a production domain yet) |

- Matching is exact (listing `www.example.com` does not allow `a.www.example.com`). Case-insensitive
- Only HTML gets the header (not images or CSS)
- If the site already sends `X-Robots-Tag` (via `_headers` or similar), it is not overwritten

### Propagation time

The public JSON is cached for 60 seconds, and HTML returned by the relay also has `max-age=60`. Once an editor clicks "Publish", every page is updated **within 1 minute**. No site redeploy is needed.

### When the CMS is down

**The relay returns the static HTML as is.** Whether the fetch fails, times out (5 seconds), or returns broken JSON, the relay just serves the page without embedding, and the site stays up.

Article pages are the only exception. If the fetch succeeds but the ID isn't found, the relay returns 404. If the fetch fails, it returns the template as is.

### Checking that it works

When the relay embeds values, the response gets an `x-sitekit-rendered: 1` header. **Check with GET. `curl -I` (HEAD) won't show it.**

```bash
curl -sD - -o /dev/null https://example.com/ | grep -i sitekit
curl -s https://example.com/ | grep -o 'data-sk-rendered="1"'
```

If it's missing, an environment variable isn't set, the relay can't reach the CMS, or the page isn't HTML (200).

## Other hosting (`data-embed`)

On hosts other than Cloudflare Pages (such as static hosting that can't run functions), add `data-embed` to the script tag.

```html
<script src="https://cdn.siteon.dev/sitekit.js" data-site="hanaya" data-embed defer></script>
```

- After the page renders, the script fetches the public JSON and fills values into the `data-cms` markers
- **It replaces the entire contents of `document.body`.** Event listeners attached to elements inside body are lost (forms are re-bound automatically). Avoid it if your site has its own JavaScript
- If the fetch fails, it does nothing and the static HTML remains
- Per-article pages (`/news/<id>`), tag pages, and article `url` are relay features and are not available

> On sites that use the relay, **don't add `data-embed`.** Embedding can't run twice (the second pass overwrites the first). Relayed pages carry `data-sk-rendered="1"` and the script skips them, but don't combine the two in the first place. The script without `data-embed` is meant to be used together with the relay (it's needed for forms and traffic-source recording).
