microCMSブログ

microCMSの最新情報や活用事例などをお届けします。

エンジニアリング

microCMSとNext.jsで構築したサイトに、WebhookとOpenAI APIによる翻訳フローを実装する

microCMSとNext.jsで構築したサイトに、WebhookとOpenAI APIによる翻訳フローを実装する
高宮 竜太 高宮 竜太

こんにちは。microCMSでカスタマーエンジニアをしている高宮です。

microCMSでは、公式ドキュメントの英語化に取り組みました。

今回は外部の翻訳サービスを利用するのではなく、microCMSのWebhookとAPI、OpenAI APIを組み合わせて、日本語版コンテンツの更新を起点に英語版コンテンツを生成・更新するフローを実装しています。

この記事では、公式ドキュメントで利用しているNext.jsのRoute HandlersをWebhookの受け口とし、Vercel Functions上で翻訳処理を実行する構成について、実装の流れや運用時に考慮したポイントを紹介します。

前提

この記事では、以下のような構成を前提にしています。

  • 日本語版と英語版でmicroCMSのAPIを分ける
  • 英語版は日本語版の更新に追従して作成・更新する
  • フロントエンドはNext.jsで実装する
  • Webhookの受け口にはVercel Functionsを使う(Next.jsのRoute Handlersで実装)
  • 翻訳にはOpenAI API(gpt-4.1-mini)を使う

今回作る仕組み

今回の構成では、日本語版のコンテンツ更新をきっかけに、英語版コンテンツを自動で作成・更新します。処理全体の流れは以下の図のとおりです。

処理全体をコードにすると、たとえば以下のようなイメージです。実際のAPIキーやエラーハンドリングは省略していますが、Webhookを受け取ってから英語版APIへ登録・更新するまでの流れをまとめています。

app/api/translate/route.js
import { createClient } from "microcms-js-sdk";
import OpenAI from "openai";

const client = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
  apiKey: process.env.MICROCMS_TRANSLATE_API_KEY,
});

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export async function POST(request) {
  const { api, id } = await request.json();

  // 日本語版コンテンツを取得する
  const news = await client.getListDetail({
    endpoint: api,
    contentId: id,
  });

  // 翻訳対象だけをOpenAI APIに渡す
  const response = await openai.responses.create({
    model: "gpt-4.1-mini",
    input: [
      {
        role: "system",
        content: `
          Translate the news content from Japanese into English.
          Keep HTML tags, attributes, URLs, and code blocks unchanged.
          Return JSON with title, description, and content.
        `,
      },
      {
        role: "user",
        content: JSON.stringify({
          title: news.title,
          description: news.description,
          content: news.content,
        }),
      },
    ],
    text: {
      format: { type: "json_object" },
    },
  });

  const translated = JSON.parse(response.output_text);

  // news-enに登録する内容を作る
  const newsEnContent = {
    title: translated.title,
    description: translated.description,
    content: translated.content,
    category: news.category.id,
    thumbnail: news.thumbnail.url,
  };

  // 登録先の英語版APIを決める(例: news → news-en)
  const translatedEndpoint = `${api}-en`;

  // 同じIDの英語版コンテンツがあれば更新、なければ作成する
  const exists = await client
    .getListDetail({
      endpoint: translatedEndpoint,
      contentId: id,
    })
    .then(() => true)
    .catch(() => false);

  if (exists) {
    await client.update({
      endpoint: translatedEndpoint,
      contentId: id,
      content: newsEnContent,
      isDraft: true,
    });
  } else {
    await client.create({
      endpoint: translatedEndpoint,
      contentId: id,
      content: newsEnContent,
      isDraft: true,
    });
  }

  return Response.json({ ok: true });
}

実装手順

今回はテンプレートの「シンプルなコーポレートサイト」のスキーマ構成を使って実装します。

ステップ1: 英語版APIを用意する

まず、翻訳結果を登録するための英語版APIを用意します。

今回の構成では、日本語版APIとは別に英語版APIを作成し、翻訳済みのコンテンツを英語版APIに保存する形にしました。たとえば、日本語版のエンドポイントが news であれば、英語版は news-en のように、対応関係がわかりやすいエンドポイント名にします。

フィールドの種類は、日本語版APIと英語版APIで基本的に同じにします。microCMSにはAPIスキーマのインポート・エクスポート機能があるため、日本語版APIのスキーマをエクスポートし、英語版API側にインポートすることで、同じ構成のAPIを比較的簡単に用意できます。

ステップ2: Webhookの受け口を用意する

次に、microCMSのWebhookを受け取るためのエンドポイントを用意します。
今回はNext.jsのRoute Handlersを使い、Vercel Functions上でWebhookを受け取る構成にしました。

microCMS管理画面でのWebhook設定手順は、公式ドキュメントを参照してください。この記事では、Webhookの送信先として指定するNext.js側の実装を中心に説明します。

Webhookのリクエストボディにはapiidが含まれるため、どのAPIのどのコンテンツが更新されたかを判定できます。たとえばapinewsidnews01 であれば、後続の処理ではnews-enのエンドポイントにコンテンツIDnews01を作成・更新します。

app/api/translate/route.js
export async function POST(request) {
  const { api, id } = await request.json();

  // apiとidを使って、以降の処理を進める
}

ステップ3: 日本語コンテンツを取得する

Webhookのペイロードから取り出したapiid をもとに、翻訳元となる最新の日本語コンテンツをmicroCMSのAPIから取得します。

app/api/translate/route.js
const news = await client.getListDetail({
  endpoint: api,
  contentId: id,
});

GET APIで最新のコンテンツを取得することで、本文や参照フィールドなど確定したデータをもとに、以降の抽出・翻訳・整形処理を確実に行えます。

ステップ4: 翻訳対象フィールドを抽出する

次に、取得したコンテンツから翻訳対象のフィールドだけを抽出します。

microCMSのAPIには、タイトル、本文、説明文、関連コンテンツ、画像、カスタムフィールドなど、さまざまな型のフィールドが含まれます。すべてをそのままAIに渡すのではなく、翻訳すべきテキストと、翻訳してはいけない値を分けて扱います。

app/api/translate/route.js
const newsTranslationFields = {
  title: news.title,
  description: news.description,
  content: news.content,
};

たとえば、以下のような値は基本的に翻訳対象に含めません。

  • 画像URL
  • コンテンツID
  • コンテンツ参照先のコンテンツID
  • true / falseなどのフラグ
  • API側で決められた固定値

これらを翻訳対象に含めてしまうと、英語版APIへの登録時に不正な値になる可能性があります。

モデルにも左右されますが、シンプルな構成のスキーマであればレスポンスをそのまま渡してもAIが解釈してくれます。ただしスキーマが複雑になると意図しない翻訳結果が返却されたためにこのような対応をしています。

ステップ5: プロンプトを用意する

前のステップで抽出した値を、OpenAI APIに渡す形に整えます。

基本的にはそのまま渡しても問題ありませんが、microCMSのリッチエディタのレスポンスではHTML文字列として取得されます。HTML文字列には本文だけでなく、タグ、属性、リンク、コードブロックなども含まれます。翻訳時にこれらの構造が変わると、表示崩れやリンク切れにつながる可能性があります。

そのため、OpenAI APIに渡すプロンプトでは、翻訳してよいテキストと維持すべきHTML構造を明確にします。

app/api/translate/route.js
const prompt = `
 Translate Japanese content into English.
 Keep HTML tags, attributes, URLs, and code blocks unchanged.
 Return JSON with title, description, and content.
`;

ステップ6: OpenAI APIで翻訳する

これまでに抽出した翻訳用データ(ステップ4)とプロンプト(ステップ5)をOpenAI APIに渡し、翻訳結果を取得します。

このとき、text.formatjson_object を指定して、レスポンスを素のJSONに固定します。

app/api/translate/route.js
const response = await openai.responses.create({
  model: "gpt-4.1-mini",
  input: [
    { role: "system", content: prompt },
    { role: "user", content: JSON.stringify(newsTranslationFields) },
  ],
  text: {
    format: { type: "json_object" },
  },
});

json_object を使う場合は、プロンプトのどこかに「JSON」という語を含めておく必要があります。これはOpenAIの公式仕様で、含まれていないとAPIがエラーを返します。今回はステップ5のプロンプトで出力形式をJSONと明記しています(参考: JSON mode)。

なお json_object が保証するのはJSONとして構文的に妥当であることだけで、キー構造までは強制されません。titledescriptioncontent といったキーを確実に揃えたい場合は、json_schema でスキーマを指定するStructured Outputsを使う方法もあります。

実際の運用では、プロンプトに以下のような情報も含めると安定しやすくなります。

  • 用語集
  • 翻訳対象外のルール
  • 内部リンクの扱い
  • 管理画面上の表記ルール
  • 文章トーン

ステップ7: 英語版APIのスキーマに合わせて整形する

OpenAI APIから返ってきた翻訳結果を、そのまま英語版APIに登録できるとは限りません。

仮に日本語版と英語版でフィールド構成が完全に一致していない場合(旧リッチエディタ → リッチエディタなど)は英語版APIのスキーマに合わせて変換する必要があります。

app/api/translate/route.js
const newsEnContent = {
  title: translated.title,
  description: translated.description,
  content: translated.content,
  category: news.category.id,
  thumbnail: news.thumbnail.url,
};

newsnews-en は同じフィールド構成にしているため、翻訳結果は titledescriptioncontent に戻し、翻訳しない categorythumbnail は元の日本語版コンテンツから引き継ぎます。

また、今回は日本語版と英語版で同じコンテンツIDを使う前提にしています。そのため、news/{id} に対応する英語版として news-en/{id} を作成・更新します。

ステップ8: 英語版コンテンツを登録・更新する

整形した英語版のペイロードができたら、登録先の英語版APIを決めて、microCMSのAPIで英語版コンテンツを登録・更新します。

登録先のエンドポイントは、日本語版のAPI ID(api)に -en を付けて組み立てます。今回は api が常に news なので news-en になります。

app/api/translate/route.js
const translatedEndpoint = `${api}-en`; // 例: news → news-en

あとは、同じコンテンツIDの英語版がすでに存在するかを確認し、初回は作成(POST)、既存の場合は更新(PATCH)に分岐します。

app/api/translate/route.js
const exists = await client
  .getListDetail({
    endpoint: translatedEndpoint,
    contentId: id,
  })
  .then(() => true)
  .catch(() => false);

if (exists) {
  await client.update({
    endpoint: translatedEndpoint,
    contentId: id,
    content: newsEnContent,
    isDraft: true,
  });
} else {
  await client.create({
    endpoint: translatedEndpoint,
    contentId: id,
    content: newsEnContent,
    isDraft: true,
  });
}

このとき、英語版をすぐに公開するか、下書きとして保存するかは運用方針によって変わります。今回のコードでは isDraft: true を指定し、英語版をいったん下書きとして登録しています。

自動翻訳の結果をそのまま公開する(自動公開)ことも可能ですが、誤訳や固有名詞の誤り、内部リンクのミスがそのまま表に出てしまう点には注意が必要です。

ステップ9: Next.js側で英語版APIを参照する

英語版コンテンツを news-en のような別APIに保存したら、フロントエンド側では表示する言語に応じて参照するAPIを切り替えます。

多言語ルーティングには next-intl を使いました。URLのロケール(/ja/en)をnext-intlが解決し、そのロケールをもとに参照するmicroCMSのAPIを切り替える、という流れです。

next-intlのセットアップ(ロケール定義やミドルウェアの設定)は公式ドキュメントを参照してください。

たとえば、URLやlocaleから現在の言語を判定し、取得先のエンドポイントを切り替えます。

app/[locale]/news/[contentId]/page.tsx
// app/[locale]/news/[contentId]/page.tsx
import { createClient } from "microcms-js-sdk";
import { Article } from "..";

// microCMSのクライアントを作成する
const client = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
  apiKey: process.env.MICROCMS_API_KEY,
});

export default async function NewsDetailPage({
  params,
}: {
  params: Promise<{ locale: string; contentId: string }>;
}) {
  // [locale] セグメントから、ロケールとコンテンツIDを受け取る
  const { locale, contentId } = await params;

  // ロケールに応じて参照するAPIを切り替える(ja → news / en → news-en)
  const endpoint = locale === "en" ? "news-en" : "news";

  // 対応する言語のAPIからコンテンツを取得する
  const data = await client.getListDetail({
    endpoint,
    contentId,
  });

  return <Article data={data} /> ;
}

このようにしておくと、同じコンテンツIDを使いながら、URLのロケールに応じて日本語版(news)と英語版(news-en)を出し分けられます。

ステップ10: ビルド・デプロイ

最後に、登録・更新した英語版コンテンツをサイトに反映します。SSG(静的生成)の構成では、コンテンツの更新をトリガーにサイトを再ビルドすることで、英語版ページが公開されます。

実装時に注意したいポイント

状態遷移は翻訳とは別の考慮が必要になる

日本語のコンテンツの下書き、公開停止、削除、コンテンツID変更などの状態遷移も、本文翻訳とは別に扱う必要があります。

内部リンクは翻訳だけでは解決しない

内部リンクは、単純な翻訳では解決できないポイントです。日本語ページへのリンクをそのまま残すと、英語版の記事内から日本語ページへ遷移してしまいます。

ページ単位のリンクであれば、本文内の /news/foo/en/news/foo に置き換える、または日本語版コンテンツIDから英語版URLを引けるようにするなど、リンク変換用のロジックを用意することで対応できます。

一方で、リッチエディタ内の見出しにはランダムなIDが付与されるため、日本語版の見出しIDをそのまま英語版に流用しても、英語版側の見出しIDとは一致しません。

そのため、見出しへのアンカーリンクまで完全に置き換えることは、microCMS側の機能だけでは難しいと考えています。対応する場合は、見出しリンクを使わない構成にする、ページ単位のリンクに寄せる、またはフロントエンド側で別途制御するなどの検討が必要です。

スキーマ変更は翻訳処理にも影響する

英語版APIの作成自体は、APIスキーマのインポート・エクスポートを使うことで比較的簡単にできます。一方で、運用開始後にフィールドを追加・変更した場合は、以下にも影響します

  • 日本語版APIのスキーマ
  • 英語版APIのスキーマ
  • フロントエンドの型定義
  • 翻訳対象フィールドの抽出処理
  • 英語版APIへの登録ペイロード

microCMS上でフィールドを変更するだけでは、翻訳処理側のコードまでは自動で更新されません。スキーマ変更時には、翻訳処理にも影響があるかを確認する必要があります。

こうした見直しは、MCPサーバーを使えば、AIに最新のスキーマを読み取らせて、翻訳対象フィールドの抽出処理や英語版APIへの登録ペイロード、フロントエンドの型定義に差分がないかを確認・修正させる、といった運用も考えられます。

さらに、現状のマネジメントAPIはスキーマの取得には対応していますが、スキーマの作成・更新には対応していません。スキーマ変更自体は管理画面での手作業になります。

今後、API経由でスキーマの作成・更新までできるようになれば、日本語版のスキーマ変更に追従して英語版のスキーマを更新し、関連する翻訳処理のコードまで調整する、という一連の流れをより自動化しやすくなりそうです。

画像の英語化は別対応になる

本文を翻訳しても、画像内に埋め込まれた文字までは自動では翻訳されません。画像を英語化するには、英語版の画像を用意して手動で差し替える必要があります。

注意したいのは、差し替えた画像が、あとの翻訳フローで上書きされてしまう点です。今回の仕組みは日本語版をもとに英語版を作り直すため、そのままでは英語版の画像も日本語版の値に戻ってしまいます。

これを防ぐには、登録処理に「英語版にすでに画像があれば、日本語版の値で上書きしない」といった制御を入れる必要があります。

外部翻訳サービスと自前実装の使い分け

今回のように、英語版コンテンツをmicroCMS上に保持し、公開フローやリンク変換、下書き確認などを細かく制御したい場合は、自前の翻訳フローが有効です。

一方で、一般的なWebサイトを手軽に多言語化したい場合や、ページ単位で翻訳・言語切り替えをしたい場合は、WOVN.ioのような外部翻訳サービスを利用する方法もあります。

どちらがよいかは、以下のような観点で決めるとよいです。

  • 翻訳コンテンツをmicroCMS上に保持したいか
  • 翻訳の編集・レビューをどこで行いたいか
  • 言語追加や運用を誰が担当するか
  • 実装コストと運用コストのどちらを重視するか

要件に応じて、外部サービスと自前実装を使い分けるのが現実的です。

それぞれのメリット・デメリットについては以下のヘルプでも記載しているのでご参照ください。

https://help.microcms.io/ja/knowledge/multilingual-site

おわりに

今回は、microCMSのWebhook、Vercel Functions、OpenAI APIを使って、翻訳フローを作る方法を紹介しました。

AIを使うことで翻訳作業の多くを自動化できますが、実際に運用へ組み込むには、翻訳APIを呼び出すだけでは不十分です。HTMLやMarkdownの保護、固有名詞の管理、内部リンクの置き換え、公開フローの設計、失敗時の通知など、周辺の設計が重要になります。

microCMSはAPIベースでコンテンツを扱えるため、Webhookや外部APIと組み合わせることで、こうした独自の翻訳フローも柔軟に実装できます。

多言語化の方法にはさまざまな選択肢があります。サイトやコンテンツの特性に合わせて、外部翻訳サービスを使うのか、自前で翻訳フローを作るのかを検討してみてください。

関連資料

まずは、無料で
試してみましょう。

microCMSは無料ではじめられます。
ご不明な点はお気軽にお問い合わせください。