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つを見れば分かります。
- CMSに保存された値を見る。microCMSならAPIプレビューや取得レスポンスで、本文フィールドに
<iframeが残っているか確認する - フロントの描画処理を見る。
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回変換を挑む層を用意しておくと、あとから拡張しやすくなります。