Data Modeling — Fields / Lexical / Relationships / Versions / Localization
この章のゴール
- PayloadCMS の Field 体系 (Data / Presentational / Virtual の 3 区分) を把握する
- リレーション系 (
relationship/upload) と構造系 (array/group/tabs/blocks) の使い分けを掴む - Lexical Rich Text のデフォルト機能と feature の足し方を理解する
- Localization (多言語化) と Versions & Drafts の必要最小限の設定を書ける
Field が住む場所 (おさらい)
02 Core Concepts で見たとおり、PayloadCMS のデータモデルは 3 階層です:
Payload Config (payload.config.ts)
├─ Collections (繰り返し / posts, users, media …) → fields[] ← この章
└─ Globals (シングルトン / header, footer …) → fields[] ← この章
公式 doc は Globals を「many ways similar to Collections, except that they correspond to only a single Document」と定義しています (Globals)。繰り返しかシングルトンかという入れ物は違っても、中身を定義する fields 配列の語彙は共通で、この章で扱うのはその fields[] の世界です。
加えて group / array / blocks / tabs などのフィールド型は自身がさらに fields を持つため、ツリーは再帰的に深くなり得ます (Fields Overview)。詳細は本章の各セクションで扱います。
Field の 3 区分
公式 doc (Fields Overview) は Field を 3 つに分けて説明しています。
| 区分 | 役割 | 含まれる type |
|---|---|---|
| Data Fields | DB に値を保存する | text / textarea / email / number / date / select / checkbox / radio / array / blocks / group / tabs (named) / relationship / upload / richText / code / json / point |
| Presentational Fields | UI 用、DB には保存しない | collapsible / row / tabs (unnamed) / group (unnamed) / ui |
| Virtual Fields | 計算で導出する値 | join / 任意フィールドの virtual: true |
「Presentational Fields は DB に値を保存せず、Admin Panel で他のフィールドを整理・表示するために使う」と公式 doc も明記しています。Data フィールドは name を必須にしますが、Presentational は name 不要、という違いで区別できます。
全 Field 共通プロパティ
| プロパティ | 意味 |
|---|---|
name | DB のキー (Data Fields は必須) |
type | フィールドの挙動を決定する (必須) |
label | Admin UI 表示名 |
required | バリデーション (空を禁止) |
validate | カスタムバリデーション関数 |
hooks | beforeChange / afterRead など Field レベルのライフサイクル |
access | Field レベルのアクセス制御 |
admin | UI カスタマイズ (description / position / readOnly 等) |
defaultValue | 初期値 |
localized | true で多言語化 (後述) |
{
name: 'myField',
type: 'text',
required: true,
admin: { description: 'A simple text field' },
}
プリミティブ Field カタログ
最小例とユースケースを並べます。
{ name: 'title', type: 'text' } // 1 行テキスト
{ name: 'body', type: 'textarea' } // 複数行テキスト
{ name: 'email', type: 'email' } // メール (バリデーション込)
{ name: 'price', type: 'number', min: 0 } // 数値
{ name: 'publishedAt', type: 'date' } // 日付
{ name: 'status', type: 'select', options: ['draft', 'published'] } // 列挙
{ name: 'isFeatured', type: 'checkbox' } // 真偽値
{ name: 'tier', type: 'radio', options: ['free', 'pro'] }
{ name: 'config', type: 'json' } // JSON 任意構造
{ name: 'snippet', type: 'code', admin: { language: 'ts' } }
{ name: 'location', type: 'point' } // 地理座標 [lng, lat]
リレーション系 Field
relationship
別の Collection を参照します。1:1、1:N、polymorphic (複数 Collection のうちどれか) も書けます。
{
name: 'author',
type: 'relationship',
relationTo: 'users', // 単一 Collection
required: true,
}
{
name: 'tags',
type: 'relationship',
relationTo: 'tags',
hasMany: true, // 1:N
}
{
name: 'related',
type: 'relationship',
relationTo: ['posts', 'pages'], // polymorphic
}
Admin UI には関連 Collection を検索しながら選ぶピッカーが自動で現れ、API レスポンスは depth パラメータでネスト展開できます (REST: ?depth=2、Local: payload.find({ ..., depth: 2 }))。
polymorphic relationship の詳しい挙動
relationTo を 配列 (['users', 'organizations']) で渡すと polymorphic relationship になります。これは「型が違うものを 1 つの口で受ける」関係です。たとえば「お知らせの送信先」が User でも Organization でも構わない、あるいは「コメントの対象」が Post でも Page でもよい、というモデルを 1 つの field で表現できます。
DB 保存形式の違い
ここがポイントで、単一 Collection を指す relationship と polymorphic とでは、DB に保存される値の形が変わります。
// relationTo: 'users' (単一) のとき、値は ID 文字列 (or 数値)
{
"author": "6031ac9e1289176380734024"
}
// relationTo: ['users', 'organizations'] (polymorphic) のとき、
// 値は「どの Collection の ID か」が分かる object 形式
{
"owners": [
{ "relationTo": "users", "value": "6031ac9e1289176380734024" },
{ "relationTo": "organizations", "value": "602c3c327b811235943ee12b" }
]
}
polymorphic では 「ID 単体」では足りない ことに注目してください。「6031ac9e... という ID」だけでは、それが users のものか organizations のものか判別できないため、relationTo を併せて保存する必要があります。
公式の完全例
公式 doc が示す polymorphic + hasMany + required を全部組み合わせた例:
{
name: 'owners',
type: 'relationship',
relationTo: ['users', 'organizations'],
hasMany: true,
required: true,
}
フロントでの分岐パターン
取得側 (Server Component / API consumer) は relationTo の値で 分岐して描画します。depth: 0 だと value は ID のまま、depth: 1 以上で関連ドキュメント本体が展開される、という挙動は Queries — Depth に明示されています (公式の単一 relationship の例で示されている挙動を、polymorphic の { relationTo, value } 構造に当てはめると以下のような分岐になります):
// depth: 1 以上で取得した場合、value にドキュメント本体が入る
post.owners?.map((owner) => {
if (typeof owner.value === 'string') return null // 何らかの理由で展開されていない
if (owner.relationTo === 'users') {
return <UserCard key={owner.value.id} user={owner.value} />
}
if (owner.relationTo === 'organizations') {
return <OrgCard key={owner.value.id} org={owner.value} />
}
return null
})
@/payload-types から自動生成される型では value が User | Organization | string のような union になり、owner.relationTo === 'users' の判定で TypeScript の型 narrowing が効くため、上記の分岐は型安全に書けます。
いつ使う / 避ける
- 使う: 「同じスロットに 異なる種類のドキュメントが入りうる」とき。例: 通知の宛先 (User / Organization)、関連リンク (Post / Page)、コメントの対象 (Article / Product / Event)
- 避ける: 1 種類しか想定しないとき。単一
relationTo: 'users'の方が 値の形がシンプル (stringで済む) で、フロント側のtypeofチェックや switch 分岐が要らないぶん、コードが軽くなります
迷ったら 「将来、複数 Collection を受ける可能性がゼロか?」 を自問し、ゼロなら単一、可能性があるなら polymorphic、で選びます。
upload
Upload-enabled Collection (例: Media) への参照です。typically 画像・PDF を relationship 的に紐付けるための糖衣構文。
{
name: 'thumbnail',
type: 'upload',
relationTo: 'media',
}
Media Collection 側で upload: { staticDir, imageSizes } を設定すると、imageSizes 設定どおりに自動でリサイズ画像が生成されます (詳細は 07 Admin / Media / Deploy)。
構造系 Field
| Field | 用途 |
|---|---|
group | フィールドをまとめて 1 つのオブジェクトに格納 ({ name: 'seo', type: 'group', fields: [...] }) |
array | 同一スキーマのサブドキュメントを N 個並べる |
tabs | Admin UI 上でフィールドをタブ分割 |
blocks | 異なるスキーマのサブドキュメントを N 個並べる (★ページビルダーの心臓) |
blocks — ページビルダーの心臓
公式 doc (Blocks) によれば、Blocks Field は「複数の異なるスキーマを持つオブジェクトの配列」を保存します。「ページビルダーで Quote、CallToAction、Slider、Gallery を混在させる」のが典型用途です。
import type { Block } from 'payload'
const Hero: Block = {
slug: 'Hero',
fields: [
{ name: 'headline', type: 'text' },
{ name: 'image', type: 'upload', relationTo: 'media' },
],
}
const CallToAction: Block = {
slug: 'CallToAction',
fields: [
{ name: 'label', type: 'text' },
{ name: 'href', type: 'text' },
],
}
// Collection の fields の中で使う
{
name: 'layout',
type: 'blocks',
blocks: [Hero, CallToAction],
}
Admin UI には「+ Add Block」ボタンが出て、編集者は Hero と CallToAction を順番に積み上げてページを組めます。コードベース側は「どのブロックがどう描画されるか」を 1 対 1 でフロントに用意するだけで、CMS の入稿フローと完全に分離できます。
Array との違い (公式の説明): array は「全項目が同一スキーマ」、blocks は「異なるコンテンツタイプを混在させ任意順序で並べる」。
Lexical Rich Text
立ち位置
3.x で Lexical (Meta が開発) が デフォルトかつ stable な Rich Text Editor になりました。旧 Slate adapter は v4.0 で削除予定 と公式 doc (Rich Text Overview) に明記されており、新規プロジェクトは Lexical を選ぶのが既定路線です。Slate からの移行ガイドも公式で提供されています。
デフォルト features
Lexical エディタは feature の集合で構成されます。公式 doc (Official Features) が「Included by default: Yes」と明示している主な feature を整理すると:
- HeadingFeature — 見出し H1〜H6
- UnorderedListFeature / OrderedListFeature / ChecklistFeature — 箇条書き / 番号付き / チェックリスト
- BlockquoteFeature — 引用ブロック
- LinkFeature — ハイパーリンク
- UploadFeature — エディタ内に画像 / PDF 等を埋め込み
- RelationshipFeature — エディタ内に Collection ドキュメントを埋め込み
- HorizontalRuleFeature — 区切り線
- ほか Bold / Italic / Underline などのインライン書式、Align / Indent
一方 BlocksFeature (エディタ内に Blocks Field と同じ Block を埋め込む、コンテンツの中にコンテンツ) はデフォルトに含まれません ("Included by default: No")。次の「features の追加」のように明示的に加えます。
features の追加
editor プロパティで feature を組み立てます。
import { lexicalEditor, BlocksFeature, LinkFeature } from '@payloadcms/richtext-lexical'
// Collection field 内で
{
name: 'body',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({ blocks: [Hero, CallToAction] }),
LinkFeature({ fields: [{ name: 'utm', type: 'text' }] }),
],
}),
}
defaultFeatures を spread して追加・差し替えする形が公式の推奨パターンです。
フロント描画
Lexical の出力は JSON 構造で保存され、フロント側は公式提供の RichText コンポーネントで React 要素に変換します (具体的な実装は 06 章 6.10 節 で扱います)。
Localization (多言語化)
Config レベル
// payload.config.ts
export default buildConfig({
// ...
localization: {
locales: [
{ label: 'English', code: 'en' },
{ label: 'Arabic', code: 'ar', rtl: true },
{ label: '日本語', code: 'ja' },
],
defaultLocale: 'en',
fallback: true,
},
})
locales は文字列配列 (['en', 'es', 'de']) でも OK、fallback: true (デフォルト) でロケール未入力時に defaultLocale の値を返します。
Field レベル
localized: true を付けたフィールドは ロケールごとのオブジェクトとして DB に保存されます。
{ name: 'title', type: 'text', localized: true }
Admin UI には言語切替セレクタが現れ、編集者はロケール別に値を入力できます。
API でのロケール指定
| API | 書き方 |
|---|---|
| REST | GET /api/posts?locale=ja&fallback-locale=none |
| GraphQL | Posts(locale: ja, fallbackLocale: none) |
| Local | payload.find({ collection: 'posts', locale: 'ja', fallbackLocale: false }) |
Versions & Drafts
Versions の有効化
Collection に versions: true を付けるとドキュメントの履歴が保存されます。
{
slug: 'posts',
versions: {
drafts: true,
maxPerDoc: 100, // デフォルト 100、超えると古いものから消える
},
fields: [/* ... */],
}
drafts: true を有効にすると、各ドキュメントに _status: 'draft' | 'published' フィールドが追加され、公開版より新しい draft を持てるようになります (公式 doc Versions Overview)。Access Control で「未公開 draft の閲覧は author 本人と admin だけ」のような制御ができます。
Autosave
versions: {
drafts: {
autosave: true,
},
}
Autosave を有効にすると、Admin UI で編集中に自動で 新しい draft 版が保存されます。既存の published 版には影響せず、編集者が「Publish」を押すまで公開には反映されません。
Live Preview / Draft Preview との関係
- Live Preview: Admin UI で書きながら同じ画面でフロントが即更新される (iframe + postMessage)
- Draft Preview:
?preview=...のような限定 URL で未公開 draft をフロントに表示する
Live Preview は Versions と独立した機能、Draft Preview は Versions の drafts: true が前提です。両方とも 06 Next.js Integration で詳しく扱います。
実装例 — Blocks の選択肢を他フィールドで絞る (filterOptions)
ページビルダーで「特定の条件下でのみ選択できる Block」を表現したいことがあります。たとえば「公開モードが drafts のときは Hero ブロックを出さない」「テンプレートを Article に切り替えたら使える Block セットを差し替える」など。
Blocks Field の filterOptions を使うと、同じドキュメント内の他フィールドの値に応じて表示する Block を動的に絞れます (Blocks Field doc)。
{
name: 'layout',
type: 'blocks',
blocks: [Hero, RichText, CallToAction],
filterOptions: ({ siblingData }) => {
return siblingData?.enabledBlocks?.length
? [siblingData.enabledBlocks]
: true
},
}
- 返り値
true→ 定義した全 Block を許可 - 返り値が block slugs の配列 → その slug の Block だけ許可
シナリオ例: enabledBlocks という select フィールドを同じ Collection に置き、編集者がそこで「使う Block セット」を選ぶと、Blocks Field の Admin UI が動的にフィルタされる、という流れです。
この章のまとめ
- Field は Data (DB に保存) / Presentational (UI 用) / Virtual (導出) の 3 区分
- リレーション系は
relationship(1:1/1:N/polymorphic) とupload(Media 参照糖衣構文) - 構造系の主役は
blocks— 異なるスキーマを順不同で並べるページビルダーの心臓 - Rich Text は Lexical が標準・stable、Slate は v4.0 で削除予定
- Localization は config の
localization+ field のlocalized: trueの 2 階建て - Versions の
drafts: trueで公開/下書きが分離、autosave: trueで編集中の自動保存 - Blocks Field は
filterOptionsで他フィールド値に応じて選択肢を動的に制御できる
次に読む
次章 04 Security & Lifecycle — Auth / Access Control / Hooks では、認証・認可・Hooks のライフサイクルを扱います。データを定義したあと「誰が読めて、誰が書けて、書き換えた瞬間に何が起きるか」のレイヤです。