mcpField
U
unlayer-custom-tools

unlayer-custom-tools

github.com/unlayer開発14131 回インストール4 週間前に更新
リポジトリ

unlayer-custom-tools について

Unlayer エディターでドラッグ&ドロップ式のカスタムコンテンツブロックを作るための開発スキルです。レンダリング、HTML 出力、プロパティ設定、メールと Web のモード構成を扱います。

主な機能

  1. 01レンダラー、エクスポーター、プロパティエディターの設定
  2. 02カスタムツールの登録と構成
  3. 03各ウィジェット値の参照方法
  4. 04React によるカスタムプロパティエディター
  5. 05メール向けテーブル HTML の指針

活用シーン

  1. 01商品カードなどのカスタムブロック追加
  2. 02エディターパネルでのコンテンツ設定の提供
  3. 03メールや Web 用のカスタムブロック出力

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/unlayer/unlayer-skills --skill unlayer-custom-tools

インストール後、Claude Code で/unlayer-custom-toolsを入力して読み込まれたか確認します。

詳細

カスタムツールの作成

概要

カスタムツールは、Unlayer エディター用に作成するドラッグ&ドロップ式のコンテンツブロックです。各ツールには以下が必要です。

  • レンダラー(エディターでユーザーに表示されるもの)
  • エクスポーター(HTML 出力。メール用にはテーブルベースである必要があります)
  • プロパティエディター(設定パネル)

完全な例:商品カード

画像、タイトル、価格、購入ボタンを備えた、完全に動作するカスタムツールです。

unlayer.registerTool({
  name: 'product_card',
  label: 'Product Card',
  icon: 'fa-shopping-cart',
  supportedDisplayModes: ['web', 'email'],

  options: {
    content: {
      title: 'Content',
      position: 1,
      options: {
        productTitle: {
          label: 'Product Title',
          defaultValue: 'Product Name',
          widget: 'text',               // → values.productTitle = 'Product Name'
        },
        productImage: {
          label: 'Image',
          defaultValue: { url: 'https://via.placeholder.com/300x200' },
          widget: 'image',              // → values.productImage.url = 'https://...'
        },
        price: {
          label: 'Price',
          defaultValue: '$99.99',
          widget: 'text',               // → values.price = '$99.99'
        },
        buttonText: {
          label: 'Button Text',
          defaultValue: 'Buy Now',
          widget: 'text',
        },
        buttonLink: {
          label: 'Button Link',
          defaultValue: { name: 'web', values: { href: 'https://example.com', target: '_blank' } },
          widget: 'link',               // → values.buttonLink.values.href = 'https://...'
        },
      },
    },
    colors: {
      title: 'Colors',
      position: 2,
      options: {
        titleColor: {
          label: 'Title Color',
          defaultValue: '#333333',
          widget: 'color_picker',       // → values.titleColor = '#333333'
        },
        buttonBg: {
          label: 'Button Background',
          defaultValue: '#007bff',
          widget: 'color_picker',
        },
      },
    },
  },

  values: {},

  renderer: {
    Viewer: unlayer.createViewer({
      render(values) {
        return `
          <div style="text-align: center; padding: 20px;">
            <img src="${values.productImage.url}" style="max-width: 100%;" />
            <h3 style="color: ${values.titleColor};">${values.productTitle}</h3>
            <p style="font-size: 24px; font-weight: bold;">${values.price}</p>
            <a href="${values.buttonLink.values.href}"
               style="display: inline-block; background: ${values.buttonBg};
                      color: #fff; padding: 12px 24px; text-decoration: none;
                      border-radius: 4px;">
              ${values.buttonText}
            </a>
          </div>
        `;
      },
    }),

    exporters: {
      web(values) {
        return `
          <div style="text-align: center; padding: 20px;">
            <img src="${values.productImage.url}" alt="${values.productTitle}" style="max-width: 100%;" />
            <h3 style="color: ${values.titleColor};">${values.productTitle}</h3>
            <p style="font-size: 24px; font-weight: bold;">${values.price}</p>
            <a href="${values.buttonLink.values.href}" target="${values.buttonLink.values.target}"
               style="display: inline-block; background: ${values.buttonBg};
                      color: #fff; padding: 12px 24px; text-decoration: none;">
              ${values.buttonText}
            </a>
          </div>
        `;
      },
      email(values) {
        // Email MUST use tables — divs break in Outlook/Gmail
        return `
          <table width="100%" cellpadding="0" cellspacing="0" border="0">
            <tr><td align="center" style="padding: 20px;">
              <img src="${values.productImage.url}" alt="${values.productTitle}"
                   style="max-width: 100%; display: block;" />
            </td></tr>
            <tr><td align="center" style="padding: 10px;">
              <h3 style="color: ${values.titleColor}; margin: 0;">${values.productTitle}</h3>
            </td></tr>
            <tr><td align="center">
              <p style="font-size: 24px; font-weight: bold; margin: 5px 0;">${values.price}</p>
            </td></tr>
            <tr><td align="center" style="padding: 15px;">
              <table cellpadding="0" cellspacing="0" border="0">
                <tr><td style="background: ${values.buttonBg}; border-radius: 4px;">
                  <a href="${values.buttonLink.values.href}" target="${values.buttonLink.values.target}"
                     style="display: inline-block; color: #fff; padding: 12px 24px;
                            text-decoration: none;">
                    ${values.buttonText}
                  </a>
                </td></tr>
              </table>
            </td></tr>
          </table>
        `;
      },
    },

    head: {
      css(values) {
        return `#${values._meta.htmlID} img { max-width: 100%; height: auto; }`;
      },
      js(values) { return ''; },
    },
  },

  validator(data) {
    const { values, defaultErrors } = data;
    const errors = [];
    if (!values.productTitle) {
      errors.push({
        id: 'PRODUCT_TITLE_REQUIRED',
        icon: 'fa-warning',
        severity: 'ERROR',
        title: 'Missing product title',
        description: 'Product title is required',
      });
    }
    if (!values.productImage?.url) {
      errors.push({
        id: 'PRODUCT_IMAGE_REQUIRED',
        icon: 'fa-warning',
        severity: 'ERROR',
        title: 'Missing product image',
        description: 'Product image is required',
      });
    }
    return [...errors, ...defaultErrors];
  },
});
初期化時に custom# プレフィックスを付けて登録します。
unlayer.init({
  tools: {
    'custom#product_card': {          // REQUIRED: custom# prefix
      data: {
        apiEndpoint: '/api/products', // Custom data accessible in renderer
      },
      properties: {
        // Override default property values or dropdown options
      },
    },
  },
});

ウィジェット値の参照ガイド

レンダラーで各ウィジェットの値を読み取る方法:

ウィジェットデフォルト値render(values) でのアクセス
text'Hello'values.myField → 'Hello'
rich_text'<p>Hello</p>'values.myField → '<p>Hello</p>'
html'<div>...</div>'values.myField → '<div>...</div>'
color_picker'#FF0000'values.myField → '#FF0000'
alignment'center'values.myField → 'center'
font_family{label:'Arial', value:'arial'}values.myField.value → 'arial'
image{url: 'https://...'}values.myField.url → 'https://...'
togglefalsevalues.myField → false
link{name:'web', values:{href,target}}values.myField.values.href → 'https://...'
counter'10'values.myField → '10'(文字列!)
dropdown'option1'values.myField → 'option1'
datetime'2025-01-01'values.myField → '2025-01-01'
border{borderTopWidth:'1px',...}values.myField.borderTopWidth → '1px'

ドロップダウンの選択肢 — ツールのプロパティ設定で unlayer.init() 経由で渡します。

unlayer.init({
  tools: {
    'custom#product_card': {
      properties: {
        department: {
          editor: {
            data: {
              options: [
                { label: 'Sales', value: 'sales' },
                { label: 'Support', value: 'support' },
              ],
            },
          },
        },
      },
    },
  },
});

カスタムプロパティエディター(React)

組み込みウィジェット以外のコントロールを使用する場合:

const RangeSlider = ({ label, value, updateValue, data }) => (
  <div>
    <label>{label}: {value}px</label>
    <input
      type="range"
      min={data.min || 0}
      max={data.max || 100}
      value={parseInt(value)}
      onChange={(e) => updateValue(e.target.value + 'px')}
    />
  </div>
);

unlayer.registerPropertyEditor({
  name: 'range_slider',
  Widget: RangeSlider,
});

// Use in your tool:
borderRadius: {
  label: 'Corner Radius',
  defaultValue: '4px',
  widget: 'range_slider',
  data: { min: 0, max: 50 },
},

バリデーターの戻り値の形式

各エラーには id、icon、severity、title、description を含める必要があります。

validator(data) {
  const { values, defaultErrors } = data;
  const errors = [];

  if (!values.productTitle) {
    errors.push({
      id: 'PRODUCT_TITLE_REQUIRED',     // Unique error ID
      icon: 'fa-warning',                // FontAwesome icon
      severity: 'ERROR',                 // 'ERROR' | 'WARNING'
      title: 'Missing product title',    // Short label
      description: 'Product title is required',  // Detailed message
    });
  }

  if (values.price && !values.price.startsWith('$')) {
    errors.push({
      id: 'PRICE_MISSING_CURRENCY',
      icon: 'fa-dollar-sign',
      severity: 'WARNING',
      title: 'Missing currency symbol',
      description: 'Price should include currency symbol',
      labelPath: 'price',               // Optional — highlights the property in the panel
    });
  }

  return [...errors, ...defaultErrors];  // Merge with built-in errors
}

メールで安全に使える HTML パターン

メールクライアント(Outlook、Gmail)では、テーブルベースの HTML が必要です。以下のパターンをコピーして使用してください。

ボタン:

<table cellpadding="0" cellspacing="0" border="0">
  <tr><td style="background: #007bff; border-radius: 4px;">
    <a href="URL" style="display: inline-block; color: #fff; padding: 12px 24px; text-decoration: none;">
      Button Text
    </a>
  </td></tr>
</table>

2 列:

<table width="100%" cellpadding="0" cellspacing="0">
  <tr>
    <td width="50%" valign="top" style="padding: 10px;">Left</td>
    <td width="50%" valign="top" style="padding: 10px;">Right</td>
  </tr>
</table>

安全な CSS プロパティ: color、background-color、font-size、font-family、font-weight、text-align、padding、margin、border、width、max-width、display: block/inline-block。

安全でないプロパティ(メールでは避けてください): flexbox、grid、position、float、box-shadow、border-radius(一部のみ対応)、calc()。


よくある間違い

間違い修正方法
custom# プレフィックスがない初期化時の tools 設定では、ツール名に custom#my_tool を必ず使用します
div ベースのメールエクスポーターメールエクスポーターは、テーブルベースの HTML を必ず返す必要があります
_meta.htmlID の指定忘れCSS の適用範囲を限定します:#${values._meta.htmlID} { ... }
レンダラーで値をハードコードするvalues オブジェクトを使用し、プロパティエディターでコンテンツを制御します
ドロップダウンの選択肢の形式が間違っているunlayer.init() の tools['custom#name'].properties.prop.editor.data.options で選択肢を渡します

トラブルシューティング

問題修正方法
ツールがエディターに表示されないsupportedDisplayModes に現在のモードが含まれているか確認します
プロパティパネルが空options の構造を確認します。group → options の階層が必要です
カスタムエディターが更新されない新しい値を指定して updateValue() が呼び出されていることを確認します
エクスポートされた HTML の表示が異なるViewer.render() と exporters.email/web() の両方を確認します

リソース

コメント