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 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. The API key needs the
deploy:writescope - 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
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, andmail, 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, theidinsitekit/site.jsonis used
What happens on deploy
- The CLI reads the files in the folder and computes a sha256 for each one
- It uploads only the files the server doesn't have yet (6 in parallel). Files unchanged since the last deploy are not sent
- 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 (A page per article)
- Language prefixes come from
localesin schema.json. The first language is served at/, the others at/<language code>(for example,["ja", "en"]givesja:/,en:/en). The article page path isnews. Ways to change either are in preparation 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
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 versionYou 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 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) |
deployis 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
deploys 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)
Custom domains Soon
You can publish on your own domain (www.example.com); requests are not open yet.
wwwis the primary host: pointwww.example.comat Siteon with aCNAME. You also add aTXTrecord to verify ownershipexample.com(without www) redirects towww- Lite allows 1, Standard 3, and Business 10 (a
wwwhost 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.
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.
functions/_middleware.ts
functions/_cms/render.ts_cms/render.tsis generated. Don't edit it (edits will conflict with future updates)- A public way to distribute these files (download, CLI) is in preparation 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.
{
"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.
"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.comdoes not allowa.www.example.com). Case-insensitive - Only HTML gets the header (not images or CSS)
- If the site already sends
X-Robots-Tag(via_headersor 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.
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.
<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-cmsmarkers - 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 articleurlare 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 carrydata-sk-rendered="1"and the script skips them, but don't combine the two in the first place. The script withoutdata-embedis meant to be used together with the relay (it's needed for forms and traffic-source recording).