Slack 通知
新しい問い合わせを Slack に知らせる任意の通知です。応答の後に送り、個人情報を含みません。
Worker のシークレット SLACK_WEBHOOK_URL に Slack の Incoming Webhook の URL を設定すると、新しい問い合わせが来たときに Slack に通知が届きます。通知は訪問者への応答の後に送ります。サイト名とフォームを送信したページを示し、個人情報も問い合わせの内容も含みません。
通知の見た目
example.com の /contact から問い合わせが届き、言語を日本語にしている場合、通知は上から次のように並びます。
| 部分 | 表示する内容 |
|---|---|
| 見出し | 🌙 新しいお問い合わせ |
| 本文 | example.com に新しいお問い合わせが届きました。 |
| 受信日時 | 2026-10-23 22:35 のような日時。読む人のタイムゾーンで表示します |
| ページ | /contact |
| 注記 | この通知には、個人情報やフォームの入力内容を含めていません。 |
| ボタン | 管理画面で開く。押すとその問い合わせの詳細が開きます。いつもどおり Passkey でのログインが必要です |
英語にした場合、見出しは「🌙 New inquiry」になります。Slack の通知バナーとスクリーンリーダーには、本文を太字なしにした「example.com に新しいお問い合わせが届きました。」という 1 行の文が使われます。
通知に含むもの
- サイト名。 既定ではリクエストのホスト名で、たとえば
example.comです。別の名前を出すときはsiteNameオプションを設定します。 - 受信日時。 Slack の日付トークンで送るので、Slack が読む人それぞれのタイムゾーンで表示します。日付はどの言語でも
2026-10-23の形です。時刻は読む人の Slack の設定に従うので、22:35とも10:35 PMとも表示されます。日付トークンを表示できない Slack アプリでは、2026-10-23 13:35 UTCのように UTC で表示されます。 - フォームを送信したページ。
/contactのようなパスだけです。クエリ文字列や#以降は取り除きます。ページのパスがわからないときは、この部分を出しません。 - 管理画面の該当の問い合わせを開くボタン。
メールアドレス、名前、フォームの項目の値、プルダウンで選ばれた選択肢、IP アドレス、ユーザーエージェントは含めません。管理者がフォームに項目を足しても、これは変わりません。
言語とサイト名
どちらも、サイトの src/worker.ts にある createGoodnightWorker() のオプションです。
export default createGoodnightWorker({
locale: siteConfig.defaultLocale,
siteName: 'My Company',
});localeは通知の言語で、jaかenを指定します。指定しなければ英語です。スターターはサイトの既定の言語を渡しています。siteNameは、通知に出すホスト名の代わりの名前です。空白だけの値は、設定していないものとして扱います。Slack には文字としてそのまま表示します。*、_、~、バッククォートは全角の文字に置き換えるので、太字などの書式にはなりません。<と>でリンクやメンションを作ることもできません。80 文字を超える名前は切り詰め、末尾を…にします。
そのほかの決まり
- 送る先は Slack だけです。 Worker は、スパムの疑いがない問い合わせを保存した後、Webhook に投稿します。URL は
https://hooks.slack.com/services/T…/B…/…の形式である必要があり、最後の部分の後ろには、スラッシュも含めて何も付けられません。ほかの値は無視し、管理画面の問い合わせ一覧に、URL の形式が正しくないことを知らせる警告を出します。警告に URL は表示しません。 - 成功を記録し、再送はしません。 投稿に成功したら、Worker は
notified_atを記録します。失敗しても送り直しません。 - スパムの疑いは通知しません。 対象は、人には見えない入力欄である honeypot に値が入っている送信と、スパムスコアが 3 以上の送信です。これらも保存し、管理画面の スパムの疑い に表示します。問い合わせフォームの保護を参照してください。
コードでは
問い合わせ API は、行を保存したあと、スパムの疑いがない問い合わせのときだけ通知を始めます。
// 8. After the response: notification (never for suspected spam) and
// pruning.
const webhookUrl = env.SLACK_WEBHOOK_URL;
if (!suspectedSpam && isSlackWebhookUrl(webhookUrl)) {
runInBackground(c, 'slack', () =>
sendSlackNotification({
db: env.DB,
webhookUrl,
submissionId: id,
payload: buildSlackPayload({
locale: options.locale,
receivedAt: now,
adminUrl: `${origin}/admin/inquiries/${id}`,
siteName: options.siteName ?? new URL(c.req.url).hostname,
pagePath: data.pagePath,
}),
}),
);
}メッセージは、次の値だけから組み立てます。訪問者が入力したものを受け取る引数はありません。
export const buildSlackPayload = ({
locale,
receivedAt,
adminUrl,
siteName,
pagePath,
}: {
locale: Locale;
receivedAt: number;
adminUrl: string;
siteName: string;
pagePath?: string | null;
}): SlackPayload => {なぜこう作るか
- 応答は Slack を待ちません。 投稿は、問い合わせを保存して訪問者に応答した後、
ctx.waitUntilの中で動きます。Slack が止まっていても、問い合わせを失うことも、訪問者にエラーを見せることもありません。 - 個人情報を Cloudflare の外に出しません。 Slack は別のサービスです。送るのはサイト名、ページのパス、日時、リンクだけなので、問い合わせの内容は D1 データベースにとどまります。
- フォームの入力内容は送りません。 フォームで何を尋ねるかは管理者が決めるので、どの項目なら送ってよいかをサーバーは判断できません。
- サイト名でサイトを見分けられます。 複数のサイトが同じチャンネルに通知しても、どのサイトに届いた問い合わせかがわかります。
- Cloudflare Queues も再送も使いません。 管理画面のヘッダーには未読件数が常に出ています。通知を見逃しても、問い合わせを見逃すことにはなりません。
- Webhook の URL はシークレットです。 URL を知っている人は誰でもチャンネルに投稿できます。そのため Worker のシークレットとして持ち、管理画面にも表示しません。