Loading...

2023-10-16(月) 15:00

📮 Next.js で SlackのIncoming Webhook を使用したお問い合わせフォームを実装する

Next.jsSlack
Next.js で Slackの Incoming Webhook を使用したお問い合わせフォームを実装する手順を解説します。

目次

前提

この記事では以下を前提としています。

  • Slack の Incoming Webhook を設定済みであること
  • Next.js のバージョンは 13.5.4 (App router 使用)
  • Typescript 使用
  • Tailwind を使用
  • react-hook-form を使用

この記事のゴール

この記事では以下のように Next.js で構築した Web サイトのお問い合わせフォームから送信し、

お問い合わせフォームの内容を以下のように Slack の特定のチャンネルに投稿するところまでゴールとします。

Next.jsのお問い合わせフォームからSlackに投稿されたメッセージ

react-hook-form をインストール

この記事では、簡単にフォームのバリデーションを実装するために react-hook-form を使用します。 そのために以下を実行してreact-hook-formをインストールします。

ターミナル
$ npm install react-hook-form
# or
$ yarn add react-hook-form

お問い合わせフォームの実装

ひとまず簡易のバリデーションで全てを詰め込んでいるコンポーネントですが、以下のようなお問い合わせフォームを作成します。
実際にはtry/catchなどを使ってエラーハンドリングをすることをおすすめします。

app/components/ContactForm.tsx
'use client';
import { useState } from 'react';
import { useForm, SubmitHandler, set } from 'react-hook-form';
import { useRouter } from 'next/navigation';
 
type FormInputs = {
  name: string;
  email: string;
  message: string;
};
 
export default function ContactForm() {
  const router = useRouter();
  const [loading, setLoading] = useState(false);
  const [isSubmitted, setIsSubmitted] = useState(false);
  const [isError, setIsError] = useState(false);
 
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<FormInputs>();
 
  const onSubmit: SubmitHandler<FormInputs> = async (data) => {
    console.log(data);
    setIsSubmitted(false);
    setIsError(false);
    setLoading(true);
 
    // /api/contactにPOSTリクエストを送る
    // /api/contact APIルートは次節に記載
    const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/contact`, {
      method: 'POST',
      mode: 'same-origin',
      credentials: 'same-origin',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    });
 
    setLoading(false);
 
    if (response.status !== 200) {
      setIsError(true);
      return;
    } else {
      setIsSubmitted(true);
      reset();
    }
  };
 
  return (
    <>
      <form
        onSubmit={handleSubmit(onSubmit)}
        className='w-full md:w-4/5 mx-auto'
      >
        <div className='flex flex-col w-full'>
          {isSubmitted ? (
            <div
              className='bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-4'
              role='alert'
            >
              <p className='font-bold'>送信しました。</p>
            </div>
          ) : null}
          {isError ? (
            <div
              className='bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4 mb-4'
              role='alert'
            >
              <p className='font-bold'>送信に失敗しました。</p>
            </div>
          ) : null}
          <div className='flex flex-col sm:flex-row sm:items-center'>
            <div className='text-left text-gray-700 dark:text-white mb-2'>
              お名前
            </div>
          </div>
          <div className='flex flex-col sm:flex-row sm:items-center mb-5'>
            <div className='w-full'>
              <input
                {...register('name', { required: true })}
                className='py-2 px-3 w-full leading-tight rounded border appearance-none text-gray-700 focus:outline-none focus:shadow-outline'
              />
              {errors.name && <span>お名前は必須です。</span>}
            </div>
          </div>
          <div className='flex flex-col sm:flex-row sm:items-center'>
            <div className='text-left text-gray-700 dark:text-white mb-2'>
              メールアドレス
            </div>
          </div>
          <div className='flex flex-col sm:flex-row mb-5'>
            <div className='w-full'>
              <input
                {...register('email', {
                  required: true,
                  pattern: /^\S+@\S+$/i,
                })}
                className='py-2 px-3 w-full leading-tight rounded border appearance-none text-gray-700 focus:outline-none focus:shadow-outline'
              />
              {errors.email && (
                <span>有効なメールアドレスを入力してください。</span>
              )}
            </div>
          </div>
          <div className='flex flex-col sm:flex-row sm:items-center'>
            <div className='text-left text-gray-700 dark:text-white mb-2'>
              本文
            </div>
          </div>
          <div className='flex flex-col sm:flex-row mb-5'>
            <div className='w-full'>
              <textarea
                {...register('message', { required: true })}
                className='py-2 px-3 w-full leading-tight rounded border appearance-none text-gray-700 focus:outline-none focus:shadow-outline'
              />
              {errors.message && <span>メッセージは必須です。</span>}
            </div>
          </div>
        </div>
        <div className='flex justify-center items-center mb-10 md:mb-20 sm:ml-8'>
          <button
            type='submit'
            disabled={loading}
            className='flex justify-center items-center py-2 px-4 w-40 font-bold text-white bg-blue-800 hover:bg-blue-900 rounded-sm disabled:opacity-50 cursor-pointer'
          >
            {loading ? (
              <div role='status' className='mr-2'>
                <svg
                  aria-hidden='true'
                  className='w-8 h-8 mr-2 text-white animate-spin fill-blue-600'
                  viewBox='0 0 100 101'
                  fill='none'
                  xmlns='http://www.w3.org/2000/svg'
                >
                  <path
                    d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z'
                    fill='currentColor'
                  />
                  <path
                    d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z'
                    fill='currentFill'
                  />
                </svg>
                <span className='sr-only'>Loading...</span>
              </div>
            ) : null}{' '}
            {loading ? '送信中...' : '送信'}
          </button>
        </div>
      </form>
    </>
  );
}

環境変数の設定

Slack の Incoming Webhook URL は、知っていれば誰でも自由にメッセージを投稿できてしまうため、環境変数に設定します。

.env
SLACK_INCOMING_WEBHOOK_URL='https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
NEXT_PUBLIC_URL='http://localhost:3011'

ここで設定した環境変数を、Next.js の API ルートでのみ使用(サーバ側からのみ使用)します。 Next.js が稼働する Web サイトの URL 用にNEXT_PUBLIC_URLも設定しています。 もしまだ Slack の Incoming Webhook を未設定で URL を取得していない場合は、以下のページに設定手順をまとめましたので参考にしてみてください。

📨 Slack の Incoming Webhook を設定する

Slack の Incoming Webhook を設定する手順を解説します。

ritaiz.com

本番環境でも忘れずにこれら2つのSLACK_INCOMING_WEBHOOK_URLとNEXT_PUBLIC_URLを設定してください。

お問い合わせ内容送信用の API ルートの作成

以下のように、お問い合わせフォームからの入力内容を Slack の Incoming Webhook を使用して Slack の特定のチャンネルにメッセージを投稿する API ルートを作成します。

app/api/contact/route.ts
import { NextResponse } from 'next/server';
 
// POST /api/contact
export async function POST(request: Request) {
  const data = await request.json();
 
  // 環境変数からSlack Incoming Webhook URLを取得
  const url = process.env.SLACK_INCOMING_WEBHOOK_URL;
 
  if (url) {
    await fetch(url, {
      method: 'POST',
      mode: 'same-origin',
      credentials: 'same-origin',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        text: `お名前:${data.name}\nメールアドレス:${data.email}\n本文:\n${data.message}`,
      }),
    });
  }
  return NextResponse.json({ message: 'Message Sent' }, { status: 200 });
}

上記では、以下のフォーマットのデータを Slack の Incoming Webhook に POST しています。

{
  "text": "投稿したいメッセージ文字列"
}

これは最もシンプルな例ですが、実際には投稿するメッセージ内にアクションボタンを設置したり、画像を添付したりといったこともできます。 具体的には、以下の公式ドキュメントに記載がありますので、目的に合わせて必要なオプションを設定してみてください。

Making it fancy with advanced formatting

Incoming Webhooks conform to the same rules and functionality as any of our other messaging APIs. You can make your posted messages as simple as a single line of text, or make them really useful with interactive components.

api.slack.com

お問い合わせフォームの動作確認

あとは以下のように適当なページで上記で作成したContactFormを使用してフォームを送信してみると、本記事冒頭に載せたゴールと同じ結果を得られると思います。

app/contact/page.tsx
// import先は適宜置き換えてください。
import ContactForm from '../components/Forms/ContactForm';
 
export default function Home() {
  return (
    <main className='flex min-h-screen flex-col items-center justify-between p-24'>
      <div className='z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex'>
        <ContactForm />
      </div>
    </main>
  );
}

まとめ

Next.js のお問い合わせフォームから Slack の Incoming Webhook を使ってお問い合わせ内容を Slack に投稿する実装について解説しました。
Incoming Webhook を使用することで、Slack の特定のチャンネルに簡単にメッセージを投稿することができます。 ただし、Incoming Webhook は URL を知っていれば誰でも自由に投稿できる点、投稿のみでその他のメッセージ編集や削除はできません。 用途に合わせて使用し、必要に応じて Incoming Webhook ではなくボットなどを使用することをおすすめします。