unlayer-custom-tools について
Unlayer エディターでドラッグ&ドロップ式のカスタムコンテンツブロックを作るための開発スキルです。レンダリング、HTML 出力、プロパティ設定、メールと Web のモード構成を扱います。
主な機能
- 01レンダラー、エクスポーター、プロパティエディターの設定
- 02カスタムツールの登録と構成
- 03各ウィジェット値の参照方法
- 04React によるカスタムプロパティエディター
- 05メール向けテーブル HTML の指針
活用シーン
- 01商品カードなどのカスタムブロック追加
- 02エディターパネルでのコンテンツ設定の提供
- 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://...' |
toggle | false | values.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() の両方を確認します |