CodePenの埋め込みコードをmicroCMSのリッチエディタに入れて保存し、公開した。でも記事には何も出てこない。

コンソールにエラーはない。Next.js側のコードも見直したが、HTMLをそのまま出しているだけで、フィルタをかける処理はどこにもない。

犯人は microCMSのリッチエディタ でした。保存する段階で <iframe> が別のものに書き換えられています。

この記事でわかること

  • microCMSのリッチエディタに <iframe> を入れると実際には何が起きるのか
  • CMS側を弄らずに埋め込みを実現する「プレースホルダ方式」の実装
  • この方式がセキュリティ面でも有利になる理由

結論:HTMLではなく「合言葉」を保存する

埋め込みタグをCMSに持たせるのをやめるのが正解でした。

CMSには [[codepen:ユーザー名/スラッグ]] というただの文字列を保存します。タグではないので、エディタのサニタイズ(危険なタグを削除・変換する処理)の対象になりません。

そしてNext.js側で、表示の直前にこの文字列を <iframe> に戻します。

CMSに保存   [[codepen:ユーザー名/スラッグ]]   ← ただの文字列
     ↓
Next.js       <iframe src="...">              ← 表示直前に復元

この役割分担にしたことで、エディタの仕様に振り回されなくなりました。

実際には「消えていた」のではなく「リンクになっていた」

最初は完全に削除されていると思っていましたが、検証すると違いました。

投入したのはこのHTMLです。

<iframe src="https://codepen.io/yabi-labs/embed/wBgQMLv?default-tab=css%2Cresult"
  height="520" style="width:100%" loading="lazy" allowfullscreen></iframe>

公開後のHTMLを見ると、こうなっていました。

<a href="https://codepen.io/yabi-labs/embed/wBgQMLv?default-tab=css%2Cresult">https://codepen.io/yabi-labs/embed/wBgQMLv?default-tab=css%2Cresult</a>

つまり <iframe>src だけが生き残り、タグ自体は <a> に置き換えられていたわけです。

気づきにくいのは、サイトの見た目では「何もない」に見えるケースがあることです。リンクのスタイル次第では、本文に埋もれて見逃します。

なおこれは2026年7月時点の挙動です。エディタのバージョンや設定によって変わる可能性はあります。

まず「どこで消えたか」を切り分ける

同じ問題にぶつかったときは、犯人を特定するのが先です。次の2つを見れば分かります。

  1. CMSに保存された値を見る。microCMSならAPIプレビューや取得レスポンスで、本文フィールドに <iframe が残っているか確認する
  2. フロントの描画処理を見る。dangerouslySetInnerHTML に渡す前に、DOMPurifyなどのサニタイザーをかませていないか確認する

1の時点で消えていればCMS側、残っていればフロント側の問題です。今回は前者でした。

実装:復元関数を1つ作る

必要なのは、プレースホルダを探してiframeに置き換える関数だけです。

src/lib/content/embeds.ts を作ります。

const CODEPEN_RE = /(?:<p>\s*)?\[\[codepen:([\w-]+)\/([\w-]+)\]\](?:\s*<\/p>)?/g;

export function convertEmbeds(html: string): string {
  return html.replace(CODEPEN_RE, (_m, user, slug) => {
    const src = `https://codepen.io/${user}/embed/${slug}?default-tab=css%2Cresult`;
    return `<iframe class="codepen-embed" height="520" style="width:100%;border:0;" loading="lazy" allowfullscreen src="${src}" title="CodePen ${user}/${slug}"></iframe>`;
  });
}

正規表現の先頭と末尾で <p> を吸収しているのがポイントです。リッチエディタは文字列を段落として保存するため、<p>[[codepen:...]]</p> の形で届きます。これを残すと <p> の中にiframeが入り、余分な余白が出ます。

あとは描画コンポーネントで通すだけです。

import { convertEmbeds } from "@/lib/content/embeds";

export function ArticleBody({ html }: { html: string }) {
  return (
    <div
      className="article-body"
      dangerouslySetInnerHTML={{ __html: convertEmbeds(html) }}
    />
  );
}

これで完了です。CMSの設定もAPIスキーマも変更していません。

手書きでも自動化でも同じ記法が使える

この方式の地味な利点は、入力経路を問わないことです。

管理画面で記事を書くときは、本文に [[codepen:ユーザー名/スラッグ]] と打つだけです。HTMLを意識せずに済みます。

自動投稿を組んでいる場合も同じです。手元ではNotionに貼ったCodePenのURLを、変換処理でこの記法に置き換えてからCMSへ送っています。

応用:埋め込み先を増やす

記法を増やせば、同じ仕組みでYouTubeやXにも拡張できます。

const YOUTUBE_RE = /(?:<p>\s*)?\[\[youtube:([\w-]+)\]\](?:\s*<\/p>)?/g;

html = html.replace(YOUTUBE_RE, (_m, id) =>
  `<iframe class="video-embed" src="https://www.youtube-nocookie.com/embed/${id}" loading="lazy" allowfullscreen title="YouTube"></iframe>`
);

ここで大事なのは、埋め込める先をコード側で限定できるという点です。

任意のHTMLをCMSに保存できる状態だと、意図しないタグや外部スクリプトが混ざる余地ができます。プレースホルダ方式なら、出すタグはこちらが書いたものだけになります。

つまり、エディタがiframeを消す仕様は、不便に見えてセキュリティ上は合理的とも言えます。回避策を入れるときも、その方針を壊さない形にするのが安全です。

注意点

導入前に知っておきたいことが3つあります。

  • 既存記事は自動では直らない。すでにリンク化されて保存された分は、手で記法に書き換える必要があります
  • プレビューでは未変換のまま見える場合がある。変換をかけていない経路があれば、[[codepen:...]] がそのまま表示されます
  • レイアウトシフト対策に高さを指定する。iframeは読み込み完了まで高さが確定しないため、CSSで固定高さか aspect-ratio を与えておくと表示が安定します

まとめ

ヘッドレスCMSを使っていると、「管理画面では入れたのに表示されない」という他人の仕様にぶつかる場面があります。

そこでエディタをなだめすよりも、CMSには壊れない形で保存し、表示側で組み立てると割り切ったほうが早くて壊れにくい、というのが今回の学びでした。

同じ発想は、シンタックスハイライトやカスタムコンポーネントの埋め込みにもそのまま使えます。表示直前に1回変換を挑む層を用意しておくと、あとから拡張しやすくなります。