# 直せる箇所の印（CMS）

> data-cms 系の属性で、テキスト・写真・お知らせの一覧・記事ページ・多言語を、お店の人が直せるようにする。

URL: https://sitekit.example/docs/cms

## 考え方

HTML の要素に `data-cms` 系の属性を付けると、その箇所が管理画面から直せるようになります。何を直せるかは [schema.json](https://sitekit.example/docs/schema) で宣言します。

- 要素の中に書いた文言は**既定の文言**です。値が無いとき（未入力、または CMS に届かないとき）は、HTML がそのまま残ります
- 値は文字としてエスケープされて入ります。お店の人の入力で HTML やスクリプトが差し込まれることはありません
- 値の埋め込みは、Cloudflare Pages の中継（配信前）か `data-embed`（表示時）が行います（[中継とホスティング](https://sitekit.example/docs/hosting)）

## 印の一覧

| 属性 | 働き |
|---|---|
| `data-cms="key"` | 要素の中の文字を置き換える |
| `data-cms-html="key"` | 同じく置き換え、改行を `<br>` にする |
| `data-cms-src="key"` | `img` / `video` / `source` の `src` を置き換える |
| `data-cms-attr="属性:key"` | 任意の属性を置き換える（例: `content:title`、`href:url`） |
| `data-cms-blocks="key"` | 記事本文（ブロックの並び）を中に描く |
| `data-cms-hide-empty` | 値が空文字のとき要素ごと消す（`data-cms` / `data-cms-html` だけに効く） |
| `<template data-cms-list="k" data-cms-limit="n">` | 一覧の1件ごとに中身を繰り返す（`n` 件まで） |
| `<template data-cms-list="k" data-cms-filter="f=v">` | 条件に合う件だけ繰り返す |
| `<template data-cms-empty="k">` | 一覧が空のときだけ中身を出す |
| `<template data-cms-entry="k">` | 記事ページで、その記事1件を描く |
| `<template data-cms-each="k">` | 配列（タグなど）の要素ごとに繰り返す |
| `data-cms-fallback="k"` | 一覧 `k` が届いたら要素ごと消す（届かないときの代わりの表示） |

## テキスト

グループ（[schema.json](https://sitekit.example/docs/schema) の `groups`）の値は `<グループ>.<項目>` で指定します。

```html
<dd data-cms="hours.days">月〜土</dd>

<!-- 改行を <br> に。空にすると枠ごと消える -->
<p class="notice" data-cms-html="hours.notice" data-cms-hide-empty>臨時休業のお知らせはありません。</p>
```

## 写真

一覧や記事の写真（`type: "image"` の項目）は、絶対 URL で届きます。`data-cms-src` で `src` に入れます。

```html
<template data-cms-list="news">
  <article>
    <img data-cms-src="thumbnail" src="/img/placeholder.jpg" alt="">
    <h3 data-cms="title">記事タイトル</h3>
  </article>
</template>
```

`href` や `src` などの URL を入れる属性には、`javascript:` / `data:` / `vbscript:` で始まる値は入りません（既定の値が残ります）。`data-cms-attr` で `on…` のイベント属性・`srcdoc`・`formaction` は書き換えられません。

## お知らせなどの一覧

コレクション（schema.json の `collections`）は `<template data-cms-list>` で繰り返します。中の `data-cms` には、その1件の項目名を書きます。

```html
<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">記事タイトル</span></a>
    </li>
  </template>

  <!-- 1件も無いときだけ出る -->
  <template data-cms-empty="news">
    <li>お知らせはまだありません。</li>
  </template>

  <!-- 一覧が届いたら消える。CMS に届かないときはこれが残る -->
  <li data-cms-fallback="news"><span>ホームページを公開しました</span></li>
</ul>
```

- 一覧の順番は schema.json の `sort`（例: `"date desc"`）。無ければ管理画面で並べた順です
- 1つの一覧に載るのは新しい順に最大 100 件です
- 記事へのリンクは `data-cms-attr="href:url"` で張ります。`url` は schema.json には無く、**中継が**記事ごとに `/news/<id>`（言語の接頭辞つき、絶対 URL）を足したものです。`data-embed` では付きません
- `data-cms-limit` は整数だけ（`"3"`）。それ以外は制限なしとして全件出ます

## 記事ごとのページ

記事ごとの URL（`/news/<id>`）は、**中継が** `news/_entry.html` を雛形にして描きます。ファイルを記事の数だけ置く必要はありません。Cloudflare Pages の中継を使うサイトだけの機能です。

```text
public/news/_entry.html     記事ページの雛形
public/news/_404.html       存在しない ID のときのページ（任意）
public/en/news/_entry.html  言語ごとに用意する
```

```html
<template data-cms-entry="news">
  <h1 data-cms="title">記事タイトル</h1>
  <time data-cms="date">2026-01-01</time>
  <div data-cms-blocks="body"></div>
</template>
```

- URL は `/news/<id>` または `/<言語>/news/<id>`。`<id>` は英小文字と数字だけ・32 文字以内
- 公開中の記事に無い ID は 404 になります（`news/_404.html` があればそれを返す）
- パスとコレクションのキーは中継の環境変数 `CMS_NEWS_PATH`（省略時 `news`）

### `<title>` と OGP は `<head>` にも template を置く

`data-cms-entry` の「その記事1件」として読めるのは template の内側だけです。`<head>` に直接 `<title data-cms="title">` と書くと、記事ではなく**サイト共通の値**を読みにいきます。画面には出ないので、SNS に貼ったときに初めて気づきます。

```html
<head>
  <template data-cms-entry="news">
    <title data-cms="title">お知らせ｜店名</title>
    <meta name="description" data-cms-attr="content:lead" content="店名からのお知らせ。">
    <meta property="og:title" data-cms-attr="content:title" content="お知らせ｜店名">
    <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>
  <!-- CMS に届かず template が展開されなかったときに残る題 -->
  <title data-cms-fallback="news">お知らせ｜店名</title>
</head>
```

`data-cms-fallback` を付けた `<title>` は、template が展開されると消えます。これが無いと `<title>` が2つ出ます。記事の `url` は絶対 URL なので、`og:url` と `canonical` にそのまま入れられます。

## 記事本文（ブロック）

`type: "blocks"` の項目は `data-cms-blocks` で描きます。出てくる要素のクラスはすべて `sk-` で始まり、**CSS はサイト側で当てます**（スタイルシートは配りません）。

```css
.sk-h  { font-size:1.05rem; margin:2rem 0 .5rem; }   /* 見出し */
.sk-p  { margin:0 0 1rem; }                          /* 文章 */
.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; }

/* 文字色。クラス名は変えない。色の値はサイトの配色に合わせてよい */
.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; }
```

お店の人は本文の中で次の書き方が使えます。

| 書き方 | 出力 |
|---|---|
| `**太字**` | `<strong>` |
| `[文字](https://…)` | 新しいタブで開くリンク（http/https だけ） |
| `{color:red}文字{/color}` | `<span class="sk-c-red">`（色は red / gold / gray / black / green / blue の6つ） |
| `https://…` をそのまま | 自動でリンク |

`.sk-c-*` を書き忘れると、色の指定をしても見た目が変わりません。色を使うサイトでは最初に6つとも定義しておきます。

## タグ

コレクションの項目を `"type": "tags"` にすると、記事にタグを付けられます（1件20個まで・1タグ40文字まで・`/` `#` `?` は使えない）。タグは翻訳しません。

**記事ごとのタグ** — `data-cms-each` で1つずつ出します。中継がタグを `{tag, url, color}` にしているので、名前とタグページへのリンクが書けます。

```html
<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">タグ</span>
  </a>
</template>
```

**タグの一覧（ナビ）** — 使われているタグが `<コレクション>_tags`（例: `news_tags`）という一覧で届きます。`{tag, count, url, color}` を持ちます。

```html
<template data-cms-list="news_tags">
  <a data-cms-attr="href:url" href="#"><span class="sk-tag" data-cms="tag">タグ</span></a>
</template>
```

**タグページ** — `/news/tag/<タグ>/` は、中継が `news/_tag.html` を雛形にして描きます。`news` の一覧はそのタグで絞られた状態で届きます。表示名は `news.tag` です。存在しないタグでも 404 にはせず、空の一覧（`data-cms-empty` が出る）を返します。

```html
<h1>タグ: <span data-cms="news.tag">タグ名</span></h1>
<ul>
  <template data-cms-list="news">
    <li><a data-cms-attr="href:url" href="./"><span data-cms="title">タイトル</span></a></li>
  </template>
  <template data-cms-empty="news"><li>このタグのお知らせはまだありません。</li></template>
</ul>
```

タグの色は管理画面の「タグの管理」で決めます。色を決めていないタグには `data-color` 属性が付かず、`.sk-tag` の既定の見た目になります。

```css
.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; }
```

`data-cms-filter="tags=秋"` のように書くと、タグページ以外でも絞り込めます（配列の項目は「含む」、1つの値は「等しい」で判定）。

## 種別のバッジ

`select` の選択肢には色を付けられます（[schema.json](https://sitekit.example/docs/schema#select-の選択肢)）。公開 JSON には保存値に加えて `<項目>_label`（表示する文字）と `<項目>_color`（色名。色の無い選択肢では出ない）が載ります。

```html
<span class="sk-badge" data-cms="category_label" data-cms-attr="data-color:category_color"></span>
```

`.sk-badge[data-color=…]` の CSS は `.sk-tag` と同じ形で6色を書きます。

## 多言語

1. schema.json の `locales` に言語を並べます（例: `["ja", "en"]`）。先頭が主言語で、お店の人はこの言語で入力します
2. 文章の項目（`text` / `textarea` / `blocks`）は、保存のあと AI が自動で翻訳します（AI 翻訳のあるプランのとき）。訳さない項目には `"translate": false` を付けます（時刻・日付・種別など）
3. 翻訳が無い・空の項目は主言語の値が出ます
4. 言語ごとのページを用意し、中継の `CMS_LOCALES` に `言語:接頭辞` を書きます（例: `ja:/,en:/en`）。言語は URL だけで決まり、`Accept-Language` は見ません

`data-embed` を使う場合は、`<html lang>` か script の `data-locale` の言語で取得します。

AI 翻訳の有無と回数の上限はプランで決まります（[プランと上限](https://sitekit.example/docs/plans)）。

## 書くときの決まり

守らなくてもエラーは出ず、表示が崩れるか、何も起きないだけです。うまくいかないときはここから確かめます。

- **印を付けた要素の中に、同じタグ名の要素を入れない。** `<div data-cms="x"><div>…</div></div>` は壊れます。`<span>` など別のタグを使います
- **1つの要素に印は1つ。** 2つ付けると後のものが勝ちます
- **`data-cms-attr` は1要素に1つだけ。** `href:url data-color:color` のように2つ書くと、どちらも書かれません。要素を分けます
- **タグ名と属性名は小文字で書く。** `<DIV DATA-CMS="k">` は無視されます
- **`<script>` と `<style>` の中に印を書かない。** 中身が書き換えられてしまいます（HTML コメントの中は安全です）
- **`<template data-cms-list>` の中に、もう1つ `data-cms-list` を入れない。** 1件の中で繰り返すときは `data-cms-each` を使います
- **`data-cms-filter` で0件になっても `data-cms-empty` は出ません。** `data-cms-empty` が見るのは一覧そのものが空かどうかです
- **静的な文言は、それだけ読んで意味が通る内容にする。** CMS に届かないときはそれが公開されます
