# Writing schema.json

> The file that declares the editable fields (groups / collections) and forms. Lists every type and constraint.

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

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.

## Overall shape

schema.json declares "what can be edited on this site" and "which forms it has". Both the dashboard's input fields and the shape of the public JSON are built from it.

```json
{
  "locales": ["ja", "en"],
  "groups": [
    { "key": "hours", "label": "Opening hours", "fields": [
      { "key": "days", "label": "Open days", "type": "text" },
      { "key": "time", "label": "Hours", "type": "text", "translate": false },
      { "key": "notice", "label": "Closure notice", "type": "textarea",
        "hint": "Any temporary closures. Leave empty to hide the whole box" }
    ] }
  ],
  "collections": [
    { "key": "news", "label": "News", "sort": "date desc", "fields": [
      { "key": "title", "label": "Title", "type": "text", "required": true },
      { "key": "date", "label": "Date", "type": "date", "translate": false },
      { "key": "tags", "label": "Tags", "type": "tags" },
      { "key": "body", "label": "Body", "type": "blocks" }
    ] }
  ],
  "forms": [
    { "key": "contact", "label": "Contact", "fields": [
      { "key": "name", "label": "Name", "type": "text", "required": true },
      { "key": "email", "label": "Email", "type": "email", "required": true },
      { "key": "message", "label": "Message", "type": "textarea" }
    ] }
  ],
  "tracking": { "gtm_id": "GTM-XXXXXXX" }
}
```

| Key | Required | Contents |
|---|---|---|
| `locales` | Required | List of languages. The first is the primary language |
| `groups` | | Sets of single-value fields (opening hours, store details, etc.) |
| `collections` | | Lists that grow over time (news, menu items, etc.) |
| `forms` | | Form definitions |
| `tracking` | | `gtm_id` only. A GTM ID set under "Settings → Advanced" in the dashboard takes precedence |

Unknown keys are not saved.

## Rules for key

The `key` of `groups` / `collections` / `forms`, and of the `fields` inside them, all follow the same rules.

- Starts with a lowercase letter and uses only lowercase letters, digits, and `_` (`/^[a-z][a-z0-9_]*$/`)
- Up to 40 characters
- Unique within the same list

## locales

- In the form `ja` or `zh-tw` (`/^[a-z]{2}(-[a-z]{2})?$/`). No duplicates. At least one is required
- The first is the language editors type in, and it is also shown when a translation is missing

## groups

Sets of fields that each hold a single value. `label` (a non-empty string) and `fields` are required.

In HTML, refer to them as `<group>.<field>`.

```html
<dd data-cms="hours.days">Mon–Sat</dd>
```

## collections

Lists that grow over time. `label` (a non-empty string) and `fields` are required.

- `sort` — `"<field>"` or `"<field> asc|desc"`. You can't name a field the collection doesn't have. If omitted, items follow the order arranged in the dashboard
- Sorting by a `number` field compares numerically
- Order is determined by the primary-language values, so it is the same in every language

In HTML, repeat them with `<template data-cms-list="<collection key>">` ([Editable areas](https://siteon.dev/docs/cms#lists-such-as-news)).

## forms

Form definitions. `fields` is required; `label` is optional.

| Key | Contents |
|---|---|
| `key` | The value you write in the form's `data-sitekit-form="…"` |
| `label` | Name shown in the dashboard and notifications |
| `fields` | Fields. Each `key` matches an input's `name` |
| `success_url` | Where to go after a submission without JavaScript (`application/x-www-form-urlencoded`). Must be an `http(s)` URL within the site's allowed origins |

- **Notification destinations (email, Slack) and the auto-reply text are not set in schema.json.** The owner can change them under "Settings → Notifications → each form" in the dashboard. `forms[].notify` / `forms[].mail_template` are ignored with a warning if present
- For regular forms sent with JavaScript, set the destination with `data-success-url` in the HTML ([Forms](https://siteon.dev/docs/forms#redirecting-to-another-page-after-submission))

## fields

| Key | Type | Contents |
|---|---|---|
| `key` | string | Required. Follows the key rules above |
| `label` | string | Field name in the dashboard |
| `type` | string | One of the types below. Defaults to `text` |
| `required` | boolean | Makes the field required |
| `translate` | boolean | `false` disables translation. If omitted, text types (`text` / `textarea` / `blocks`) are translated |
| `hint` | string | Input help shown in the dashboard |
| `options` | array | Options for `select` (required for `select`) |

## Types

| Type | Use | Values accepted from forms |
|---|---|---|
| `text` | Single line of text | Up to 200 characters |
| `textarea` | Multiple lines of text | Up to 4,000 characters |
| `blocks` | Article body (blocks such as headings, photos, video) | — |
| `date` | Date | A real date as `YYYY-MM-DD` |
| `image` | Photo (absolute URL in the public JSON) | — |
| `select` | Choice | One of `options` |
| `number` | Number | A finite number |
| `email` | Email address (used as the recipient of the form's auto-reply) | Up to 254 characters. Saved in lowercase |
| `tel` | Phone number | Digits, `+`, `-`, spaces, parentheses; up to 32 characters |
| `checkbox` | Checkbox | `true` / `"on"` / `"1"` / `"true"` |
| `tags` | Tags (for collections) | — |

`email` / `tel` / `checkbox` are meant for forms. In groups or collections they are treated as a plain single line of text.

## Options for select

Write options as an array of strings or as objects with `value` / `label` / `color` (you can mix both).

```json
{ "key": "category", "label": "Category", "type": "select", "translate": false,
  "options": [
    { "value": "Announcement", "color": "gray" },
    { "value": "New product", "label": "New arrival", "color": "red" },
    "Other"
  ] }
```

- `value` is a non-empty string; no duplicates
- If you write `label`, it must be a non-empty string. If omitted, `value` is displayed as is
- `color` is one of `red` / `gold` / `gray` / `black` / `green` / `blue`
- The public JSON gains `category_label` and `category_color` (only for options with a color)
- Categories are classification labels, so set `"translate": false`

## tags

- Up to 20 per item, each up to 40 characters
- `/` `#` `?` are not allowed (they appear in tag page URLs)
- Leading and trailing whitespace is trimmed, and duplicates are merged
- `translate` is always `false` even if you set it (tags are the same in every language)

## Validation errors

If schema.json has mistakes, you get a list of what's wrong and nothing is saved. For example:

```text
groups[0].fields[1].key must match /^[a-z][a-z0-9_]*$/ (max 40)
collections[0].sort names a field this collection does not have
forms[0].fields[2] is a select and needs a non-empty options array
forms[0].success_url must be an http(s) URL
tracking.gtm_id must match /^GTM-[A-Z0-9]+$/
```

## Matching schema.json and HTML

| schema.json | HTML |
|---|---|
| `groups[].key` and `fields[].key` | `data-cms="<group>.<field>"` |
| `collections[].key` | `<template data-cms-list="…">` / `data-cms-entry` / `data-cms-empty` |
| A collection's `fields[].key` | `data-cms="…"` inside the template |
| `forms[].key` | `<form data-sitekit-form="…">` |
| A form's `fields[].key` | The input's `name="…"` |

Form inputs whose `name` isn't in schema.json are ignored without an error (and not saved). When you add a field, add it to schema.json too.
