How it works
Add a data-cms attribute to an HTML element and that spot becomes editable from the dashboard. What can be edited is declared in schema.json.
- The text inside the element is the default text. When there is no value (not entered yet, or the CMS can't be reached), the HTML stays as is
- Values are escaped and inserted as text. Editors' input can never inject HTML or scripts
- Values are embedded by Siteon hosting or the Cloudflare Pages relay (before delivery), or by
data-embed(at render time) (Hosting and delivery)
Marker reference
| Attribute | Effect |
|---|---|
data-cms="key" | Replaces the text inside the element |
data-cms-html="key" | Same replacement, with line breaks turned into <br> |
data-cms-src="key" | Replaces src on img / video / source |
data-cms-attr="attribute:key" | Replaces any attribute (e.g. content:title, href:url) |
data-cms-blocks="key" | Renders an article body (a sequence of blocks) inside the element |
data-cms-hide-empty | Removes the whole element when the value is an empty string (only with data-cms / data-cms-html) |
<template data-cms-list="k" data-cms-limit="n"> | Repeats its contents for each item in the list (up to n items) |
<template data-cms-list="k" data-cms-filter="f=v"> | Repeats only for items matching the condition |
<template data-cms-empty="k"> | Shows its contents only when the list is empty |
<template data-cms-entry="k"> | On an article page, renders that single article |
<template data-cms-each="k"> | Repeats for each element of an array (tags, etc.) |
data-cms-fallback="k" | Removes the element once list k arrives (a fallback shown when it doesn't) |
Text
Values in a group (groups in schema.json) are referenced as <group>.<field>.
<dd data-cms="hours.days">Mon–Sat</dd>
<!-- Line breaks become <br>. If empty, the whole box disappears -->
<p class="notice" data-cms-html="hours.notice" data-cms-hide-empty>No temporary closures at the moment.</p>Photos
Photos in lists and articles (fields with type: "image") arrive as absolute URLs. Put them into src with data-cms-src.
<template data-cms-list="news">
<article>
<img data-cms-src="thumbnail" src="/img/placeholder.jpg" alt="">
<h3 data-cms="title">Article title</h3>
</article>
</template>URL attributes such as href and src never receive values starting with javascript: / data: / vbscript: (the default value stays). data-cms-attr cannot rewrite on… event attributes, srcdoc, or formaction.
Lists such as news
Collections (collections in schema.json) are repeated with <template data-cms-list>. Inside it, data-cms takes the field names of a single item.
<ul class="news">
<template data-cms-list="news" data-cms-limit="3">
<li>
<time data-cms="date">2026-01-01</time>
<a data-cms-attr="href:url" href="/news/"><span data-cms="title">Article title</span></a>
</li>
</template>
<!-- Shown only when there are no items -->
<template data-cms-empty="news">
<li>No news yet.</li>
</template>
<!-- Removed once the list arrives. Stays if the CMS can't be reached -->
<li data-cms-fallback="news"><span>Our website is now live</span></li>
</ul>- List order follows
sortin schema.json (e.g."date desc"). Without it, items follow the order arranged in the dashboard - A single list holds up to 100 items, newest first
- Link to articles with
data-cms-attr="href:url".urlis not in schema.json; the relay adds it to each article as/news/<id>(with the language prefix, as an absolute URL). It is not added withdata-embed data-cms-limitaccepts integers only ("3"). Anything else is treated as no limit and every item is shown
A page per article
Per-article URLs (/news/<id>) are rendered by the relay using news/_entry.html as the template. You don't need one file per article. This is available only on sites using the Cloudflare Pages relay.
public/news/_entry.html Article page template
public/news/_404.html Page for IDs that don't exist (optional)
public/en/news/_entry.html One per language<template data-cms-entry="news">
<h1 data-cms="title">Article title</h1>
<time data-cms="date">2026-01-01</time>
<div data-cms-blocks="body"></div>
</template>- URLs are
/news/<id>or/<language>/news/<id>.<id>is lowercase letters and digits only, up to 32 characters - IDs not among published articles return 404 (
news/_404.htmlis served if present) - The path and collection key come from the relay's
CMS_NEWS_PATHenvironment variable (defaultnews)
Per-entry pages for other collections (Siteon hosting)
On Siteon hosting, collections other than news (talent, menu items, case studies and so on) can have a page per entry too. Put _entry.html in a folder named after the collection key.
talents/_entry.html Template for each talent's page (inside: <template data-cms-entry="talents">)
en/talents/_entry.html One per language
talents/_404.html Page for IDs that don't exist (optional)- URLs are
/talents/<id>or/<language>/talents/<id>. Only in languages that have the template, each list item gets aurland the pages are listed in sitemap.xml - Collections without a template still have no per-entry pages
- The pre-publish check (
siteon check/check_site) warns when a list links to entries but there's no template, or when the template lacksdata-cms-entry
Put a template in <head> too for <title> and OGP
Only the inside of the template can read "that single article" from data-cms-entry. If you write <title data-cms="title"> directly in <head>, it reads the site-wide value instead of the article's. Since it isn't visible on the page, you typically only notice when the link is shared on social media.
<head>
<template data-cms-entry="news">
<title data-cms="title">News | Store name</title>
<meta name="description" data-cms-attr="content:lead" content="News from Store name.">
<meta property="og:title" data-cms-attr="content:title" content="News | Store name">
<meta property="og:image" data-cms-attr="content:thumbnail" content="../assets/hero.jpg">
<meta property="og:url" data-cms-attr="content:url" content="">
<link rel="canonical" data-cms-attr="href:url" href="">
</template>
<!-- Title that remains if the CMS can't be reached and the template isn't expanded -->
<title data-cms-fallback="news">News | Store name</title>
</head>A <title> with data-cms-fallback is removed once the template is expanded. Without it, you'd get two <title> elements. An article's url is absolute, so it can go straight into og:url and canonical.
Article body (blocks)
Fields with type: "blocks" are rendered with data-cms-blocks. Every class on the output starts with sk-, and styling is up to your site's CSS (no stylesheet is provided).
.sk-h { font-size:1.05rem; margin:2rem 0 .5rem; } /* Heading */
.sk-p { margin:0 0 1rem; } /* Paragraph */
.sk-q { border-left:3px solid #e5e7eb; padding-left:1rem; color:#6b7280; }
.sk-ul, .sk-ol { margin:0 0 1rem; padding-left:1.4rem; }
.sk-img img { width:100%; height:auto; }
.sk-img figcaption { color:#6b7280; font-size:.8rem; }
.sk-video { width:100%; }
.sk-embed { position:relative; padding-top:56.25%; }
.sk-embed iframe { position:absolute; inset:0; width:100%; height:100%; border:0; }
.sk-hr { border:0; border-top:1px solid #e5e7eb; }
.sk-pdf, .sk-card, .sk-video-link { display:inline-block; }
/* Text colors. Keep the class names. Adjust the color values to your site's palette */
.sk-c-red { color:#c0392b; }
.sk-c-gold { color:#b8860b; }
.sk-c-gray { color:#6b7280; }
.sk-c-black { color:#111827; }
.sk-c-green { color:#15803d; }
.sk-c-blue { color:#1d4ed8; }Editors can use the following syntax in the body.
| Syntax | Output |
|---|---|
**bold** | <strong> |
[text](https://…) | A link that opens in a new tab (http/https only) |
{color:red}text{/color} | <span class="sk-c-red"> (six colors: red / gold / gray / black / green / blue) |
A bare https://… | Linked automatically |
If you forget to define .sk-c-*, color markup has no visible effect. On sites that use colors, define all six up front.
Tags
Set a collection field to "type": "tags" to tag articles (up to 20 per article, up to 40 characters per tag, / # ? not allowed). Tags are not translated.
Tags on each article — output them one by one with data-cms-each. The relay turns each tag into {tag, url, color}, so you can write the name and a link to its tag page.
<template data-cms-each="tags">
<a data-cms-attr="href:url" href="#">
<span class="sk-tag" data-cms-attr="data-color:color" data-cms="tag">Tag</span>
</a>
</template>Tag list (navigation) — tags in use arrive as a list named <collection>_tags (e.g. news_tags), each with {tag, count, url, color}.
<template data-cms-list="news_tags">
<a data-cms-attr="href:url" href="#"><span class="sk-tag" data-cms="tag">Tag</span></a>
</template>Tag pages — /news/tag/<tag>/ is rendered by the relay using news/_tag.html as the template. The news list arrives already filtered by that tag. The display name is news.tag. A tag that doesn't exist does not return 404; it returns an empty list (so data-cms-empty is shown).
<h1>Tag: <span data-cms="news.tag">Tag name</span></h1>
<ul>
<template data-cms-list="news">
<li><a data-cms-attr="href:url" href="./"><span data-cms="title">Title</span></a></li>
</template>
<template data-cms-empty="news"><li>No news with this tag yet.</li></template>
</ul>Tag colors are set under "Settings → Advanced → Manage tags" in the dashboard. Tags without a color get no data-color attribute and use the default .sk-tag look.
.sk-tag { display:inline-block; padding:.1rem .5rem; border-radius:999px;
font-size:.75rem; background:#f3f4f6; color:#374151; text-decoration:none; }
.sk-tag[data-color=red] { background:#fdecea; color:#c0392b; }
.sk-tag[data-color=gold] { background:#fdf6e3; color:#8a6d1f; }
.sk-tag[data-color=gray] { background:#f3f4f6; color:#4b5563; }
.sk-tag[data-color=black] { background:#1f2937; color:#fff; }
.sk-tag[data-color=green] { background:#e8f5ec; color:#15803d; }
.sk-tag[data-color=blue] { background:#e8effd; color:#1d4ed8; }Writing data-cms-filter="tags=autumn" filters outside tag pages too (array fields match on "contains", single values on "equals").
Category badges
select options can have colors (schema.json). Besides the stored value, the public JSON includes <field>_label (the display text) and <field>_color (the color name; omitted for options without a color).
<span class="sk-badge" data-cms="category_label" data-cms-attr="data-color:category_color"></span>Write the .sk-badge[data-color=…] CSS for the six colors in the same form as .sk-tag.
Multiple languages
- List languages in
localesin schema.json (e.g.["ja", "en"]). The first is the primary language, and editors enter content in it - Text fields (
text/textarea/blocks) are translated automatically by AI after saving (on plans with AI translation). Add"translate": falseto fields that shouldn't be translated (times, dates, categories, etc.) - Fields with no translation, or an empty one, show the primary-language value
- Prepare a page per language and write
language:prefixpairs in the relay'sCMS_LOCALES(e.g.ja:/,en:/en). The language is determined by the URL alone;Accept-Languageis ignored
With data-embed, content is fetched in the language from <html lang> or the script's data-locale.
Whether AI translation is available, and how many times, depends on the plan (Plans and limits).
Rules for writing markers
Breaking these produces no error; the page just renders wrong or nothing happens. Start here when something doesn't work.
- Don't nest an element with the same tag name inside a marked element.
<div data-cms="x"><div>…</div></div>breaks. Use a different tag such as<span> - One marker per element. If you add two, the later one wins
- Only one
data-cms-attrper element. Writing two, likehref:url data-color:color, writes neither. Split them across elements - Write tag and attribute names in lowercase.
<DIV DATA-CMS="k">is ignored - Don't put markers inside
<script>or<style>. Their contents would be rewritten (inside HTML comments is safe) - Don't put another
data-cms-listinside<template data-cms-list>. To repeat within a single item, usedata-cms-each data-cms-emptydoesn't appear whendata-cms-filterleaves zero items.data-cms-emptyonly checks whether the list itself is empty- Make the static text make sense on its own. It is what gets published when the CMS can't be reached