カナエテの管理画面には、コラムやお知らせを編集できる CMS があります。今回は、そこに Editor.js を導入して、商品を記事に埋め込めるカスタムツールを作った話です。
エディタ選定で悩んだ話
カナエテにはコラム記事やお知らせを編集する機能があります。最初は TinyMCE や CKEditor を検討していました。正直どれでもいいかなと思っていたのですが、一つだけ厄介な要件がありました。
「記事の中に商品カードを埋め込みたい」
これが意外と難しい。TinyMCE だと、HTML を直接書くか、頑張ってプラグインを作るかしないといけません。それはちょっと辛い。
調べているうちに Editor.js を見つけました。
なぜ Editor.js だったのか
Editor.js は Notion や WordPress の Gutenberg のような「ブロックエディタ」と呼ばれるタイプのエディタです。コンテンツをブロック単位で管理します。
採用を決めた理由は3つあります。
JSON で保存できる
普通のリッチテキストエディタは HTML を出力しますが、Editor.js は JSON で出力します。これが地味に嬉しくて、サーバー側で加工しやすいですし、フロントでの表示方法も自由に変えられます。将来アプリを作るときも同じデータが使えます。
プラグインで拡張できる
見出し、リスト、画像といった基本機能は公式プラグインで提供されています。さらに、自分でカスタムツールも作れます。これなら商品埋め込み機能も実装できそうでした。
ドキュメントが整備されている
TypeScript の型定義もありますし、Vue との連携例も見つかりました。学習コストはそこまで高くなさそうでした。
使ったプラグイン
結局こんな構成になりました。
- @editorjs/header(見出し)
- @editorjs/list(箇条書き)
- @editorjs/image(画像)
- @editorjs/raw(HTML直書き用。たまに必要)
- ProductPickerTool(自作の商品埋め込みツール)
公式プラグインだけでも記事は書けますが、やはり商品埋め込みがないと意味がありません。
ProductPickerTool を作った

商品を記事に埋め込むためのカスタムツールです。エディタ上で商品を検索して、選択すると記事にカードが挿入されます。
作るときに考えたことをいくつか紹介します。
検索のタイミング
1文字入力するたびに API を叩くのはさすがにまずい。250ms のデバウンスを入れて、入力が止まってから検索するようにしました。
また、商品リストをあらかじめフロントに渡しておき、まずローカルで絞り込む設計にしています。商品数が増えてきたら API 検索に切り替える想定です。
保存するのは商品 ID だけ
最初は商品名や価格も一緒に保存しようと思っていました。でもよく考えると、商品情報は更新されます。価格改定や画像差し替えなど。記事保存時点の情報を固定してしまうと、古い情報がずっと表示されることになります。
なので、保存するのは商品 ID だけにしました。表示するときに最新の情報を取得します。
編集中のプレビュー
商品を選択したら、その場でプレビューが見えるようにしました。商品画像、名前、価格、カテゴリが表示されます。実際にどう見えるか確認しながら編集できます。
フロントでの表示
Editor.js の JSON を HTML に変換するには editorjs-html を使いました。ブロックタイプごとにパーサーを設定できます。
ただし、商品ブロックには商品 ID しか入っていません。表示するには商品の詳細情報が必要です。
これはサーバー側で解決しました。記事を表示するとき、まず JSON から商品 ID を抽出して、その商品情報を取得します。「商品ID → 商品情報」のマップをフロントに渡し、フロントのパーサーはこのマップを参照して商品カードの HTML を組み立てます。
「毎回取得するのは非効率では?」と思うかもしれません。でもこの方式なら常に最新情報を出せますし、サーバー側でキャッシュすればパフォーマンスも問題ありません。記事データ自体も軽くなります。
サーバー側の処理
ProductPickerTool のブロックデータはサニタイズしています。フロントから余計なデータが来ても、必要なフィールドだけ残して保存します。
また、記事表示時に商品 ID を抽出するメソッドも用意しました。JSON のブロック配列をループして、productPicker タイプのブロックから productId を集め、重複を除いて返します。
やってみて
Editor.js は思ったより使いやすかったです。カスタムツールの仕組みがシンプルで、TypeScript で書けるのも良い。
商品埋め込み機能を作れたのは大きいです。EC サイトの CMS として、これがないと結局「商品ページの URL をコピペしてリンク貼って…」みたいなことになります。
JSON で保存する設計は、最初は「HTML のほうが楽じゃない?」と思いましたが、カスタムブロックを増やしていくことを考えると正解だったと思います。