Webhookを活用して外部サービスと連携する
概要
Craft Cross CMSでは、コンテンツ操作に関するイベントをトリガーに、Craft Functionsで任意のバックエンド処理を実行できます。
この仕組みを使うことで、CMSのイベントを起点に外部サービスへHTTPリクエスト(Webhook)を送信し、さまざまな処理を自動化できます。たとえば次のようなユースケースがあります。
- コンテンツの公開・非公開をトリガーにWebサイトのデプロイを実行する
- コンテンツの公開をSlackチャンネルに通知する
- コンテンツの公開・非公開をトリガーにGitHub Actionsのワークフローを実行する
このドキュメントでは、Hook v2とCraft Functionsを使って、CMSのイベントをトリガーにWebhookを送信する方法をユースケースごとに紹介します。
全体の流れ
CMSのWebhook連携は、次の流れで動作します。
- Craft Cross CMSの管理画面でコンテンツを操作する
- Hook v2のトリガーによって、Craft Functionsのファンクションが実行される
- ファンクション内で外部サービスのURLにHTTPリクエストを送信する
事前準備
各ユースケースに共通して、次の準備が必要です。
1. API v2アプリを作成する
Hook v2を利用するために、API v2アプリを作成します。
- KARTE管理画面にログインし、[ストア > API v2設定]を選択します
- 右上の「作成」ボタンを押します
- 次の設定でアプリを作成します
- タイプ:
token - 名前:(適当な名前を付けます)
- スコープ:
beta.cms.content.get
- タイプ:
- 保存時に表示されるモーダルで、[Craft Secretへのトークン保存 > 保存]ボタンを押します(キー名は
KARTE_APP_TOKENなど、わかりやすい名前を付けてください)
2. Craft Functionsのファンクションを作成する
Webhook送信処理を実装するファンクションを作成します。
- KARTE管理画面の[Craft > ファンクション > 新規作成 > コードを書いて作成]を選択します
- ファンクションのタイプを イベント駆動タイプ にします
- コードを記述してデプロイします(各ユースケースのサンプルコードを参照)
3. Hook v2のトリガーを設定する
作成したAPI v2アプリにHook v2のトリガーを設定し、CMSのイベント発生時にファンクションが実行されるようにします。
- 作成したAPI v2アプリの編集画面を開きます
- [Hook設定]タブで「編集」を選択します
- [トリガー設定]から、ユースケースに応じたCMSのイベントを選択して追加します
- [チャンネル設定 > Craft > Hookステータス > 有効にする]にチェックを付け、手順2で作成したファンクションを選択します
- アプリ設定を保存します
Craft Functionsが受け取るデータ
Hook v2経由でCMSイベントをトリガーにファンクションが実行された場合、data には次のような情報が含まれます。
data.kind:"karte/apiv2-hook"data.jsonPayload.event_type: CMSイベントの種類(例:"cms/content/publish")data.jsonPayload.data.id: コンテンツIDdata.jsonPayload.data.sys.modelId: モデルID
これらの情報を使って、対象のイベントやモデルを絞り込んだ上で外部サービスへのWebhookを実行できます。
ユースケース1: Webサイトをデプロイする
コンテンツの公開・非公開をトリガーにWebサイトのデプロイを実行するケースです。VercelやNetlifyなどのホスティングサービスが提供するDeploy Hook URLにリクエストを送ることで、サイトのデプロイを自動的に開始できます。
処理の流れ
- CMSでコンテンツを公開・非公開にする
- Hook v2のトリガー(
KARTE CMS: コンテンツの公開時、KARTE CMS: コンテンツの非公開時)によってCraft Functionsが実行される - ホスティングサービスのDeploy Hook URLにPOSTリクエストを送信する
- サイトのデプロイが開始される
サンプルコード
Deploy Hook URLはファンクションの[変数]タブで設定します。
const LOG_LEVEL = "<% LOG_LEVEL %>";const DEPLOY_HOOK_URL = "<% DEPLOY_HOOK_URL %>";
export default async function (data, { MODULES }) { const { initLogger } = MODULES; const logger = initLogger({ logLevel: LOG_LEVEL });
// Hook v2からのトリガーでない場合はスキップ if (data.kind !== "karte/apiv2-hook") { return; }
try { const response = await fetch(DEPLOY_HOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), });
if (!response.ok) { const text = await response.text(); throw new Error(`Deploy hook request failed: ${response.status} ${text}`); }
logger.log("Deploy hook triggered successfully."); } catch (e) { logger.error(`Error triggering deploy hook: ${e.message}`); }}変数の設定
ファンクションの[変数]タブで次の値を設定します。
| 変数名 | 説明 | 例 |
|---|---|---|
LOG_LEVEL | 出力するログのレベル | DEBUG, INFO, WARN, ERROR |
DEPLOY_HOOK_URL | ホスティングサービスのDeploy Hook URL | https://api.vercel.com/v1/integrations/deploy/xxxx |
ユースケース2: Slack通知
特定のモデル(例: 投稿)のコンテンツが公開されたときにSlackチャンネルへ通知するケースです。Slackアプリを作成し、chat.postMessage APIを使うことで通知を実現できます。
処理の流れ
- CMSでコンテンツを公開する
- Hook v2のトリガー(
KARTE CMS: コンテンツの公開時)によってCraft Functionsが実行される - 対象のモデルかどうかを判定する
- コンテンツの情報からSlackメッセージを組み立てる
- Slackの
chat.postMessageAPIでメッセージを送信する
事前準備
Slackアプリの設定
- Slackアプリ管理画面から[Create New App]をクリックします
- [From scratch]を選択し、アプリ名と対象のワークスペースを選択して作成します
- 作成後の画面で[OAuth & Permissions]を開き、[Bot Token Scopes]に
chat:writeを追加して保存します - [Install to Workspace]をクリックし、認証画面で[許可する]を選択してワークスペースにインストールします
- 生成されたBot User OAuth Token(
xoxb-で始まるトークン)を控えておきます - 取得したBot User OAuth TokenをCraft Secret Managerに登録します
- 通知先のSlackチャンネルの設定から[インテグレーション > アプリを追加する]で作成したアプリを追加します
コンテンツ取得用のアクセストークンを確認する
コンテンツの詳細情報を取得するために、事前準備のステップ1で作成したAPI v2アプリのアクセストークンを使用します。このアプリにはスコープ beta.cms.content.get が設定されているため、Management APIでコンテンツを取得できます。
Craft Secretに保存したアクセストークンのキー名を控えておいてください。
サンプルコード
SlackのBot TokenとAPI v2アプリのアクセストークンはCraft Secret Managerから取得します。シークレット名は環境ごとに分けて管理できるよう、Craft Functionsの変数で設定しています。
モジュールの設定
ファンクションの[モジュール]タブに次の内容を設定します。
{ "api": "6.1.3", "@slack/web-api": "7.14.1"}コード
コードは次のように記載します。
import api from "api";import { WebClient } from "@slack/web-api";
const LOG_LEVEL = "<% LOG_LEVEL %>";const TARGET_MODEL_ID = "<% TARGET_MODEL_ID %>";const SLACK_CHANNEL_ID = "<% SLACK_CHANNEL_ID %>";const SLACK_BOT_TOKEN_SECRET_NAME = "<% SLACK_BOT_TOKEN_SECRET_NAME %>";const KARTE_APP_TOKEN_SECRET_NAME = "<% KARTE_APP_TOKEN_SECRET_NAME %>";
// CMS APIクライアントを初期化const cmsClient = api("@dev-karte/v1.0#jj0g1jm98bme78");
export default async function (data, { MODULES }) { const { initLogger, secret } = MODULES; const logger = initLogger({ logLevel: LOG_LEVEL });
// Hook v2からのトリガーでない場合はスキップ if (data.kind !== "karte/apiv2-hook") { return; }
const modelId = data.jsonPayload.data.sys.modelId; const contentId = data.jsonPayload.data.id;
// 対象のモデル以外はスキップ if (modelId !== TARGET_MODEL_ID) { return; }
try { // Craft Secret Managerからシークレットを取得 const { [SLACK_BOT_TOKEN_SECRET_NAME]: slackBotToken, [KARTE_APP_TOKEN_SECRET_NAME]: token, } = await secret.get({ keys: [SLACK_BOT_TOKEN_SECRET_NAME, KARTE_APP_TOKEN_SECRET_NAME], });
// Management APIでコンテンツのタイトルを取得 cmsClient.auth(token); const cmsResponse = await cmsClient.postV2betaCmsContentGet({ modelId, contentId, }); const content = cmsResponse.data; const title = content.title || "(タイトルなし)";
// Slack WebClientでメッセージを送信 const slackClient = new WebClient(slackBotToken); await slackClient.chat.postMessage({ channel: SLACK_CHANNEL_ID, text: `CMSコンテンツが公開されました: ${title}`, });
logger.log("Slack notification sent successfully."); } catch (e) { logger.error(`Error sending Slack notification: ${e.message}`); }}変数の設定
ファンクションの[変数]タブで次の値を設定します。
| 変数名 | 説明 | 例 |
|---|---|---|
LOG_LEVEL | 出力するログのレベル | DEBUG, INFO, WARN, ERROR |
TARGET_MODEL_ID | 通知対象のモデルID | a1234567890b2345678901c3 |
SLACK_CHANNEL_ID | 通知先のSlackチャンネルID | C01ABCDEFGH |
SLACK_BOT_TOKEN_SECRET_NAME | 事前準備で登録したSlack Bot TokenのCraft Secret Managerのシークレット名 | SLACK_BOT_TOKEN |
KARTE_APP_TOKEN_SECRET_NAME | API v2アプリのアクセストークンを保存したCraft Secret Managerのシークレット名 | KARTE_APP_TOKEN |
送信されるメッセージ
上記のサンプルコードでは、Slackに次のようなテキストメッセージが送信されます。
CMSコンテンツが公開されました: 記事のタイトルよりリッチなメッセージを送信したい場合は、Slack Block Kit を参照してください。
ユースケース3: GitHub Actionsを実行する
コンテンツの公開・非公開をトリガーにGitHub Actionsのワークフローを実行するケースです。たとえば、CMSのイベントを起点に静的サイトのビルドやテストを実行できます。
処理の流れ
- CMSでコンテンツを公開・非公開にする
- Hook v2を経由してCraft Functionsが実行される
- GitHub APIのCreate a repository dispatch eventを呼び出す
- 指定したGitHub Actionsワークフローが実行される
事前準備: GitHubの設定
- GitHub Personal Access Token(fine-grained)を作成します
- リポジトリへの
ContentsのRead and write権限が必要です
- リポジトリへの
- 取得したトークンをCraft Secret Managerに登録します
- トリガーしたいワークフローファイル(例:
.github/workflows/deploy.yml)にrepository_dispatchトリガーを追加します
# .github/workflows/deploy.yml の例name: Deployon: repository_dispatch: types: [cms-deploy] # 任意の名前を指定できます。サンプルコードの event_type と一致させてください。サンプルコード
GitHub TokenはCraft Secret Managerから取得します。シークレット名は環境ごとに分けて管理できるよう、Craft Functionsの変数で設定しています。
const LOG_LEVEL = "<% LOG_LEVEL %>";const GITHUB_OWNER = "<% GITHUB_OWNER %>";const GITHUB_REPO = "<% GITHUB_REPO %>";const GITHUB_TOKEN_SECRET_NAME = "<% GITHUB_TOKEN_SECRET_NAME %>";
export default async function (data, { MODULES }) { const { initLogger, secret } = MODULES; const logger = initLogger({ logLevel: LOG_LEVEL });
// Hook v2からのトリガーでない場合はスキップ if (data.kind !== "karte/apiv2-hook") { return; }
try { // Craft Secret Managerからシークレットを取得 const { [GITHUB_TOKEN_SECRET_NAME]: githubToken } = await secret.get({ keys: [GITHUB_TOKEN_SECRET_NAME], });
// GitHub Actions の repository_dispatch イベントを発火する const url = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/dispatches`; const response = await fetch(url, { method: "POST", headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${githubToken}`, "X-GitHub-Api-Version": "2022-11-28", }, body: JSON.stringify({ event_type: "cms-deploy", }), });
if (!response.ok) { const errorBody = await response.text(); throw new Error( `GitHub API request failed: ${response.status} ${errorBody}`, ); }
logger.log("GitHub Actions workflow triggered successfully."); } catch (e) { logger.error(`Error triggering GitHub Actions: ${e.message}`); }}変数の設定
ファンクションの[変数]タブで次の値を設定します。
| 変数名 | 説明 | 例 |
|---|---|---|
LOG_LEVEL | 出力するログのレベル | DEBUG, INFO, WARN, ERROR |
GITHUB_OWNER | GitHubリポジトリのオーナー名 | my-org |
GITHUB_REPO | GitHubリポジトリ名 | my-website |
GITHUB_TOKEN_SECRET_NAME | 事前準備で登録したGitHub TokenのCraft Secret Managerのシークレット名 | GITHUB_TOKEN |