📗

microCMSの拡張フィールドが使いやすくなった件について

に公開

はじめに

最近(といっても 1 ヶ月前くらいですが)microCMS では拡張フィールドで iframe 要素に対して送信される値が追加されました
https://blog.microcms.io/iframe-change-postmessage/

この追加で、今まで拡張フィールドではできなかったことや、回り道をすればなんとか実装できてたような機能がサクッと作れるようになりました!

それでは早速、どんな値が追加されたかみていきます

何が変わったのか

従来の問題点

いままでの拡張フィールドでは、iframe 要素側で「どのコンテンツ/API から呼び出されているのか」を把握することができませんでした

例えば、同じ拡張フィールドを複数の API で使い回している場合、iframe 側では以下のような情報が分からない状態でした

  • どの API エンドポイントから呼び出されているか
  • 新規作成なのか編集なのか
  • 編集の場合、どのコンテンツ ID なのか

今回の変更内容

2025 年 5 月 26 日のアップデートで、MICROCMS_GET_DEFAULT_DATAとして送信される初期データに context オブジェクトが追加されました

{
    "id": "some-id",
    "action": "MICROCMS_GET_DEFAULT_DATA",
    "user": {
        "email": "example@microcms.co.jp"
    },
    "message": {
        "data": "iframeでデータを登録しています"
    },
    "context": {
        // ここが今回追加された値
        "type": "new_content",
        "endpoint": "some-api",
        "content": {
            "id": null
        }
    }
}

context オブジェクトの中身には、 type, endpoint, content.id が入ります

詳細については公式の記事を読んでいただくのが良いかと思います
https://blog.microcms.io/iframe-change-postmessage/#he7d36798df

試してみる

今回の仕様変更された値を使って記事のプレビューを表示する拡張フィールドを作ってみます

作成する機能

iframe で記事の ID と記事 API が取得できるようになったということで、書いている記事のプレビューをする拡張フィールドを作ってみたいと思います

ただし、新規作成時はそもそも記事 ID が null なので、その場合はプレビューできないよ〜という表示をします

また、記事を下書き保存または公開ボタンを押して更新しない限りプレビューの中身も変わりません(リアルタイムでプレビューするのは無理)

今回実用性というよりはあくまで追加された値を使ってどんなことができるかを試すのが目的なので、その点はご了承ください

実装

それでは実装してみます
環境構築するのは面倒なので、全て 1 つの HTML で作ってしまいます

拡張フィールド(HTML)

長いので折りたたんでます
index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>記事プレビュー</title>
        <style>
            body {
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                margin: 0;
                padding: 20px;
                background-color: #f8f9fa;
            }
            .preview-container {
                background: white;
                border-radius: 8px;
                padding: 24px;
                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            }
            .preview-header {
                border-bottom: 1px solid #e9ecef;
                padding-bottom: 16px;
                margin-bottom: 24px;
            }
            .preview-title {
                font-size: 24px;
                font-weight: bold;
                color: #212529;
                margin: 0;
            }
            .preview-meta {
                color: #6c757d;
                font-size: 14px;
                margin-top: 8px;
            }
            .preview-content {
                line-height: 1.6;
                color: #495057;
            }
            .preview-content img {
                width: 100%;
                height: auto;
                border-radius: 4px;
            }
            .no-preview {
                text-align: center;
                color: #6c757d;
                padding: 40px 20px;
            }
            .loading {
                text-align: center;
                padding: 40px 20px;
                color: #6c757d;
            }
            .error {
                background-color: #f8d7da;
                color: #721c24;
                padding: 16px;
                border-radius: 4px;
                margin-bottom: 16px;
            }
        </style>
    </head>
    <body>
        <div id="app">
            <div class="loading">読み込み中...</div>
        </div>

        <script>
            class ArticlePreview {
                constructor() {
                    this.iframeId = null;
                    this.context = null;
                    this.apiKey = "YOUR_API_KEY"; // 実際のAPIキーに置き換えてください
                    this.serviceDomain = "YOUR_SERVICE_DOMAIN"; // 実際のサービスドメインに置き換えてください
                    this.height = 500;

                    this.init();
                }

                init() {
                    window.addEventListener("message", (e) => {
                        if (e.isTrusted === true && e.data.action === "MICROCMS_GET_DEFAULT_DATA") {
                            this.iframeId = e.data.id;
                            this.context = e.data.context;

                            this.setHeight();
                            this.handleInitialData();
                        }
                    });
                }

                async setHeight() {
                    // ウィンドウの高さを指定
                    window.parent.postMessage(
                        {
                            action: "MICROCMS_UPDATE_STYLE",
                            id: this.iframeId,
                            message: {
                                width: "100%",
                                height: this.height,
                            },
                        },
                        `https://${this.serviceDomain}.microcms.io`
                    );
                }

                async handleInitialData() {
                    const app = document.getElementById("app");

                    // 新規作成の場合
                    if (this.context.type === "new_content") {
                        app.innerHTML = `
                        <div class="preview-container">
                            <div class="no-preview">
                                <h3>プレビューはまだ利用できません</h3>
                                <p>記事を保存してから編集画面でプレビューをご確認ください</p>
                            </div>
                        </div>
                    `;
                        return;
                    }

                    // 編集の場合、記事データを取得してプレビューを表示
                    if (this.context.type === "edit_content" && this.context.content.id) {
                        try {
                            const article = await this.fetchArticle();
                            this.renderPreview(article);
                        } catch (error) {
                            this.renderError(error.message);
                        }
                    }
                }

                async fetchArticle() {
                    const response = await fetch(
                        `https://${this.serviceDomain}.microcms.io/api/v1/${this.context.endpoint}/${this.context.content.id}`,
                        {
                            headers: {
                                "X-MICROCMS-API-KEY": this.apiKey,
                            },
                        }
                    );

                    if (!response.ok) {
                        throw new Error("記事の取得に失敗しました");
                    }

                    return await response.json();
                }

                renderPreview(article) {
                    const app = document.getElementById("app");

                    app.innerHTML = `
                    <div class="preview-container">
                        <div class="preview-header">
                            <h1 class="preview-title">${article.title || "タイトルなし"}</h1>
                            <div class="preview-meta">
                                API: ${this.context.endpoint} |
                                作成日: ${new Date(article.createdAt).toLocaleDateString("ja-JP")} |
                                更新日: ${new Date(article.updatedAt).toLocaleDateString("ja-JP")}
                            </div>
                        </div>
                        <div class="preview-content">
                            ${this.renderContent(article)}
                        </div>
                    </div>
                `;
                }

                renderContent(article) {
                    let content = "";

                    // アイキャッチ画像の表示
                    if (article.eyecatch) {
                        content += `
                        <div style="margin-bottom: 24px;">
                            <img src="${article.eyecatch.url}" alt="${article.title}"
                                 style="width: 100%; max-width: 600px; height: auto; border-radius: 4px;">
                        </div>
                    `;
                    }

                    // カテゴリの表示
                    if (article.category) {
                        content += `
                        <div style="margin-bottom: 16px;">
                            <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">
                                ${article.category.name}
                            </span>
                        </div>
                    `;
                    }

                    // 本文の表示
                    if (article.content) {
                        content += `<div>${article.content}</div>`;
                    } else {
                        content += "<p>本文がありません</p>";
                    }

                    return content;
                }

                renderError(message) {
                    const app = document.getElementById("app");
                    app.innerHTML = `
                    <div class="preview-container">
                        <div class="error">
                            エラー: ${message}
                        </div>
                        <div class="no-preview">
                            <p>プレビューの表示に失敗しました</p>
                        </div>
                    </div>
                `;
                }
            }

            // アプリケーションを初期化
            new ArticlePreview();
        </script>
    </body>
</html>

実装結果

こんな感じで、microCMS の記事入稿画面で記事を表示できるようになりました

一度も記事を下書き or 公開していない場合はエラーが表示されます

解説

少しだけコードの解説をしていきます

type オブジェクトの利用

microCMS から渡された拡張フィールドの context の値は this.context に入っているので、それを読んで新規か編集かを判別する処理を handleInitialData に記述しています

async handleInitialData() {
    const app = document.getElementById("app");

    // 新規作成の場合
    if (this.context.type === "new_content") {
        app.innerHTML = `
        <div class="preview-container">
            <div class="no-preview">
                <h3>プレビューはまだ利用できません</h3>
                <p>記事を保存してから編集画面でプレビューをご確認ください</p>
            </div>
        </div>
    `;
        return;
    }

    // 編集の場合、記事データを取得してプレビューを表示
    if (this.context.type === "edit_content" && this.context.content.id) {
        try {
            const article = await this.fetchArticle();
            this.renderPreview(article);
        } catch (error) {
            this.renderError(error.message);
        }
    }
}

なぜ判別するかというところですが、新規記事では記事 ID を持たずプレビューを利用できないためです

そのため、context の type が new_content の場合はプレビューを表示し、edit_content の場合はすでに記事 ID が存在しているので記事データを取得してプレビューを表示するという処理をしています

記事データの取得

編集の場合は記事 ID が存在するので、記事データを取得してプレビューを表示するための処理を fetchArticle 関数として記述しています

async fetchArticle() {
    const response = await fetch(
        `https://${this.serviceDomain}.microcms.io/api/v1/${this.context.endpoint}/${this.context.content.id}`,
        {
            headers: {
                "X-MICROCMS-API-KEY": this.apiKey,
            },
        }
    );

    if (!response.ok) {
        throw new Error("記事の取得に失敗しました");
    }

    return await response.json();
}

記事自体は fetch で取得して、response の json を返しています

やっぱりコンテンツ ID とエンドポイントが context から取得できるようになったので、拡張フィールドから一発で記事を取得できるようになったのが大きいですね

以前は拡張フィールドから表示されている記事の中身を取得するために、エンドポイントをハードコーディングした上で拡張フィールドにユニークな値を設定して microCMS に値を返して、その値を元に記事の中身を取得していました

記事一覧を取得して、その中の記事ひとつひとつから拡張フィールドの値を読んで一致するものがあればその記事を取得して……というめんどくさい処理をしていたので、本当に助かります

おわりに

今回の microCMS の仕様変更で、拡張フィールドがより便利に使えるようになりましたね〜

特に、以前は API のエンドポイントを拡張フィールドからは判別できずに実装ができなかった(もしくは中身が全く一緒でエンドポイント分だけ拡張フィールドを用意してた)ようなものに関しては、今回の仕様変更でだいぶ恩恵を受けられるようになったのではないでしょうか

拡張フィールドを有効活用して楽しい microCMS ライフを!

Discussion