3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

デザイナーでもバックエンドを気にせずデータの送受信ができるサービスを、手軽に作ってみたい!
というわけで、microCMSとNext.jsを使ってそれっぽいプロジェクトに挑戦しました。
今回はその制作プロセスをざっくり共有します!

どんなものを作る?

突然ですが、こんなシチュエーション、ありませんか?
「気の利いたメッセージや、日々の叫びやつぶやきをボトルに詰めて海に流したい!」
ありますよね。…あると、思います。

というわけで、以下のようなものを作ってみました。

メッセージを投稿すると、すでに投稿された中からランダムに1件が返ってくる。
誰かのメッセージを受け取れる体験を提供するインタラクティブなWebサービス。

使用した技術

  • フロントエンド … Next.js(React + TypeScript)
  • バックエンド … microCMS(ヘッドレスCMS)
  • プラットフォーム … Vercel、GitHub
  • サポートAI … ChatGPT、Copilot(AIコーディング支援)

開発プロセス

なぜヘッドレスCMS?

バックエンドやデータベース構築の知識があまりない私でも扱いやすく、
microCMSは「データの管理」と「API提供」を簡単に実現できる点が魅力でした。

サーバーレスな設計

サーバーの管理や構築を一切せず、データベース的な役割をmicroCMSに、
デプロイはVercelにお任せ。サーバーレスアーキテクチャの恩恵を活用。

APIの設計

今回作成したPOST APIの概要です。

スキーマ

フィールド 表示名 種類 必須 詳細設定
message メッセージ テキストエリア true 文字数を制限

ヘッドレスCMSにデータを送るには、microCMSの「APIキー管理」でPOSTリクエストを有効化します。
※ Hobby(無料)版だとPOST使えないので有料版のみ(今回はお試しなのでトライアルで)

スクリーンショット 2024-12-15 16.52.03.png

フロントエンドの実装

環境セットアップ

Next.jsのプロジェクトを作成
…気づけば…15…!

npx create-next-app@latest
  • 必要なコンポーネントやページを配置(ChatGPTに色々お願いしつつ)
  • App RouterのRoute HandlersでmicroCMSにメッセージを投稿

メッセージ入力ページ

page.tsx
  async function handleSubmit(event: React.FormEvent) {
    event.preventDefault();
    setIsSubmitting(true);
    setError('');
    try {
      // 入力されたメッセージを取得
      const message = textareaRef.current?.value || '';
      // API Routeにデータを送信
      const response = await fetch('/api/sendMessage', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message }),
      });
      // レスポンスの確認
      const result = await response.json();
      if (!response.ok) {
        throw new Error(result.error || '送信に失敗しました');
      }
      // 成功時に別のページにリダイレクト
      router.push('/random');
    } catch {
      setError('エラーが発生しました');
    } finally {
      setIsSubmitting(false);
    }
  }
  return (
    <div className={`${css.contents_wrap}`}>
      <p>メッセージをボトルにいれて海に流してみましょう</p>
      <form onSubmit={handleSubmit}>
        <div className={`${css.form_wrap}`}>
          <div className={`${css.form_inner}`}>
            <label htmlFor="message" className={`${css.label}`}>
              Dear...
            </label>
            <textarea
              ref={textareaRef}
              className={`${css.textarea}`}
              name="message"
              placeholder="Write your message here"
              required
              defaultValue=""
              onInput={handleInput}
            ></textarea>
            {error && <p className={`${css.error}`}>{error}</p>}
            <button type="submit" disabled={isSubmitting} className={`${css.button}`}>
              {isSubmitting ? '送信中...' : 'メッセージを送る'}
            </button>
          </div>
        </div>
      </form>
    </div>
  );

Route

route.tsx
export async function POST(request: Request) {
  try {
    const { message } = await request.json();

    if (!message) {
      return NextResponse.json({ error: 'メッセージは必須です' }, { status: 400 });
    }
    const microCMSResponse = await fetch('YOUR_API_ENDPOINT', {
      headers: {
        'X-MICROCMS-API-KEY': process.env.MICROCMS_API_KEY || '',
        'Content-Type': 'application/json',
      },
      method: 'POST',
      body: JSON.stringify({ message }),
    });
    if (!microCMSResponse.ok) {
      const error = await microCMSResponse.json();
      return NextResponse.json(
        { error: error.message || '送信に失敗しました' },
        { status: microCMSResponse.status }
      );
    }
    return NextResponse.json({ success: true });
  } catch (error) {
    return NextResponse.json({ error: 'サーバーエラーが発生しました' }, { status: 500 });
  }
}

ランダムメッセージページ

送信後はすでに投稿されたメッセージからランダムに1件メッセージが表示されるページに遷移します。

page.tsx
export default async function Home() {
  // 記事を取得
  const contents = await getRandomPosts();
  // ランダムなインデックスを生成
  const getRandomIndices = (num: number, max: number): number[] => {
    const indices: number[] = [];
    while (indices.length < num) {
      const index = Math.floor(Math.random() * max);
      if (!indices.includes(index)) {
        indices.push(index);
      }
    }
    return indices;
  };
  //現在は1件だけど、複数設定できる
  const randomIndices = getRandomIndices(1, contents.length);
  const randomContents = randomIndices.map((index) => contents[index]);

  return (
    <>
      <ul className={`${css.list}`}>
        {randomContents.map((post) => (
          <li key={post.id}>
            <p>誰かのメッセージが流れ着きました</p>
            <article className={`${css.message_wrap}`}>
              <h2 className={`${css.head}`}>Dear...</h2>
              <div className={`${css.message}`}>{post.message || 'メッセージはありません'}</div>
            </article>
          </li>
        ))}
      </ul>
    </>
  );
}

デプロイ

  • GitHubとVercelの連携
    コードをGitHubにプッシュするたびにVercelが自動ビルドを実行。

完成

  • メッセージを投稿すると、ランダムで誰かのメッセージが返ってくる
    (デモページではRandomからアクセスして確認)
  • Collectionにこれまで投稿したメッセージ一覧を表示

image.jpeg

デモはこちらです
※送信機能については、Hobby(無料)版のため使えなくなっています。

振り返り

良かった点

  • バックエンドに触れずとも、インタラクティブなサービスを作れた!
  • ヘッドレスCMSやサーバーレスの使い勝手の良さを実感
  • microCMSとNext.js(App Route)を駆使して、フロントエンドとバックエンドをシンプルに統合

反省点

  • フォーム送信時や、メッセージ表示に演出を加えたかった
  • クライアントサイドとサーバーサイドの理解がまだイマイチ
  • ISR設定だけだと、自分の投稿したメッセージがコレクションに反映されるまで時間がかかる→On-demand ISRを使えば解決する?

正直デザイナーでも簡単に…とまではいきませんでしたが、それっぽいサービスを作ることはできそうでした!
ローカルの環境とかも作りやすくて、昔に比べて格段にできることが増えるので、制作の幅が広がりそうです。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?