# 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()` のオプションです。

```ts title="src/worker.ts"
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 以上の送信です。これらも保存し、管理画面の **スパムの疑い** に表示します。[問い合わせフォームの保護](https://goodnight-web.dev/ja/docs/how-it-works/contact-protection.md)を参照してください。

## コードでは

問い合わせ API は、行を保存したあと、スパムの疑いがない問い合わせのときだけ通知を始めます。

```ts title="packages/server/src/features/contact/contact-routes.ts"
// 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,
      }),
    }),
  );
}
```

メッセージは、次の値だけから組み立てます。訪問者が入力したものを受け取る引数はありません。

```ts title="packages/server/src/features/notify/slack.ts"
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 のシークレットとして持ち、管理画面にも表示しません。
