本文にCTAや動くデモを入れる|EmDashでリッチテキストを拡張する4つの方法
記事の途中にCTAボックスや比較表、動くデモを入れたい。ヘッドレスCMSでそれを実現する方法をHTMLブロック・セクション・カスタムブロック・プラグインの4段階に整理し、自サイトでGSAPデモを埋め込んだときに詰まった点も含めて実装者視点でまとめました。

「記事の途中に、無料相談へのボタンを入れたいんです。CMSでできますか」。ヘッドレスCMSの構成を提案すると、ディレクターからよく返ってくる質問です。リッチテキストが標準で持っているのは見出し・段落・画像・リスト・コードあたり。その外側にあるもの、たとえばCTAボックス、比較表、地図の埋め込み、動くデモ。これをどう入れるかで公開後の運用のラクさが決まります。
自分のサイトのブログには、読者がその場でコードを書き換えて実行できるGSAPのデモを埋め込んでいます。最初は生HTMLで貼ろうとして、見事に失敗しました。動かない理由を追いかけてたどり着いたのが、この記事でまとめる4つの選択肢。先に結論を書くと、上から順に試して、足りないときだけ下に降りるのがいちばん安く済みます。

まず試すのは生HTML。ただし script は消える
EmDashのリッチテキストエディタは、スラッシュコマンドで部品を差し込めます。/image で画像、/code でコードブロック、/html で生のHTMLブロック。外部の予約ウィジェットや地図を貼るなら、まずここを試します。
落とし穴は、公開側でサニタイズ(危険なタグを取り除く処理)が走ることです。XSS対策として、iframeは既定で www.youtube.com と player.vimeo.com からのみ許可され、それ以外のホストは取り除かれます。自サイトで確かめたところ、<script> も同じく落ちました。生HTMLブロックにJavaScriptを書いても動きません。
許可するiframeのホストを足したいときは、htmlBlockの描画コンポーネントを自前のものに差し替えます。
<PortableText
value={post.data.content}
components={{ type: { htmlBlock: MyHtmlBlock } }}
/>差し替えたコンポーネントの中で、サニタイズの許可ホスト一覧に必要なものを足す、という流れ。許可リストを広げる作業なので、追加するホストは案件ごとに絞ります。何でも通す設定にすると、静的サイトを選んだ意味が薄れてしまう。
向いているのは、その記事にしか出てこない一度きりの埋め込みです。同じものを何度も入れるなら、次の選択肢へ。
何度も使う定型は「セクション」で配る
EmDashには再利用ブロックの仕組み(セクション)があります。編集画面で /section と打つと一覧が出て、選ぶとその中身が本文に差し込まれる。CTA、注意書き、著者プロフィールのような定型に向いています。
押さえておく仕様がひとつ。挿入されるのは中身のコピーで、あとからセクション側を直しても、すでに挿入済みの記事は変わりません。編集者が挿入後に自由へ手を入れられる利点の裏返しです。全ページで同期させたい文言(規約の抜粋など)をセクションで配ると、変更のたびに全記事を開くはめになります。同期が要るならウィジェットエリアを使う、と切り分けます。
制作会社の立場だと、ここがいちばん費用対効果の高い層だと感じています。実装側の作業は「よく使う体裁をセクションとして登録しておく」だけ。あとはクライアントが自分で入れられます。
デザインが決まった部品は、テーマ側のカスタムブロックにする
体裁を崩されたくない。あるいはJavaScriptを動かしたい。そうなるとカスタムブロックの出番です。本文のPortable Textに名前空間つきの _type を持つブロックを置き、対応するAstroコンポーネントで描画します。
自サイトのGSAPデモがこれで、本文には次のようなブロックが入っています。
{ "_type": "tsudzuri.gsapDemo", "demo": "basic", "title": "基本のto()" }配線で詰まった点を、実装者の備忘として3つ。
- 受け取りは type(単数形)。
components={{ type: { "tsudzuri.gsapDemo": GsapDemo } }}と書き、ブロック側はnodeプロパティで値を受け取れました。ドキュメントにはテーマ向けの説明としてtypes(複数形)にvalueで受ける例もあり、最初はそちらを写して描画されず止まりました。 - sandbox="allow-scripts" を付けたiframeはオリジンが不透明になり、<script src="..."> で外部ファイルを読み込めません。読者のコードを隔離するためsandboxは外せないので、GSAP本体は親ページで一度だけ取得し、
srcdocにインラインで流し込む形にしました。 - 読者が書いたコードは独立した <script> に置く。try/catchと同じブロックにまとめると、構文エラーのときブロックごと解析されず、エラーを掴めません。
動きを入れるなら、prefers-reduced-motion(動きを減らす設定)が有効なときは自動再生しない、も忘れずに。デモの中身そのものはGSAPの使い方の記事に書きました。
注意点はひとつ。カスタムブロックは既定では管理画面の編集UIを持ちません。つまり本文に入れるのは実装者の作業になります。クライアントが日常的に入れるなら、次の段へ。

クライアントが自分で入れるなら、プラグインで編集UIごと用意する
プラグインはPortable Textのブロック型を宣言でき、入力フィールドを付ければエディタ側にフォームが出ます。スラッシュコマンドのメニューにラベルとアイコンで並ぶので、編集者から見ると標準機能と区別がつきません。
公開側の描画コンポーネントまでプラグインで配る場合は、npmパッケージをビルド時に読み込むネイティブ形式が必要です。編集UIだけならサンドボックス形式のままで済みます。手間は4つの中でいちばん大きいので、「複数サイトに同じ部品を配る」「クライアントが毎週のように入れる」のどちらかに当てはまるときだけ選びます。
判断軸は「誰が入れるか」と「何回入れるか」
- 一度きり・外部サービスのiframe → HTMLブロック
- 同じ体裁を何度も・編集者が自分で入れる → セクション
- デザインを固定したい・JavaScriptが要る → テーマのカスタムブロック
- クライアントが日常的に入れる・複数サイトへ配る → プラグイン
見積もりの段階でここを詰めておくと、公開後の「料金表ブロックを追加したい」に慌てずに済みます。更新しても崩れないフィールド設計とセットで考えるのがおすすめです。
AstroとヘッドレスCMSで本文に独自の部品を入れたい、既存サイトの記事内に動く要素を足したい。そうした実装はお問い合わせからご相談ください。要件をうかがって、上の4つのどれで足りるかから一緒に決めます。
本記事は2026年8月時点の情報です。