はじめに
ZeroCode.jsは、フレームワーク非依存のWeb ComponentsベースのCMSエディターライブラリです。Vue.jsで実装されており、カスタムHTMLテンプレート構文を使用して動的なコンテンツ管理を提供します。
プロダクトの位置づけ・設計方針の詳細はZeroCode.js とはを参照してください。
ZeroCode.js とは
ZeroCode.js は、既存 Web サービスの認証・永続化・バックエンドデータをホスト側で担いながら、フロントエンドに後付けできる 埋め込み型 CMS ライブラリ です。
目的
既存 Web サービスの運営者が、開発者に依存せずページを更新・運用できる環境を提供します。
そのために、既存サービスの 認証・データ・HTML 構造の方針 を維持したまま導入できる CMS を、フロントエンドライブラリとして実現します。
解決したい課題
一般的な CMS やページビルダーは、CMS 独自の仕組みを持つことが多く、既存システムとの統合コストが発生します。
システム面
- CMS 独自の認証を持つ
- CMS 独自のデータ管理を持つ
- 既存システムとの連携・同期が必要になる
編集面
- 編集画面と公開画面に差異がある
- 実際の表示を確認しながら編集しづらい
- CMS 都合の HTML や DOM 構造が生成される
運用面
- CMS とアプリケーションの二重管理が発生する
- ページ修正のたびに開発工数が発生しやすい
- 開発者と編集者の責務分離が曖昧になりやすい
設計方針
ZeroCode.js は、次の方針に基づいて設計されています。
システム
| 方針 | 内容 |
|---|---|
| 埋め込み型 | 既存 Web サービスへ後付けで導入する |
| バックエンド非提供 | 永続化・認証・認可はホストアプリが担当する |
| 既存認証の利用 | CMS 独自のログイン機能は持たない |
| イベント駆動の保存 |
保存ボタンで save-request を発火し、ホストが
getData() で取得して永続化する
|
表示
| 方針 | 内容 |
|---|---|
| WYSIWYG に近い編集 | 公開画面とほぼ同じ見た目・構造で編集する |
| 編集用メタデータの分離 | 編集時のみ識別用の属性等を付与し、公開 HTML には載せない |
| 差異の最小化 | 編集画面と公開画面の DOM 構造の差を小さく保つ |
編集モードでは、主に data-zcode-id・data-zcode-path・data-zcode-part
などの
data-zcode-* 属性 が付与されます。公開レンダリング(SSR
含む)では
enableEditorAttributes: false(デフォルト)により、これらは出力されません。
出力
| 方針 | 内容 |
|---|---|
| 開発者定義の HTML | 公開 HTML は開発者が定義したパーツテンプレートから生成する |
| CMS 都合の DOM を挟まない | WordPress ブロックやページビルダー的な CMS 専用ラッパー markup は生成しない |
| 記法の展開 |
{$field} や
z-if などのテンプレート記法はレンダリング時に展開・除去される
|
補足: ZeroCode.js は「HTML を生成しない」のではなく、「CMS 内部都合の HTML を生成しない」という意味です。公開ページの HTML
構造の正は、開発者が定義したパーツテンプレート(part.body)です。
編集者向け(<zcode-cms>)
- パーツの追加・削除・並べ替え
- パーツ設定(フィールド)の編集
- テキスト・画像の編集
- パーツの組み合わせによるレイアウト構築
開発者向け(<zcode-editor> / <zcode-studio>)
- 独自記法でパーツテンプレートを定義する
- 編集可能な領域・設定項目(フィールド)を定義する
- パーツ・画像・CSS を管理画面で登録・管理する
- スロットでパーツの入れ子構成を定義する
定義
ZeroCode.js は、既存 Web サービスの認証・永続化・バックエンドデータをホスト側で担いながら、フロントエンドに後付けできる埋め込み型 CMS ライブラリです。
開発者が定義した パーツテンプレート を組み合わせ、運営者はノーコードでページを構築・編集できます。
公開 HTML はパーツテンプレートから生成され、CMS 都合の DOM は挟みません。編集時のみ
data-zcode-* 等の識別情報を付与し、公開レンダリングでは除去してクリーンな
HTML を出力します。
データの扱い
ZeroCode.js には2種類のデータがあります。
| 種類 | 説明 | 永続化 |
|---|---|---|
| ページ編集データ |
page・parts・images・css など
ZeroCode 形式の JSON
|
ホストが save-request 受信後に永続化 |
| バックエンドデータ | 既存 API / DB から渡す参照用データ({@user.name} 等) |
ZeroCode は参照のみ。更新はホスト側 |
「データは既存システムを利用する」とは、認証・永続化・動的参照をホストに委ねる という意味です。ページ構成データそのものは ZeroCode 形式で管理されます。
コンポーネントと役割
| コンポーネント | 想定ユーザー | 主な用途 |
|---|---|---|
<zcode-cms> |
運営者・編集者 | ページの編集・追加・削除・並べ替え |
<zcode-editor> |
開発者 | パーツ管理・画像管理・データビューアを含むフル機能 |
<zcode-studio> |
制作会社など信頼できるユーザー | zcode-editor 同等 UI。専用パーツ・専用 CSS・専用画像の編集に限定 |
ZeroCode.js が提供しないもの
次は 意図的に提供しません。ホストアプリ側で実装してください。
- ユーザー認証・認可
- データベース・API(永続化層)
- 自動保存(保存は保存ボタンと
save-requestのみ) - 既存ページ DOM のその場直接編集(パーツテンプレートベースの構成)
技術的特徴(概要)
- フレームワーク非依存 — Web Components として React / Vue / 素の HTML などに埋め込める
- カスタムテンプレート記法 — フィールド・条件分岐・ループ・スロット等
-
SSR 対応 —
zerocodejs/ssrのrenderToHtml()で公開 HTML をサーバー生成可能 - Beta — API・データ形式は変更される可能性があります
編集体験改善ロードマップ
社内ユーザーテスト(2026年)を踏まえ、ZeroCode.js 本体で対応した項目を Issue 分割しています。埋め込み先プロダクト 固有の機能(公開画面・下書きフロー・スタッフ表示等)は含みません。
詳細・実装方針・受け入れ条件は
TODO.md – Phase 5
を参照してください。GitHub Issue 作成時は
ID(ZC-n)をタイトルに含めると追いやすいです。
| 順 | ID | 内容 | 状態 |
|---|---|---|---|
| 1 | ZC-1 | 編集プレビューで z-empty によりパーツ内容が消える |
完了 |
| 2 | ZC-2 | リッチテキスト空値の正規化 | 完了 |
| 3 | ZC-3 | 編集パネル内ドラッグでパネルが閉じる(useClickHandlers) |
完了 |
| 4 | ZC-4 | パーツ間の追加ボタン(追加モード時・先頭・入れ子対応) | 完了 |
| 5 | ZC-5 | 並べ替え D&D(構造リスト・スロット含む) | 完了 |
クイックスタート
ZeroCode.jsをすぐに使い始めるための基本的な手順です。
インストール
npm install zerocodejs
ZeroCode.jsは内部でVue 3を使用しています。npm 7以降では、peer
dependenciesが自動的にインストールされるため、npm install zerocodejsだけでVueも一緒にインストールされます。
注意:
npm 6以前を使用している場合は、明示的にnpm install zerocodejs vueを実行してください。
基本的な使用例
CDNを使用した最も簡単な例(HTMLファイルをブラウザで開くだけで動作):
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/zerocodejs/dist/zerocodejs.css">
</head>
<body>
<zcode-editor></zcode-editor>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/zerocodejs/dist/zerocode.umd.js"></script>
</body>
</html>
npmを使用した例:
<zcode-editor></zcode-editor>
<script type="module">
import 'zerocodejs';
</script>
zcode-editorはエンジニア・デザイナー向けの管理画面で、パーツ管理・画像管理・データビューアが利用できます。はじめての方におすすめです。
zcode-cmsはエンドユーザー向けの管理画面で、編集・追加・削除・並べ替えのみが利用できます。
複数インスタンス対応
ZeroCode.jsは、同じページに複数のzcode-cmsやzcode-editorインスタンスを配置することができます。各インスタンスは独立したデータを管理し、互いに影響を与えません。
<!-- インスタンス1 -->
<zcode-cms id="cms-1">
<link slot="css" rel="stylesheet" href="/css/common.css" />
</zcode-cms>
<!-- インスタンス2 -->
<zcode-cms id="cms-2">
<link slot="css" rel="stylesheet" href="/css/common.css" />
</zcode-cms>
<script type="module">
import 'zerocodejs';
// 各インスタンスに独立したデータを設定
const cms1 = document.getElementById('cms-1');
cms1.setAttribute('page', JSON.stringify([...]));
cms1.setAttribute('parts-common', JSON.stringify([...]));
const cms2 = document.getElementById('cms-2');
cms2.setAttribute('page', JSON.stringify([...]));
cms2.setAttribute('parts-common', JSON.stringify([...]));
</script>
動作:
-
独立したデータ管理:
各インスタンスは
id属性で識別され、独立したデータを管理します - セッションストレージの分離: セッションストレージはインスタンスIDごとに分離され、データが混在することはありません
- イベントリスナーの分離: 各インスタンスのイベントリスナーは独立して動作します
-
IDの自動生成:
id属性が指定されていない場合、自動的に一意のIDが生成されます
注意:
複数インスタンスを使用する場合は、各インスタンスに一意のid属性を指定することを推奨します。これにより、データの管理が明確になり、デバッグも容易になります。
CDN経由で使用する場合
CDN経由で使用する場合は、Vueを先に読み込む必要があります。
<!-- Vueを先に読み込む -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- ZeroCode.jsを読み込む -->
<script src="https://unpkg.com/zerocodejs/dist/zerocode.umd.js"></script>
<link rel="stylesheet" href="https://unpkg.com/zerocodejs/dist/style.css">
<zcode-cms id="cms">
<link slot="css" rel="stylesheet" href="/css/common.css" />
</zcode-cms>
<script>
const cms = document.getElementById('cms');
cms.setAttribute('page', JSON.stringify([]));
cms.setAttribute('parts-common', JSON.stringify([]));
cms.setAttribute('parts-individual', JSON.stringify([]));
cms.setAttribute('parts-special', JSON.stringify([]));
cms.setAttribute('images-common', JSON.stringify([]));
cms.setAttribute('images-individual', JSON.stringify([]));
cms.setAttribute('images-special', JSON.stringify([]));
</script>
Shadow DOM
ZeroCode.jsは、デフォルトでShadow DOMを使用してCSS/JSを完全に分離します。これにより、呼び出し側のCSSやJavaScriptとの競合を防ぎ、副作用のない安全な統合を実現します。
Shadow DOMの利点
- CSS分離: ZeroCode.jsのスタイルが呼び出し側のCSSに影響を与えません
- JavaScript分離: Shadow DOM内のスクリプトが外部のJavaScriptと競合しません
- セキュリティ: 外部からのスタイルやスクリプトの干渉を防ぎます
ホストスタイルのリセット(:host)
Shadow DOM を使うとき、カスタム要素(zcode-cms / zcode-editor /
zcode-studio)にはホストページ側からフォントや文字色などが継承されることがあります。ZeroCode.js
がシャドウ内に注入するスタイルシートの先頭では、:host に対して
all: initial を指定し、この継承を一度リセットしています。
続けて明示しているのはレイアウト上ほぼ必須な display: block と、内部の
.zcode-* と整合させるための box-sizing: border-box
のみです。サイト固有のタイポグラフィや配色をライブラリ側で決めることはなく、ページに合わせた見た目が必要な場合は従来どおり
slot="css" でシャドウに渡すスタイルシートなどで追加してください。
注意: Light DOM(use-shadow-dom="false")では
:host は効きません。ホストの CSS
がそのまま影響するため、競合対策は別途検討してください。
Shadow DOMの無効化
use-shadow-dom="false"属性を指定することで、Shadow DOMを無効化できます(Light
DOMモード)。
<zcode-cms id="cms" use-shadow-dom="false">
<link slot="css" rel="stylesheet" href="/css/common.css" />
</zcode-cms>
注意: Shadow DOMを無効化すると、CSSやJavaScriptの競合が発生する可能性があります。通常はShadow DOMを使用することを推奨します。
Shadow DOM内でのjQuery使用
ZeroCode.jsは、Shadow DOM内でjQueryを使用する場合に自動的に拡張を行います。これにより、Shadow DOM内の要素に対してjQueryのセレクターやイベントハンドラーが正常に動作します。
テンプレート記法
ZeroCode.jsでは、カスタムHTMLテンプレート構文を使用して動的なコンテンツを定義します。
フィールド記法
テキストフィールド
{$fieldName:defaultValue}
単一行のテキスト入力フィールドとして表示されます。
<h1>{$title:タイトル}</h1>
テキストエリアフィールド
{$fieldName:defaultValue:textarea}
複数行のテキスト入力フィールドとして表示されます。
<p>{$description:説明文:textarea}</p>
リッチテキストフィールド
{$fieldName:defaultValue:rich}
リッチテキストエディター(TipTap)として表示されます。HTMLタグを含むテキストを編集できます。
<div>{$content:本文:rich}</div>
画像フィールド
{$fieldName:defaultValue:image}
画像選択フィールドとして表示されます。画像管理から画像を選択できます。
<img src="{$image:default.jpg:image}" alt="画像" />
グループ化されたフィールド
フィールド名にドット(.)を使用して、フィールドをグループ化できます。
{$fieldName.groupName:defaultValue}
例:
<div>
<h2>{$hero.title:ヒーロータイトル}</h2>
<p>{$hero.description:ヒーロー説明}</p>
</div>
グループ化されたフィールドは、編集パネルでグループとして表示され、整理された編集が可能です。
オプショナルフィールド(空入力制御)
フィールド名の後に?を追加することで、オプショナルフィールドとして定義できます。ユーザーが何も入力しなかった場合、フィールドの値はundefinedになります。
{$fieldName?:defaultValue}
{$fieldName?:defaultValue:rich}
{$fieldName?:defaultValue:image}
{$fieldName.groupName?:defaultValue}
例:
<div class="section">
<div class="section__title">{$title:タイトル(必須)}</div>
<div class="section__subtitle">{$subtitle?:サブタイトル(オプション)}</div>
<img src="{$optional_image?:default.jpg:image}" alt="{$optional_alt?:画像の説明}" />
</div>
動作:
-
ユーザーが何も入力しなかった場合、フィールドの値は
undefinedになります(デフォルト値は使用されません) - 編集パネルでは、オプショナルフィールドは空欄として表示されます
-
属性値がオプショナルフィールドのみで構成されていて、値が
undefined(または空)の場合、その属性自体がHTMLから削除されます -
基本動作では、オプショナルフィールドが空の場合でも親要素(タグ)は残ります(例:
<p></p>)。親要素ごと削除したい場合は、z-empty属性を使用してください(詳細は「条件分岐」セクションの「z-empty 属性」を参照)
例:
<!-- テンプレート -->
<img src="{$optional_image?:default.jpg:image}" alt="{$optional_alt?:画像の説明}" />
<!-- optional_imageとoptional_altが両方undefinedの場合 -->
<img />
<!-- optional_imageに値がある場合 -->
<img src="image.jpg" alt="画像の説明" />
親要素を削除したい場合:
オプショナルフィールドが空の場合、基本動作では親要素(タグ)が残ります(例:
<p></p>)。親要素ごと削除したい場合は、z-empty属性を使用してください。
<!-- 基本動作:空タグが残る -->
<p>{$subtitle?:サブタイトル}</p>
<!-- subtitleがundefinedの場合、<p></p>が残る -->
<!-- z-emptyを使用:親要素を削除 -->
<div z-empty="$subtitle">
<p>{$subtitle?:サブタイトル}</p>
</div>
<!-- subtitleがundefinedの場合、<div>要素ごと削除される -->
注意: 属性値全体がオプショナルフィールドのみで構成されている場合のみ属性が削除されます。他のテキストが含まれている場合は削除されません。
補足:
z-empty属性の詳細については、「条件分岐」セクションの「z-empty
属性」を参照してください。
バリデーション記法
フィールドにバリデーションルールを追加できます。フロントエンドでの同期バリデーションと、バックエンドでの非同期バリデーションの両方をサポートしています。
{$fieldName:defaultValue:required}
{$fieldName:defaultValue:max=100}
{$fieldName:defaultValue:required:max=50}
{$fieldName:defaultValue:readonly}
{$fieldName:defaultValue:disabled}
例:
<div class="form">
<input type="text" name="title" value="{$title:タイトル:required:max=100}" />
<input type="email" name="email" value="{$email:メールアドレス:required}" />
<textarea name="description">{$description:説明:max=500}</textarea>
<input type="text" name="readonly_field" value="{$readonly_field:読み取り専用:readonly}" />
<input type="text" name="disabled_field" value="{$disabled_field:無効化:disabled}" />
</div>
バリデーションルール:
-
:required- 必須フィールド。空の場合はエラーメッセージが表示されます。 -
:max=N- 最大文字数制限(Nは数値)。指定した文字数を超えるとエラーメッセージが表示されます。 -
:readonly- 読み取り専用フィールド。編集できませんが、値は送信されます。 :disabled- 無効化フィールド。編集できず、値も送信されません。
動作:
-
フロントエンドバリデーション(同期):
requiredとmaxは、ユーザーが入力中にリアルタイムでチェックされます。エラーがある場合は、フィールドの下にエラーメッセージが表示されます。 -
バックエンドバリデーション(非同期):
保存ボタンをクリックすると、
save-requestイベントが発火します。バックエンドでバリデーションを行い、エラーがある場合はsave-resultイベントでエラーを返してください。詳細は「保存リクエスト」セクションを参照してください。 - バリデーションエラーは、編集パネルが開いている場合は該当フィールドに表示され、編集パネルが閉じている場合は画面上部にエラーバナーが表示されます。
注意:
複雑なバリデーション(メール形式チェック、重複チェックなど)は、バックエンドで実施することを推奨します。フロントエンドでは、requiredとmaxのみをサポートしています。
バックエンドデータの参照
バックエンドから渡されたデータをテンプレート内で参照できます。動的URLや共通で使用するデータ(店舗ID、ユーザー情報など)を表示する際に便利です。
{@fieldName}
{@items[0].name}
{@items.length}
/shop/{shop_id}/products
例:
<div class="shop-header">
<h1>{@title}</h1>
<a href="{@url}">店舗詳細</a>
<p>アイテム数: {@items.length}</p>
<p>最初のアイテム: {@items[0].name}</p>
<a href="/shop/{shop_id}/products">商品一覧</a>
</div>
動作:
- 基本的なデータ参照:
{@fieldName}でバックエンドデータを参照 - ネストされたオブジェクト:
{@user.name}のようにドット記法で参照 - 配列の参照:
{@items[0]}で配列の要素を参照 - 配列のlength:
{@items.length}で配列の長さを取得 - URL内プレースホルダー:
/shop/{shop_id}/のようにURL内に直接記述可能
使用例:
<!-- Web Componentにバックエンドデータを渡す -->
<zcode-cms
backend-data='{"title":"A店舗","url":"/shop/123/","shop_id":"123","items":[{"name":"商品1"}]}'
>
<!-- テンプレート内で {@title}, {@url}, {@items[0].name} などが使用可能 -->
</zcode-cms>
注意: バックエンドデータは信頼できるソースからのみ使用してください。存在しないパスを参照した場合は空文字列が返されます。
z-for ループ記法
バックエンドデータの配列をループ表示するには、z-for属性を使用します。現在はバックエンドデータの配列のみをサポートしています。
<div z-for="item in {@items}">
<!-- ループ内のコンテンツ -->
</div>
構文:
z-for="変数名 in {@配列パス}"
例:
<!-- 店舗一覧をループ表示 -->
<div class="shop-list">
<div z-for="shop in {@shops}" class="shop-item">
<h2>{shop.name}</h2>
<p>{shop.description}</p>
<a href="{shop.url}">詳細を見る</a>
</div>
</div>
<!-- 商品一覧をループ表示 -->
<div class="product-list">
<div z-for="product in {@products}" class="product-item">
<h3>{product.name}</h3>
<p>価格: {product.price}円</p>
<p>カテゴリ: {product.category}</p>
</div>
</div>
ループ変数の参照:
-
{変数名.プロパティ名}: ループ変数のプロパティを参照(例:{shop.name}) -
{変数名.ネストされた.プロパティ}: ネストされたプロパティを参照(例:{shop.address.city}) -
{変数名}: ループ変数自体を参照(オブジェクトの場合はJSON文字列として表示)
ループ内での他のテンプレート構文:
ループ内では、通常のテンプレート構文({$field}、{@data}など)も使用できます。
<div z-for="shop in {@shops}" class="shop-item">
<h2>{shop.name}</h2>
<p>{$description:説明文}</p>
<a href="/shop/{@shop_id}/{shop.id}/">詳細</a>
</div>
制限事項:
- 現在はバックエンドデータの配列のみをサポート(コンポーネントデータの配列は非対応)
- ネストループは非対応
-
インデックス変数は非対応(
z-for="(item, index) in {@items}"のような構文は使用不可) - 空の配列の場合は、ループ要素全体が削除されます
完全な例:
<!-- HTML -->
<zcode-cms id="cms" backend-data='{"shops": [{"id": "1", "name": "A店舗", "url": "/shop/1/"}, {"id": "2", "name": "B店舗", "url": "/shop/2/"}]}'>
<link slot="css" rel="stylesheet" href="/css/common.css" />
</zcode-cms>
<!-- パーツテンプレート -->
<div class="section">
<div class="section__head">
<div class="section__title">{$title:店舗一覧}</div>
</div>
<div class="section__contents">
<div class="section__items">
<div z-for="shop in {@shops}" class="shop-item">
<h2>{shop.name}</h2>
<a href="{shop.url}">詳細を見る</a>
</div>
</div>
</div>
</div>
注意: z-forループ記法は、バックエンドデータの配列を表示するためのシンプルな実装です。将来的には、コンポーネントデータの配列やネストループなどの機能が追加される予定です。
選択肢記法
ラジオボタン(単一選択)
($fieldName:option1|option2|option3)
パイプ(|)で区切られたオプションから1つを選択します。
<div>($color:red|blue|green)</div>
チェックボックス(複数選択)
($fieldName:option1,option2,option3)
カンマ(,)で区切られたオプションから複数を選択できます。
<div>($tags:tag1,tag2,tag3)</div>
セレクトボックス(単一選択)
($fieldName@:option1|option2|option3)
アットマーク(@)とパイプ(|)で区切られたオプションから1つを選択します。
<div>($size@:S|M|L)</div>
セレクトボックス(複数選択)
($fieldName@:option1,option2,option3)
アットマーク(@)とカンマ(,)で区切られたオプションから複数を選択できます。
<div>($categories@:cat1,cat2,cat3)</div>
条件分岐
z-if は表示するかしないかを決めます。fieldName に紐づく形ではありません(fieldName に紐づくのは z-empty)。属性値に指定したキーの真偽で、要素を表示または削除します。
<element z-if="showContent">
<!-- 条件が真の場合に表示 -->
</element>
<div z-if="showContent">
<p>コンテンツが表示されます</p>
</div>
動作:
- 指定キーの値が真(
true、非空文字列など)の場合、要素を表示 -
指定キーの値が偽(
false、null、空文字列、0)の場合は要素を削除 - 指定キーが存在しない(
undefined)場合は表示として扱う
z-tag 属性(タグ名の動的変更)
<element z-tag="$tagName:h1|h2|h3">
<!-- タグ名を動的に変更 -->
</element>
HTMLタグ名を動的に変更できます。テンプレートで書いたタグ名がデフォルト値として使用されます。
<!-- 見出しタグを動的に変更(h2をデフォルト) -->
<h2 z-tag="$headingTag:h1|h2|h3" class="title">{$title:タイトル}</h2>
<!-- headingTagが"h1"の場合 → <h1 class="title">タイトル</h1> -->
<!-- headingTagが"h2"の場合 → <h2 class="title">タイトル</h2> -->
<!-- 選択肢を指定しない場合(全量表示) -->
<div z-tag="$containerTag" class="container">
{$content:コンテンツ}
</div>
動作:
-
デフォルト値: テンプレートで書いたタグ名(例:
<h2>)がデフォルト値として使用されます -
選択肢の指定:
z-tag="$tagName:h1|h2|h3"のように、パイプ(|)で区切って選択肢を指定できます - 全量表示: 選択肢を指定しない場合、すべての有効なタグが選択肢として表示されます
-
属性の保持:
タグ名が変更されても、
class、idなどの属性は保持されます - 子要素の保持: タグ名が変更されても、子要素は保持されます
対応タグ:
-
見出し:
h1,h2,h3,h4,h5,h6 - コンテナ:
div,p,span - リスト:
li,ul,ol -
セマンティック:
section,article,aside,nav,header,footer,main -
その他:
figure,figcaption,blockquote,pre,code -
テーブル:
table,thead,tbody,tr,th,td
使用例:
<!-- 見出しタグを動的に変更 -->
<h2 z-tag="$headingTag:h1|h2|h3" class="title">{$title:タイトル}</h2>
<!-- リストアイテムのタグを変更 -->
<li z-tag="$itemTag:li|div|span" class="list-item">{$item:項目}</li>
<!-- コンテナタグを変更 -->
<div z-tag="$containerTag:div|section|article" class="container">
{$content:コンテンツ}
</div>
注意: テンプレートで書いたタグ名が選択肢に含まれていない場合、開発環境では警告が表示され、選択肢の最初の値がデフォルト値として使用されます。
z-empty 属性(fieldName に紐づく・空なら親要素削除)
<element z-empty="$fieldName">
<!-- フィールドが空の場合、要素ごと削除 -->
</element>
z-empty は
$fieldName
で指定したフィールドに紐づきます。そのフィールドが空(undefined、null、空文字列、または実質的に空のrichテキスト)の場合、親要素を削除します。基本動作では空タグが残るため、親ごと消したいときだけ
z-empty を使ってください。
<!-- 基本動作:空タグが残る -->
<p>{$subtitle?:サブタイトル}</p>
<!-- subtitleがundefinedの場合、<p></p>が残る -->
<!-- z-empty:親要素を削除 -->
<div z-empty="$subtitle">
<p>{$subtitle?:サブタイトル}</p>
</div>
<!-- subtitleがundefinedの場合、<div>要素ごと削除される -->
動作:
- フィールドが
undefined、null、空文字列の場合、要素を削除 -
richテキストが実質的に空(
<p></p>、<p> </p>など)の場合も要素を削除 - フィールドに値がある場合は、要素を表示
使用例:
<!-- オプショナルフィールドで親要素を削除したい場合 -->
<div z-empty="$subtitle">
<p>{$subtitle?:サブタイトル}</p>
</div>
<!-- richテキストが空の場合も削除 -->
<div z-empty="$content">
<div>{$content?:デフォルトコンテンツ:rich}</div>
</div>
<!-- textareaフィールドでも使用可能 -->
<div z-empty="$description">
<div>{$description?:説明文:textarea}</div>
</div>
注意:
fieldName に紐づくのは z-empty です。z-if は表示 on/off
のみで、field には紐づきません。
スロット(ネスト構造)
<element z-slot="slotName">
<!-- スロットコンテンツ -->
</element>
スロットを使用して、パーツ内に他のパーツをネストできます。
<div class="features">
<div z-slot="items">
<!-- ここに子パーツが追加されます -->
</div>
</div>
スロットの制限
PartDataのslotsプロパティでallowedPartsを指定することで、スロットに追加可能なパーツを制限できます。
{
"id": "feature-list",
"title": "機能一覧",
"body": "<div z-slot=\"items\"></div>",
"slots": {
"items": {
"allowedParts": ["feature-item-1", "feature-item-2"]
}
}
}
セキュリティ注意: テンプレート構文で属性値にユーザー入力を設定する場合、基本的なエスケープ処理とURL検証が適用されますが、サーバー側での検証を必ず実装してください。
編集モード
ZeroCode.jsでは、4つの編集モードが利用できます。
edit(編集モード)
既存のコンポーネントを編集するモードです。
- コンポーネントのフィールドを編集
- 親要素への移動(「親要素を選択」ボタン)
- 同じパーツをクリックした場合はパネルを閉じる
add(追加モード)
新しいコンポーネントを追加するモードです。
- パーツ一覧から選ぶと、既定では対象要素の後ろにすぐ追加される(編集モードへは移動しない)
- 既定では追加パネルは開いたまま続けて選択でき、追加直後は追加したコンポーネントがアンカーになる
- カテゴリタブ行の右端の設定アイコンから、「選択した要素の前に追加する」「追加後に編集に移動」「プレビューに+ボタンを表示」を切り替えられる。追加後に編集に移動をオンにすると、追加直後に編集モードへ切り替わり追加したパーツを編集できる
- 「選択したパーツ」タブではカードをクリックすると、プレビューで選んだパーツを複製して追加できる
- 親要素への移動(「親要素を選択」ボタン)
delete(削除モード)
コンポーネントを削除するモードです。
- 削除確認
- 親要素への移動(「親要素を選択」ボタン)
reorder(並べ替えモード)
コンポーネントの順序を変更するモードです。
- 並べ替えパネル(構造リスト): パーツをクリックするとパネルが開き、行の D&D または行クリックで操作できます(page 直下・スロット内)
- プレビュー click-click: 移動元 → 移動先の順にクリック(D&D と同じ insert 移動)
- プレビュー上の直接 D&D は不可(パネル D&D または click-click を利用)
-
オプション「パーツにラベルを表示」(
showReorderStructureLabels)で同階層ラベルを表示 - 親要素への移動(「親要素を選択」ボタン)
- 他モードから切り替えた際、選択中パーツを移動元として引き継ぎ
親要素選択
各編集モードで使用可能な親要素選択機能です。
- 「親要素を選択」ボタンで親要素に移動
- 親要素がない場合はボタンが非表示になる
- 移動時に自動的にスクロールして移動先を表示
パーツ管理
パーツ管理は、zcode-editorでのみ利用可能な機能です。
タイプとパーツ
- タイプ(Type): パーツのグループ。複数のパーツを含む
- パーツ(Part): 実際のコンポーネントテンプレート
共通パーツ、個別パーツ、専用パーツ
- 共通パーツ: すべてのページで使用可能なパーツ
- 個別パーツ: 特定のページでのみ使用可能なパーツ
- 専用パーツ: 動的ページ(動的ルートやURLパラメータに応じて生成されるページ)で使用可能なパーツ
パーツ管理機能
- タイプの作成・編集・削除
- パーツの作成・編集・削除
- タイプ間の並べ替え
- パーツのプレビュー表示
- Monaco Editorによるコード編集
- テンプレート記法の予測変換(オプション)
パーツテンプレートの構造
{
"id": "part-id",
"title": "パーツタイトル",
"description": "パーツの説明",
"body": "<div>{$title:タイトル}</div>",
"slots": {
"slotName": {
"allowedParts": ["part-id-1", "part-id-2"]
}
},
"slotOnly": false
}
テンプレート記法の後から追加と自動初期化
パーツ管理で既存のパーツにテンプレート記法(フィールド)を後から追加した場合、既存のページデータ(page)にそのパーツを使用しているコンポーネントがあると、自動的に不足しているフィールドが初期化されます。
動作:
- データ読み込み時の自動初期化: ページデータが読み込まれる際に、すべてのコンポーネント(スロット内も含む)に対して自動的に初期化処理が実行されます
-
通常フィールドの初期化:
テンプレートに定義されている通常フィールド(
{$field:default})で、コンポーネントデータに存在しない(undefined)場合、デフォルト値で初期化されます -
オプショナルフィールドの扱い:
オプショナルフィールド(
{$field?:default})は初期化されず、undefinedのまま残ります - 再帰的な処理: スロット内の子コンポーネントも再帰的に処理され、すべての階層で初期化が行われます
初期化されるデフォルト値:
-
テキストフィールド(
{$field:default:text}): テンプレートで指定されたデフォルト値、または空文字列 -
テキストエリア(
{$field:default:textarea}): テンプレートで指定されたデフォルト値、または空文字列 -
リッチテキスト(
{$field:default:rich}): デフォルト値がある場合は<p>デフォルト値</p>、ない場合は<p></p> -
画像フィールド(
{$field:default:image}): テンプレートで指定されたデフォルト値、または空文字列 -
ラジオボタン(
($field:option1|option2)): 最初の選択肢 -
セレクトボックス(
($field@:option1|option2)): 最初の選択肢 -
チェックボックス(
($field:option1,option2)): 空配列[] -
複数選択セレクト(
($field@:option1,option2)): 空配列[] -
ブール値(
z-ifで使用される場合など):true
使用例:
// 既存のパーツテンプレート
{
"id": "hero-part",
"title": "ヒーローセクション",
"body": "<div>{$title:タイトル}</div>"
}
// 後からテンプレート記法を追加
{
"id": "hero-part",
"title": "ヒーローセクション",
"body": "<div>{$title:タイトル}</div><div>{$subtitle:サブタイトル}</div>"
}
// 既存のページデータ(初期化前)
{
"id": "hero-1",
"part_id": "hero-part",
"title": "既存のタイトル"
// subtitleフィールドが存在しない
}
// データ読み込み後の自動初期化(初期化後)
{
"id": "hero-1",
"part_id": "hero-part",
"title": "既存のタイトル",
"subtitle": "サブタイトル" // 自動的に追加・初期化される
}
オプショナルフィールドの例:
// パーツテンプレートにオプショナルフィールドを追加
{
"id": "hero-part",
"title": "ヒーローセクション",
"body": "<div>{$title:タイトル}</div><div>{$subtitle?:サブタイトル}</div>"
}
// 既存のページデータ(初期化後)
{
"id": "hero-1",
"part_id": "hero-part",
"title": "既存のタイトル"
// subtitleはundefinedのまま(初期化されない)
}
オプショナルフィールドで親要素を削除したい場合:
オプショナルフィールドが空の場合、基本動作では親要素(タグ)が残ります。親要素ごと削除したい場合は、z-empty属性を使用してください。
// パーツテンプレート(z-emptyを使用)
{
"id": "hero-part",
"title": "ヒーローセクション",
"body": "<div>{$title:タイトル}</div><div z-empty=\"$subtitle\"><p>{$subtitle?:サブタイトル}</p></div>"
}
// 既存のページデータ(subtitleがundefinedの場合)
{
"id": "hero-1",
"part_id": "hero-part",
"title": "既存のタイトル"
// subtitleはundefinedのまま
}
// レンダリング結果:subtitleがundefinedの場合、<div z-empty="$subtitle">要素ごと削除される
<div>既存のタイトル</div>
<!-- subtitleのdiv要素は表示されない -->
注意:
この初期化処理は、データ読み込み時(page属性が設定された時点)に自動的に実行されます。パーツテンプレートを編集した後、ページデータを再読み込みすると、新しいフィールドが自動的に初期化されます。
補足: テンプレート記法を削除した場合、ページデータにはフィールドが残りますが、パーツテンプレートには存在しないため、表示上は非表示になります。不要になったフィールドは、手動でページデータから削除するか、バックエンドで一括削除する処理を実装してください。
画像管理
画像の一覧管理(アップロード・ID 編集・削除)は zcode-editor /
zcode-studio
の画像管理タブで行います。ページ編集時の画像選択は
zcode-cms(および Editor のページ管理)の編集パネルから行います。
画像管理機能
- 画像のアップロード
- 画像の編集(ID、名前、URL)
- 画像の削除
- 共通・個別・専用カテゴリの管理(Editor / Studio)
- 専用画像の page-id スコープ(CMS 画像選択モーダル。詳細は下記)
画像データ構造
interface ImageData {
id: string;
name: string;
url: string;
mimeType?: string;
needsUpload?: boolean;
scope?: 'shared' | 'page'; // 専用画像(images-special)のみ
pageId?: string; // scope が 'page' のとき
}
専用画像のスコープ(page-id)
ブログの記事編集など、同一ユーザーが複数ページを編集する場合、page-id
属性で専用画像をページ単位に区別できます。内部キーは従来どおり
images-special です。
scope |
意味 | CMS 画像選択(page-id 指定時) |
|---|---|---|
未指定 / shared |
全ページで選択可能 | 表示される |
page + pageId |
当該 page-id の編集画面のみ | 一致する pageId のみ |
-
zcode-cms/zcode-editor/zcode-studioのpage-id属性に記事 ID 等を渡す - CMS 編集画面から追加した専用画像は
{ scope: 'page', pageId }になる -
Editor / Studio の画像管理タブから追加した専用画像は
scope: 'shared'(管理者向け) -
保存は従来どおり
images-specialターゲット(スコープ付き JSON をホストが永続化)
<zcode-cms
page-id="post-123"
page="..."
images-special="..."
></zcode-cms>
mimeType
画像のMIMEタイプ(例: image/jpeg,
image/png)。base64画像の場合に設定されます。
needsUpload
trueの場合、バックエンドで画像のアップロード処理が必要です。通常、base64画像はneedsUpload: trueとして保存されます。
画像選択モーダル
編集パネルから画像を選択する際に表示されるモーダルです(zcode-cms および
Editor のページ管理)。
- タブ: 全て / 共通 / 個別 / 専用
- 専用タブおよび「全て」タブで、専用画像の追加・並べ替え・削除が可能
- ツールバー右の「専用画像を追加」は常時表示(追加後は自動選択)
-
page-id指定時は、他 page の専用画像は一覧から除外(UI バッジは表示しない)
設定オプション
ZeroCode.jsでは、config属性で初期設定を指定できます。
設定の構造
設定はcms、dev、および共通設定の3つのカテゴリに分離されています。
{
"cms": {
"allowDynamicContentInteraction": false,
"devRightPadding": false,
"enableContextMenu": false
},
"dev": {
"showDataViewer": false,
"enableTemplateSuggestions": false
},
"categoryOrder": "common"
}
設定の優先順位
- localStorage: ユーザーが変更した設定(最優先)
- config属性: 初期設定として指定された値
- デフォルト値: 全て
false
CMS設定(cms)
zcode-cmsとzcode-editorの両方で共有される設定です。
allowDynamicContentInteraction
デフォルト: false
アコーディオン、タブ、モーダル、リンクなどの動的コンテンツの動作を有効/無効にします。
設定パネルでは「ページの動作を有効にする」として表示されます。
devRightPadding
デフォルト: false
編集パネル表示時にコンテンツの右余白を追加します。
設定パネルでは「編集パネル分の余白をつける」として表示されます。
enableContextMenu
デフォルト: false
右クリックメニューを有効にします。
設定パネルでは「右クリックメニューを有効にする」として表示されます。
Dev設定(dev)
zcode-editor専用の設定です。
showDataViewer
デフォルト: false
データビューアを表示します。
設定パネルでは「データビューアを表示」として表示されます。
enableTemplateSuggestions
デフォルト: false
テンプレート記法の予測変換を有効にします。
パーツ管理パネルのエディタで使用されます。
共通設定
zcode-cmsとzcode-editorの両方で使用される設定です。
categoryOrder
デフォルト: "common"
パーツ管理、画像管理、データビューア、追加パネルにおける「共通」「個別」「専用」タブの表示順序と初期選択を制御します。
設定可能な値:
-
"common": 共通タブを先に表示し、初期状態で共通タブを選択(デフォルト) "individual": 個別タブを先に表示し、初期状態で個別タブを選択-
"special": 専用タブを先に表示し、初期状態で専用タブを選択(専用データが存在する場合のみ表示)
この設定は以下の画面に適用されます:
- パーツ管理タブ(
zcode-editorのみ) - 画像管理タブ(
zcode-editorのみ) - データビューアタブ(
zcode-editorのみ、パーツ/画像表示時) - 追加パネル(
zcode-cmsとzcode-editorの両方)
設定の使用例
<zcode-cms
config='{"cms": {"allowDynamicContentInteraction": true, "devRightPadding": true, "enableContextMenu": true}, "categoryOrder": "individual"}'
></zcode-cms>
<zcode-editor
config='{"cms": {"allowDynamicContentInteraction": true}, "dev": {"showDataViewer": true}, "categoryOrder": "individual"}'
></zcode-editor>
または、JavaScript変数で指定することもできます:
const cmsConfig = {
cms: {
allowDynamicContentInteraction: true,
devRightPadding: true,
enableContextMenu: true
},
categoryOrder: 'individual'
};
const cmsElement = document.getElementById('cms');
cmsElement.setAttribute('config', JSON.stringify(cmsConfig));
APIリファレンス
zcode-cms
ユーザー向け管理画面のWebコンポーネント。
属性
| 属性名 | 型 | 説明 |
|---|---|---|
page |
string | ページデータ(JSON文字列) |
page-id |
string |
専用画像のページスコープ用 ID(記事 ID
など)。指定時、画像選択モーダルでは「全ページ」(shared) と当該 page-id
の「このページ」(scope: 'page') のみ表示。CMS から専用画像を追加すると
scope: 'page' が付与される
|
parts-common |
string | 共通パーツデータ(JSON文字列) |
parts-individual |
string | 個別パーツデータ(JSON文字列) |
parts-special |
string | 専用パーツデータ(JSON文字列) |
images-common |
string | 共通画像データ(JSON文字列) |
images-individual |
string | 個別画像データ(JSON文字列) |
images-special |
string | 専用画像データ(JSON文字列) |
config |
string | 初期設定データ(JSON文字列) |
use-shadow-dom |
string | Shadow DOMを使用するか('true' | 'false'、デフォルト: 'true') |
スロット
css: CSSファイルを指定script: JavaScriptファイルを指定
zcode-editor
エンジニア・デザイナー向け管理画面のWebコンポーネント。
ZeroCodeCMSの機能に加えて、パーツ管理・画像管理・データビューアが利用できます。
属性
zcode-cmsの属性(page-id
含む)に加えて、以下の属性が利用できます:
| 属性名 | 型 | 説明 |
|---|---|---|
enable-parts-manager |
string | パーツ管理を有効にするか(デフォルト: 'true') |
enable-images-manager |
string | 画像管理を有効にするか(デフォルト: 'true') |
renderToHtml()
ページコンポーネントからHTML文字列を生成します。CSSは含まれません。
Node や SSR では zerocodejs/ssr から import
することを推奨します(軽量エントリ。Vue / Web Components は含みません)。ブラウザ用の一括
import は zerocodejs のままでも問題ありません。
import { renderToHtml } from 'zerocodejs/ssr';
const html = renderToHtml(data, {
enableEditorAttributes: false
});
パラメータ
data: ZeroCodeData形式のデータ-
options.enableEditorAttributes: 編集用属性を有効にするか(デフォルト:false)
戻り値
生成されたHTML文字列
renderCssToHtml()
CSSデータから<style>タグのHTML文字列を生成します。common → individual →
special の順で出力されます。
import { renderToHtml, renderCssToHtml } from 'zerocodejs/ssr';
const content = renderToHtml(data, { enableEditorAttributes: false });
const styles = renderCssToHtml(data.css);
// content は <body> に、styles は <head> に配置
パラメータ
-
css:{ common?: string; individual?: string; special?: string }
戻り値
<style>タグを含むHTML文字列。空やundefinedのカテゴリはスキップされます。
イベント
save-request
保存ボタンクリック時に発火します。event.detail に
data は含まれません。保存対象のデータは
cms.getData() で取得してください。
cms.addEventListener('save-request', (event) => {
const { requestId, source, targets, timestamp } = event.detail;
const data = cms.getData();
for (const target of targets) {
// target ごとに data から必要な部分を切り出してサーバーへ送る
}
});
event.detail
requestId: リクエストID(save-resultで対応付けに使用)source: 送信元('cms'または'editor')-
targets: 保存対象の配列('page','parts-common','parts-individual','parts-special','images-common','images-individual','images-special','parts-common-css','parts-individual-css','parts-special-css'のいずれか) timestamp: タイムスタンプ
含まれないもの: target(単数)・data。データは
cms.getData() で取得する。
zcode-dom-updated
DOMが更新されたときに発火します。動的コンテンツの初期化などに使用できます。
window.addEventListener('zcode-dom-updated', () => {
// DOM更新後の処理
initializeAccordion();
});
メソッド
getData(path?: string)
データを取得します。
const cms = document.getElementById('cms');
// 全体のデータを取得
const allData = cms.getData();
// 特定のパスのデータを取得
const pageData = cms.getData('page');
const firstComponent = cms.getData('page.0');
const title = cms.getData('page.0.title');
パラメータ
-
path(オプション): データのパス(例:'page','page.0','page.0.title')
戻り値
指定したパスのデータ。パスが指定されていない場合は全体のデータを返します。
setData(path: string | object, value?: any)
データを設定します。
const cms = document.getElementById('cms');
// パスを指定して値を設定
cms.setData('page.0.title', '新しいタイトル');
// オブジェクト全体を設定
cms.setData({
page: [...],
parts: {
common: [...],
individual: [...]
}
});
セキュリティ注意: このメソッドはクライアント側から任意のデータを設定できます。開発者ツールからも呼び出し可能です。サーバー側での検証を必ず実装してください。
パラメータ
-
path: データのパス(文字列の場合)またはデータオブジェクト全体(オブジェクトの場合) value(オプション): 設定する値(pathが文字列の場合)
allowDynamicContentInteraction(プロパティ)
動的コンテンツの動作を有効/無効にします(getter/setter)。
const cms = document.getElementById('cms');
// 値を取得
const isEnabled = cms.allowDynamicContentInteraction;
// 値を設定
cms.allowDynamicContentInteraction = true;
データ構造
ZeroCode.jsで使用するデータ構造の説明です。
ZeroCodeData
interface ZeroCodeData {
page: ComponentData[];
parts: {
common: TypeData[];
individual: TypeData[];
special: TypeData[];
};
images: {
common: ImageData[];
individual: ImageData[];
special: ImageData[];
};
}
ComponentData
interface ComponentData {
id: string;
part_id: string;
[key: string]: any;
slots?: Record<string, ComponentData[] | SlotConfig>;
}
id: コンポーネントの一意のIDpart_id: パーツID(タイトル変更時も紐付けが維持される)-
[key: string]: any: フィールドの値(テンプレート記法で定義されたフィールド) slots: スロットの子コンポーネント
SlotConfig
interface SlotConfig {
allowedParts?: string[];
children?: ComponentData[];
}
allowedParts: 許可されるパーツIDの配列-
children: 子コンポーネントの配列(ComponentData[]としても使用可能)
TypeData
interface TypeData {
id: string;
type: string;
description: string;
parts: PartData[];
}
id: タイプID(タイプ変更時も紐付けが維持される)type: タイプ名description: タイプの説明parts: パーツの配列
PartData
interface PartData {
id: string;
title: string;
description: string;
body: string;
slots?: Record<string, { allowedParts?: string[] }>;
slotOnly?: boolean;
}
id: パーツID(タイトル変更時も紐付けが維持される)title: パーツタイトルdescription: パーツの説明body: パーツのテンプレート(HTML文字列)slots: スロットの設定slotOnly: スロット専用パーツかどうか
ImageData
interface ImageData {
id: string;
name: string;
url: string;
mimeType?: string;
needsUpload?: boolean;
}
id: 画像の一意のIDname: 画像名-
url: 画像のURL(base64データの場合はdata:image/...形式) -
mimeType: MIMEタイプ(例:image/jpeg,image/png) -
needsUpload: アップロードが必要かどうか(trueの場合、バックエンドでアップロード処理が必要)
データベース構成
ZeroCode.jsのデータをRDBで管理する場合の推奨データベース設計です。
テーブル構成
以下の3つのテーブルで構成されます:
- zcode_common_parts: 共通パーツ管理(店舗に依存しない)
- zcode_common_images: 共通画像管理(店舗に依存しない)
- zcode_individual: 店舗ごとのページ・個別パーツ・個別画像
zcode_common_parts(共通パーツ)
すべての店舗で共有される共通パーツを管理します。
CREATE TABLE zcode_common_parts (
id VARCHAR(50) PRIMARY KEY,
type VARCHAR(50) NOT NULL,
description TEXT,
parts JSON NOT NULL,
version INT NOT NULL DEFAULT 1,
is_published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
| カラム名 | 型 | NULL | 説明 |
|---|---|---|---|
id |
VARCHAR(50) | NO | パーツタイプID(PK) |
type |
VARCHAR(50) | NO | パーツタイプ(例: hero, features) |
description |
TEXT | YES | タイプの説明 |
parts |
JSON | NO | パーツ配列(PartData[]形式) |
version |
INT | NO | バージョン番号(楽観的ロック用) |
is_published |
BOOLEAN | NO | 公開フラグ |
created_at |
TIMESTAMP | NO | 作成日時 |
updated_at |
TIMESTAMP | NO | 更新日時 |
zcode_common_images(共通画像)
すべての店舗で共有される共通画像を管理します。
CREATE TABLE zcode_common_images (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
url TEXT NOT NULL,
mime_type VARCHAR(50) NOT NULL,
needs_upload BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
| カラム名 | 型 | NULL | 説明 |
|---|---|---|---|
id |
VARCHAR(50) | NO | 画像ID(PK) |
name |
VARCHAR(255) | NO | 画像名 |
url |
TEXT | NO | 画像URL(base64の場合はdata:image/...形式) |
mime_type |
VARCHAR(50) | NO | MIMEタイプ(例: image/jpeg, image/png) |
needs_upload |
BOOLEAN | NO |
アップロード要否(trueの場合、バックエンドでアップロード処理が必要)
|
created_at |
TIMESTAMP | NO | 作成日時 |
updated_at |
TIMESTAMP | NO | 更新日時 |
zcode_individual(店舗ごとの個別データ)
店舗ごとのページ、個別パーツ、個別画像を管理します。
CREATE TABLE zcode_individual (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
data_type ENUM('page', 'parts', 'images') NOT NULL,
content JSON NOT NULL,
version INT NOT NULL DEFAULT 1,
status ENUM('draft', 'published', 'archived') NOT NULL DEFAULT 'draft',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_type_status (user_id, data_type, status)
);
| カラム名 | 型 | NULL | 説明 |
|---|---|---|---|
id |
BIGINT | NO | レコードID(PK, AUTO_INCREMENT) |
user_id |
BIGINT | NO | 店舗ID(外部キー) |
data_type |
ENUM | NO | データ種別(page, parts, images) |
content |
JSON | NO |
データ内容(ComponentData[], TypeData[],
ImageData[]形式)
|
version |
INT | NO | バージョン番号(楽観的ロック用) |
status |
ENUM | NO |
ステータス(draft: 下書き, published: 公開,
archived: アーカイブ)
|
created_at |
TIMESTAMP | NO | 作成日時 |
updated_at |
TIMESTAMP | NO | 更新日時 |
データ例
zcode_individual のデータ例
-- 店舗1(user_id=1)のページデータ(下書き)
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
1,
'page',
'[{"id": "hero-1", "part_id": "zcode-part-1", "title": "店舗1のタイトル", ...}]',
'draft'
);
-- 店舗1(user_id=1)のページデータ(公開版)
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
1,
'page',
'[{"id": "hero-1", "part_id": "zcode-part-1", "title": "店舗1のタイトル", ...}]',
'published'
);
-- 店舗1(user_id=1)の個別パーツデータ
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
1,
'parts',
'[{"id": "zcode-part-12", "type": "cta", "description": "店舗専用のCTA", ...}]',
'published'
);
-- 店舗1(user_id=1)の個別画像データ
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
1,
'images',
'[{"id": "img-ind-1", "name": "店舗専用画像", "url": "/images/store1-hero.jpg", ...}]',
'published'
);
クエリ例
店舗の公開ページデータを取得
SELECT content
FROM zcode_individual
WHERE user_id = 1
AND data_type = 'page'
AND status = 'published'
LIMIT 1;
店舗の下書きページデータを取得
SELECT content
FROM zcode_individual
WHERE user_id = 1
AND data_type = 'page'
AND status = 'draft'
LIMIT 1;
共通パーツを取得
SELECT parts
FROM zcode_common_parts
WHERE is_published = TRUE;
設計の特徴
- 共通データの一元管理: 共通パーツと共通画像は1箇所で管理され、すべての店舗で共有されます
- 個別データの統合管理: 店舗ごとのページ、個別パーツ、個別画像を1つのテーブルで管理します
- ステータス管理: 下書き・公開・アーカイブを1テーブルで管理できます(1店舗あたり最大9レコード: 3種別 × 3ステータス)
-
楽観的ロック:
versionカラムを使用して同時更新の競合を防ぎます -
スケーラビリティ:
200店舗規模でも問題なく動作します(インデックス
idx_user_type_statusが有効)
注意事項
-
contentカラムはJSON型またはLONGTEXT型を使用してください(データ量が多い場合) -
MySQLの場合、
JSON型は最大1GBですが、実用的には数MB程度を目安にしてください - PostgreSQLの場合、
JSONB型を使用すると検索パフォーマンスが向上します - 更新時は
versionカラムをチェックして楽観的ロックを実装してください
保存リクエスト
保存ボタンをクリックすると、save-requestイベントが発火します。
イベントの受け取り方
event.detail に data は含まれません。cms.getData()
で取得してください。
const cms = document.getElementById('cms');
cms.addEventListener('save-request', (event) => {
const { requestId, source, targets, timestamp } = event.detail;
const data = cms.getData();
for (const target of targets) {
fetch('/api/zero-code/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target, source, data, requestId, timestamp })
});
}
});
保存ターゲットの仕様
save-requestイベントのtargets配列には、現在のタブやモードに応じて複数のターゲットが含まれる場合があります。
| タブ/モード | 含まれるターゲット | 説明 |
|---|---|---|
| ページ管理(編集モード) |
['page', 'images-special']
|
ページデータと専用画像プールを保存対象として通知(ホストが永続化方針を決定) |
| パーツ管理 |
['parts-common', 'parts-common-css']、['parts-individual', 'parts-individual-css']、または['parts-special', 'parts-special-css']
|
パーツデータとページCSSを保存(パーツ編集時にページCSSも編集可能なため) |
| 画像管理 |
['images-common']、['images-individual']、または['images-special']
|
画像データのみを保存 |
| データビューアー | 選択中のタブとカテゴリに応じて決定 | 現在表示中のデータに対応するターゲット |
画像のアップロード
targets配列にimages-common、images-individual、またはimages-specialが含まれる場合、needsUpload: trueの画像をアップロード処理してください。data はハンドラ先頭で
cms.getData() したもの。
for (const target of targets) {
if (target.startsWith('images-')) {
const images = target === 'images-common'
? data.images?.common || []
: target === 'images-individual'
? data.images?.individual || []
: data.images?.special || [];
const imagesToUpload = images.filter(img => img.needsUpload === true);
for (const image of imagesToUpload) {
// base64データをデコード
const base64Data = image.url.replace(/^data:image\/\w+;base64,/, '');
const buffer = Buffer.from(base64Data, 'base64');
// S3などにアップロード
// アップロード後、image.urlを更新し、needsUploadをfalseに設定
}
}
}
バリデーションエラーの返却
バックエンドでバリデーションを行い、エラーがある場合はsave-resultイベントでエラーを返してください。フロントエンドでは、requiredとmaxのみをチェックします。複雑なバリデーション(メール形式チェック、重複チェックなど)はバックエンドで実施してください。
イベント形式:
// 成功時
cms.dispatchEvent(
new CustomEvent('save-result', {
detail: {
requestId: 'req-1234567890-abc', // save-request の requestId
target: 'page', // この回の保存対象(targets の要素のいずれか)
ok: true,
errors: []
},
bubbles: true,
composed: true
})
);
// エラー時
cms.dispatchEvent(
new CustomEvent('save-result', {
detail: {
requestId: 'req-1234567890-abc', // save-request の requestId
target: 'page', // この回の保存対象(targets の要素のいずれか)
ok: false,
errors: [
{
path: 'page.0', // コンポーネントのパス(オプション、指定しない場合はすべてのコンポーネントに適用)
field: 'email', // フィールド名
message: 'メールアドレスの形式が正しくありません', // エラーメッセージ
code: 'FORMAT' // エラーコード(オプション)
}
]
},
bubbles: true,
composed: true
})
);
実装例(Node.js/Express):
cms.addEventListener('save-request', async (event) => {
const { requestId, source, targets, timestamp } = event.detail;
const data = cms.getData();
for (const target of targets) {
try {
// バリデーション
const errors = [];
if (target === 'page') {
// ページデータのバリデーション
const pageData = data.page || [];
for (let i = 0; i < pageData.length; i++) {
const component = pageData[i];
// メール形式チェック(例)
if (component.email && !component.email.includes('@')) {
errors.push({
path: `page.${i}`,
field: 'email',
message: 'メールアドレスの形式が正しくありません',
code: 'FORMAT'
});
}
// 重複チェック(例)
if (component.title && await isTitleDuplicate(component.title)) {
errors.push({
path: `page.${i}`,
field: 'title',
message: 'このタイトルは既に使用されています',
code: 'DUPLICATE'
});
}
}
} else if (target === 'parts-common-css' || target === 'parts-individual-css' || target === 'parts-special-css') {
// CSSのバリデーション(例)
const css = target === 'parts-common-css' ? data.css?.common :
target === 'parts-individual-css' ? data.css?.individual :
data.css?.special || '';
if (css.length > 10000) {
errors.push({
field: target,
message: 'CSSのサイズが大きすぎます',
code: 'SIZE_LIMIT'
});
}
} else if (target.startsWith('images-')) {
// 画像データのアップロード処理
const images = target === 'images-common'
? data.images?.common || []
: target === 'images-individual'
? data.images?.individual || []
: data.images?.special || [];
for (const image of images) {
if (image.needsUpload) {
// 画像をアップロード
const uploadedUrl = await uploadImage(image);
image.url = uploadedUrl;
image.needsUpload = false;
}
}
}
if (errors.length > 0) {
// エラーがある場合
cms.dispatchEvent(
new CustomEvent('save-result', {
detail: {
requestId,
target,
ok: false,
errors
},
bubbles: true,
composed: true
})
);
continue; // 次のターゲットへ
}
// データベースに保存
await saveToDatabase(target, data);
// 成功時
cms.dispatchEvent(
new CustomEvent('save-result', {
detail: {
requestId,
target,
ok: true,
errors: []
},
bubbles: true,
composed: true
})
);
} catch (error) {
// エラー時
cms.dispatchEvent(
new CustomEvent('save-result', {
detail: {
requestId,
target,
ok: false,
errors: [
{
field: 'general',
message: '保存に失敗しました',
code: 'SAVE_FAILED'
}
]
},
bubbles: true,
composed: true
})
);
}
}
}
});
エラーの表示:
-
編集パネルが開いている場合:
エラーは該当フィールドの下に表示されます。
pathが指定されている場合、そのパスのコンポーネントのフィールドにのみ表示されます。 - 編集パネルが閉じている場合: エラーは画面上部にエラーバナーとして表示されます。編集パネルを開くと、該当フィールドに詳細エラーが表示されます。
注意:
requestIdはsave-requestイベントのrequestIdと同じ値を使用してください。これにより、複数の保存リクエストが同時に発火した場合でも、正しいレスポンスとリクエストを関連付けることができます。
セキュリティ
重要: ZeroCode.jsはフロントエンドライブラリのため、クライアント側での完全なセキュリティ保証はできません。サーバー側での検証を必ず実装してください。
必須実装事項
1. サーバー側でのデータ検証(必須)
- すべてのデータをサーバー側で検証してください
-
パーツテンプレート(
part.body)が信頼できるソースからのみ来ることを確認してください -
save-requestイベントのsourceフィールドを確認し、CMSからのパーツデータ保存を拒否してください
2. 認証・認可
- パーツデータの変更は認証されたユーザーのみ許可してください
- ロールベースアクセス制御を実装してください
- CMS(
source: 'cms')からのパーツデータ保存を拒否してください
3. 属性値のセキュリティ
-
URL属性(
href,src,action)の検証を実装してください -
style属性にユーザー入力を直接設定する場合は、サーバー側での検証を推奨します - 基本的なエスケープ処理が適用されますが、サーバー側での追加検証を推奨します
4. パーツテンプレートの管理
- パーツテンプレートは信頼できるソースからのみ受け入れてください
- サーバー側でテンプレートの妥当性を検証してください
実装例
※ クライアントは save-request ハンドラで cms.getData() し、targets
をループして各 target と data を
req.body に含めて送る。
// Node.js/Express の例(サーバー側)
app.post('/api/zero-code/save', authenticate, (req, res) => {
const { target, source, data } = req.body;
if (source === 'cms' && target.startsWith('parts-')) {
return res.status(403).json({
error: 'CMSからのパーツ保存は拒否されました'
});
}
// データ構造の検証
if (!validateDataStructure(data)) {
return res.status(400).json({
error: '無効なデータです'
});
}
// パーツテンプレートの検証
if (target.startsWith('parts-')) {
if (!validatePartTemplate(data)) {
return res.status(400).json({
error: '無効なテンプレートです'
});
}
}
// 画像アップロード処理
if (target.startsWith('images-')) {
const imagesToUpload = data.filter(img => img.needsUpload === true);
for (const image of imagesToUpload) {
// アップロード処理
}
}
// 保存処理
// ...
res.json({ success: true });
});
画像アップロードのセキュリティ
needsUpload: trueの画像のみアップロード処理を行ってください- MIMEタイプ(
mimeType)の検証を実装してください - ファイルサイズの制限を設定してください
- ファイル名のサニタイズを実装してください