「記事の途中に、無料相談へのボタンを入れたいんです。CMSでできますか」。ヘッドレスCMSの構成を提案すると、ディレクターからよく返ってくる質問です。リッチテキストが標準で持っているのは見出し・段落・画像・リスト・コードあたり。その外側にあるもの、たとえばCTAボックス、比較表、地図の埋め込み、動くデモ。これをどう入れるかで公開後の運用のラクさが決まります。

自分のサイトのブログには、読者がその場でコードを書き換えて実行できるGSAPのデモを埋め込んでいます。最初は生HTMLで貼ろうとして、見事に失敗しました。動かない理由を追いかけてたどり着いたのが、この記事でまとめる4つの選択肢。先に結論を書くと、上から順に試して、足りないときだけ下に降りるのがいちばん安く済みます。

本文を拡張する4つの選択肢:HTMLブロック、セクション、カスタムブロック、プラグイン

まず試すのは生HTML。ただし script は消える

EmDashのリッチテキストエディタは、スラッシュコマンドで部品を差し込めます。/image で画像、/code でコードブロック、/html で生のHTMLブロック。外部の予約ウィジェットや地図を貼るなら、まずここを試します。

落とし穴は、公開側でサニタイズ(危険なタグを取り除く処理)が走ることです。XSS対策として、iframeは既定で www.youtube.complayer.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月時点の情報です。