Skip to content
Siteon
Contents Editable area markers (CMS)

Docs

Editable area markers (CMS)

Use data-cms attributes to let the business owner edit text, photos, news lists, article pages, and multiple languages.

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.

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

AttributeEffect
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-emptyRemoves 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 sort in 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". url is 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 with data-embed
  • data-cms-limit accepts 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.html is served if present)
  • The path and collection key come from the relay's CMS_NEWS_PATH environment variable (default news)

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 a url and 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 lacks data-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.

SyntaxOutput
**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

  1. List languages in locales in schema.json (e.g. ["ja", "en"]). The first is the primary language, and editors enter content in it
  2. Text fields (text / textarea / blocks) are translated automatically by AI after saving (on plans with AI translation). Add "translate": false to fields that shouldn't be translated (times, dates, categories, etc.)
  3. Fields with no translation, or an empty one, show the primary-language value
  4. Prepare a page per language and write language:prefix pairs in the relay's CMS_LOCALES (e.g. ja:/,en:/en). The language is determined by the URL alone; Accept-Language is 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-attr per element. Writing two, like href: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-list inside <template data-cms-list>. To repeat within a single item, use data-cms-each
  • data-cms-empty doesn't appear when data-cms-filter leaves zero items. data-cms-empty only 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

This page is also available as Markdown. For AI agents, pass llms.txt (contents) or llms-full.txt (full text).