はじめに

ZeroCode.jsは、フレームワーク非依存のWeb ComponentsベースのCMSエディターライブラリです。Vue.jsで実装されており、カスタムHTMLテンプレート構文を使用して動的なコンテンツ管理を提供します。

プロダクトの位置づけ・設計方針の詳細はZeroCode.js とはを参照してください。

ZeroCode.js とは

ZeroCode.js は、既存 Web サービスの認証・永続化・バックエンドデータをホスト側で担いながら、フロントエンドに後付けできる 埋め込み型 CMS ライブラリ です。

目的

既存 Web サービスの運営者が、開発者に依存せずページを更新・運用できる環境を提供します。

そのために、既存サービスの 認証・データ・HTML 構造の方針 を維持したまま導入できる CMS を、フロントエンドライブラリとして実現します。

解決したい課題

一般的な CMS やページビルダーは、CMS 独自の仕組みを持つことが多く、既存システムとの統合コストが発生します。

システム面

編集面

運用面

設計方針

ZeroCode.js は、次の方針に基づいて設計されています。

システム

方針 内容
埋め込み型 既存 Web サービスへ後付けで導入する
バックエンド非提供 永続化・認証・認可はホストアプリが担当する
既存認証の利用 CMS 独自のログイン機能は持たない
イベント駆動の保存 保存ボタンで save-request を発火し、ホストが getData() で取得して永続化する

表示

方針 内容
WYSIWYG に近い編集 公開画面とほぼ同じ見た目・構造で編集する
編集用メタデータの分離 編集時のみ識別用の属性等を付与し、公開 HTML には載せない
差異の最小化 編集画面と公開画面の DOM 構造の差を小さく保つ

編集モードでは、主に data-zcode-iddata-zcode-pathdata-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>

定義

ZeroCode.js は、既存 Web サービスの認証・永続化・バックエンドデータをホスト側で担いながら、フロントエンドに後付けできる埋め込み型 CMS ライブラリです。

開発者が定義した パーツテンプレート を組み合わせ、運営者はノーコードでページを構築・編集できます。

公開 HTML はパーツテンプレートから生成され、CMS 都合の DOM は挟みません。編集時のみ data-zcode-* 等の識別情報を付与し、公開レンダリングでは除去してクリーンな HTML を出力します。

データの扱い

ZeroCode.js には2種類のデータがあります。

種類 説明 永続化
ページ編集データ pagepartsimagescss など ZeroCode 形式の JSON ホストが save-request 受信後に永続化
バックエンドデータ 既存 API / DB から渡す参照用データ({@user.name} 等) ZeroCode は参照のみ。更新はホスト側

「データは既存システムを利用する」とは、認証・永続化・動的参照をホストに委ねる という意味です。ページ構成データそのものは ZeroCode 形式で管理されます。

コンポーネントと役割

コンポーネント 想定ユーザー 主な用途
<zcode-cms> 運営者・編集者 ページの編集・追加・削除・並べ替え
<zcode-editor> 開発者 パーツ管理・画像管理・データビューアを含むフル機能
<zcode-studio> 制作会社など信頼できるユーザー zcode-editor 同等 UI。専用パーツ・専用 CSS・専用画像の編集に限定

ZeroCode.js が提供しないもの

次は 意図的に提供しません。ホストアプリ側で実装してください。

技術的特徴(概要)

編集体験改善ロードマップ

社内ユーザーテスト(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-cmszcode-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属性を指定することを推奨します。これにより、データの管理が明確になり、デバッグも容易になります。

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の利点

ホストスタイルのリセット(: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>

動作:

例:

<!-- テンプレート -->
<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>

バリデーションルール:

動作:

注意: 複雑なバリデーション(メール形式チェック、重複チェックなど)は、バックエンドで実施することを推奨します。フロントエンドでは、requiredmaxのみをサポートしています。

バックエンドデータの参照

バックエンドから渡されたデータをテンプレート内で参照できます。動的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>

動作:

使用例:

<!-- 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>

ループ変数の参照:

ループ内での他のテンプレート構文:

ループ内では、通常のテンプレート構文({$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>

制限事項:

完全な例:

<!-- 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>

動作:

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="$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 で指定したフィールドに紐づきます。そのフィールドが空(undefinednull、空文字列、または実質的に空のrichテキスト)の場合、親要素を削除します。基本動作では空タグが残るため、親ごと消したいときだけ z-empty を使ってください。

<!-- 基本動作:空タグが残る -->
<p>{$subtitle?:サブタイトル}</p>
<!-- subtitleがundefinedの場合、<p></p>が残る -->

<!-- z-empty:親要素を削除 -->
<div z-empty="$subtitle">
  <p>{$subtitle?:サブタイトル}</p>
</div>
<!-- subtitleがundefinedの場合、<div>要素ごと削除される -->

動作:

使用例:

<!-- オプショナルフィールドで親要素を削除したい場合 -->
<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>

スロットの制限

PartDataslotsプロパティで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(並べ替えモード)

コンポーネントの順序を変更するモードです。

親要素選択

各編集モードで使用可能な親要素選択機能です。

パーツ管理

パーツ管理は、zcode-editorでのみ利用可能な機能です。

タイプとパーツ

共通パーツ、個別パーツ、専用パーツ

パーツ管理機能

パーツテンプレートの構造

{
  "id": "part-id",
  "title": "パーツタイトル",
  "description": "パーツの説明",
  "body": "<div>{$title:タイトル}</div>",
  "slots": {
    "slotName": {
      "allowedParts": ["part-id-1", "part-id-2"]
    }
  },
  "slotOnly": false
}

テンプレート記法の後から追加と自動初期化

パーツ管理で既存のパーツにテンプレート記法(フィールド)を後から追加した場合、既存のページデータ(page)にそのパーツを使用しているコンポーネントがあると、自動的に不足しているフィールドが初期化されます。

動作:

初期化されるデフォルト値:

使用例:

// 既存のパーツテンプレート
{
  "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 のページ管理)の編集パネルから行います。

画像管理機能

画像データ構造

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
  page-id="post-123"
  page="..."
  images-special="..."
></zcode-cms>

mimeType

画像のMIMEタイプ(例: image/jpeg, image/png)。base64画像の場合に設定されます。

needsUpload

trueの場合、バックエンドで画像のアップロード処理が必要です。通常、base64画像はneedsUpload: trueとして保存されます。

画像選択モーダル

編集パネルから画像を選択する際に表示されるモーダルです(zcode-cms および Editor のページ管理)。

設定オプション

ZeroCode.jsでは、config属性で初期設定を指定できます。

設定の構造

設定はcmsdev、および共通設定の3つのカテゴリに分離されています。

{
  "cms": {
    "allowDynamicContentInteraction": false,
    "devRightPadding": false,
    "enableContextMenu": false
  },
  "dev": {
    "showDataViewer": false,
    "enableTemplateSuggestions": false
  },
  "categoryOrder": "common"
}

設定の優先順位

  1. localStorage: ユーザーが変更した設定(最優先)
  2. config属性: 初期設定として指定された値
  3. デフォルト値: 全てfalse

CMS設定(cms)

zcode-cmszcode-editorの両方で共有される設定です。

allowDynamicContentInteraction

デフォルト: false

アコーディオン、タブ、モーダル、リンクなどの動的コンテンツの動作を有効/無効にします。

設定パネルでは「ページの動作を有効にする」として表示されます。

devRightPadding

デフォルト: false

編集パネル表示時にコンテンツの右余白を追加します。

設定パネルでは「編集パネル分の余白をつける」として表示されます。

enableContextMenu

デフォルト: false

右クリックメニューを有効にします。

設定パネルでは「右クリックメニューを有効にする」として表示されます。

Dev設定(dev)

zcode-editor専用の設定です。

showDataViewer

デフォルト: false

データビューアを表示します。

設定パネルでは「データビューアを表示」として表示されます。

enableTemplateSuggestions

デフォルト: false

テンプレート記法の予測変換を有効にします。

パーツ管理パネルのエディタで使用されます。

共通設定

zcode-cmszcode-editorの両方で使用される設定です。

categoryOrder

デフォルト: "common"

パーツ管理、画像管理、データビューア、追加パネルにおける「共通」「個別」「専用」タブの表示順序と初期選択を制御します。

設定可能な値:

この設定は以下の画面に適用されます:

設定の使用例

<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')

スロット

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
});

パラメータ

戻り値

生成された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> に配置

パラメータ

戻り値

<style>タグを含むHTML文字列。空やundefinedのカテゴリはスキップされます。

イベント

save-request

保存ボタンクリック時に発火します。event.detaildata は含まれません。保存対象のデータは 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

含まれないもの: 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');

パラメータ

戻り値

指定したパスのデータ。パスが指定されていない場合は全体のデータを返します。

setData(path: string | object, value?: any)

データを設定します。

const cms = document.getElementById('cms');

// パスを指定して値を設定
cms.setData('page.0.title', '新しいタイトル');

// オブジェクト全体を設定
cms.setData({
  page: [...],
  parts: {
    common: [...],
    individual: [...]
  }
});

セキュリティ注意: このメソッドはクライアント側から任意のデータを設定できます。開発者ツールからも呼び出し可能です。サーバー側での検証を必ず実装してください。

パラメータ

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>;
}

SlotConfig

interface SlotConfig {
  allowedParts?: string[];
  children?: ComponentData[];
}

TypeData

interface TypeData {
  id: string;
  type: string;
  description: string;
  parts: PartData[];
}

PartData

interface PartData {
  id: string;
  title: string;
  description: string;
  body: string;
  slots?: Record<string, { allowedParts?: string[] }>;
  slotOnly?: boolean;
}

ImageData

interface ImageData {
  id: string;
  name: string;
  url: string;
  mimeType?: string;
  needsUpload?: boolean;
}

データベース構成

ZeroCode.jsのデータをRDBで管理する場合の推奨データベース設計です。

テーブル構成

以下の3つのテーブルで構成されます:

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;

設計の特徴

注意事項

保存リクエスト

保存ボタンをクリックすると、save-requestイベントが発火します。

イベントの受け取り方

event.detaildata は含まれません。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-commonimages-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イベントでエラーを返してください。フロントエンドでは、requiredmaxのみをチェックします。複雑なバリデーション(メール形式チェック、重複チェックなど)はバックエンドで実施してください。

イベント形式:

// 成功時
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
          })
        );
      }
    }
  }
});

エラーの表示:

注意: requestIdsave-requestイベントのrequestIdと同じ値を使用してください。これにより、複数の保存リクエストが同時に発火した場合でも、正しいレスポンスとリクエストを関連付けることができます。

セキュリティ

重要: ZeroCode.jsはフロントエンドライブラリのため、クライアント側での完全なセキュリティ保証はできません。サーバー側での検証を必ず実装してください。

必須実装事項

1. サーバー側でのデータ検証(必須)

2. 認証・認可

3. 属性値のセキュリティ

4. パーツテンプレートの管理

実装例

※ クライアントは save-request ハンドラで cms.getData() し、targets をループして各 targetdatareq.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 });
});

画像アップロードのセキュリティ