コンテンツにスキップ

Webhookで外部サービスと連携する

このチュートリアルでは、Craft Cross CMSのコンテンツ操作イベントをトリガーに、Craft Functionsで外部サービスへWebhookを送信する手順をユースケースごとに紹介します。Webhookの仕組みや、対応するイベント・ペイロードの構造については、コンセプトのWebhookを参照してください。

前提

このチュートリアルを始める前に、コンセプトのWebhookの「管理画面での設定」に従って、次の準備を済ませてください。

  • beta.cms.content.get スコープを持つAPI v2アプリを作成する
  • API v2アプリにHook v2のトリガーを設定する

トリガー設定でファンクションを選択する際は、次の手順で作成するファンクションを指定します。

ファンクションを作成する

Webhook送信処理を実装するファンクションを作成します。

  • KARTE管理画面の[Craft]>[ファンクション]>[新規作成]>[コードを書いて作成]を選択します
  • ファンクションのタイプを「イベント駆動タイプ」にします
  • コードを記述してデプロイします(各ユースケースのサンプルコードを参照)

ユースケース1: Webサイトをデプロイする

コンテンツの公開・非公開をトリガーにWebサイトのデプロイを実行するケースです。VercelやNetlifyなどのホスティングサービスが提供するDeploy Hook URLにリクエストを送ることで、サイトのデプロイを自動的に開始できます。

処理の流れ

  1. CMSでコンテンツを公開・非公開にする
  2. Hook v2のトリガー(KARTE CMS: コンテンツの公開時KARTE CMS: コンテンツの非公開時)によってCraft Functionsが実行される
  3. ホスティングサービスのDeploy Hook URLにPOSTリクエストを送信する
  4. サイトのデプロイが開始される

サンプルコード

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 URLhttps://api.vercel.com/v1/integrations/deploy/xxxx

ユースケース2: Slack通知

特定のモデル(例: 投稿)のコンテンツが公開されたときにSlackチャンネルへ通知するケースです。Slackアプリを作成し、chat.postMessage APIを使うことで通知を実現できます。

処理の流れ

  1. CMSでコンテンツを公開する
  2. Hook v2のトリガー(KARTE CMS: コンテンツの公開時)によってCraft Functionsが実行される
  3. 対象のモデルかどうかを判定する
  4. Management APIでコンテンツの情報を取得し、Slackメッセージを組み立てる
  5. Slackの chat.postMessage APIでメッセージを送信する

事前準備: Slackアプリの設定

  1. Slackアプリ管理画面から[Create New App]をクリックします
  2. [From scratch]を選択し、アプリ名と対象のワークスペースを選択して作成します
  3. 作成後の画面で[OAuth & Permissions]を開き、[Bot Token Scopes]に chat:write を追加して保存します
  4. [Install to Workspace]をクリックし、認証画面で[許可する]を選択してワークスペースにインストールします
  5. 生成されたBot User OAuth Token(xoxb- で始まるトークン)を控えておきます
  6. 取得したBot User OAuth TokenをCraft Secret Managerに登録します
  7. 通知先のSlackチャンネルの設定から[インテグレーション]>[アプリを追加する]で作成したアプリを追加します

事前準備: コンテンツ取得用のアクセストークンを確認する

コンテンツの詳細情報を取得するために、前提で作成した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通知対象のモデルIDa1234567890b2345678901c3
SLACK_CHANNEL_ID通知先のSlackチャンネルIDC01ABCDEFGH
SLACK_BOT_TOKEN_SECRET_NAME事前準備で登録したSlack Bot TokenのCraft Secret Managerのシークレット名SLACK_BOT_TOKEN
KARTE_APP_TOKEN_SECRET_NAMEAPI v2アプリのアクセストークンを保存したCraft Secret Managerのシークレット名KARTE_APP_TOKEN

上記のサンプルコードでは、Slackに次のようなテキストメッセージが送信されます。

CMSコンテンツが公開されました: 記事のタイトル

よりリッチなメッセージを送信したい場合は、Slack Block Kit を参照してください。

ユースケース3: GitHub Actionsを実行する

コンテンツの公開・非公開をトリガーにGitHub Actionsのワークフローを実行するケースです。たとえば、CMSのイベントを起点に静的サイトのビルドやテストを実行できます。

処理の流れ

  1. CMSでコンテンツを公開・非公開にする
  2. Hook v2を経由してCraft Functionsが実行される
  3. GitHub APIのCreate a repository dispatch eventを呼び出す
  4. 指定したGitHub Actionsワークフローが実行される

事前準備: GitHubの設定

  1. GitHub Personal Access Token(fine-grained)を作成します
    • リポジトリへの ContentsRead and write 権限が必要です
  2. 取得したトークンをCraft Secret Managerに登録します
  3. トリガーしたいワークフローファイル(例: .github/workflows/deploy.yml)に repository_dispatch トリガーを追加します
# .github/workflows/deploy.yml の例
name: Deploy
on:
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_OWNERGitHubリポジトリのオーナー名my-org
GITHUB_REPOGitHubリポジトリ名my-website
GITHUB_TOKEN_SECRET_NAME事前準備で登録したGitHub TokenのCraft Secret Managerのシークレット名GITHUB_TOKEN