Next.js + Firebaseで構築した採用企業サイト〜文系プログラマーの苦労と成長〜

# Next.js + Firebaseで構築した採用企業サイト〜文系プログラマーの苦労と成長〜

こんにちは、[Your Name]です。プログラミング歴2年の文系出身エンジニアです。今回は初めて担当した企業の採用サイト制作について、技術選定から実装、デプロイまでを共有したいと思います。

## はじめに：プロジェクト概要

インターン先の社長から「採用サイトを作ってほしい」と依頼があり、以下の要件で開発を行いました。

- 企業の採用情報を効果的に伝えるサイト
- モバイルフレンドリーな設計
- 問い合わせフォーム機能
- 運用しやすい構成

私自身、企業サイト制作は初めてだったため、様々な調査と学習を重ねながら取り組みました。

## 技術選定

まず技術選定において重視したのは、以下の点です。

1. パフォーマンスと SEO 対策
2. 拡張性と保守性
3. 学習コストとコミュニティのサポート

これらを考慮した結果、以下のスタックを選定しました。

- **フレームワーク**: Next.js 15
- **バックエンド/ホスティング**: Firebase (Hosting, Functions)
- **スタイリング**: TailwindCSS
- **フォーム処理**: Nodemailer

Next.js を選んだ理由は、React ベースでありながら SSG (Static Site Generation) をサポートしており、高速なパフォーマンスと優れた SEO 対策ができる点です。また、App Router の採用により、ファイルベースのルーティングで直感的な開発ができる点も魅力でした。

Firebase は、バックエンド構築の手間を省き、素早くデプロイできる点で採用しました。特に Functions を使ったお問い合わせフォームの実装と Hosting でのデプロイが簡単にできる点が決め手でした。

## プロジェクト構造

プロジェクトのディレクトリ構造は以下のようになっています：

```
src/
├── app/
│   ├── about/
│   ├── contact/
│   ├── services/
│   │   ├── analytics/
│   │   ├── performance/
│   │   ├── recruitment/
│   │   └── rpo/
│   ├── api/
│   │   └── contact/
│   ├── page.tsx
│   └── layout.tsx
├── components/
│   ├── Hero.tsx
│   ├── NavBar.tsx
│   ├── Footer.tsx
│   ├── ServiceSection.tsx
│   └── ...
└── styles/
    └── globals.css
```

Next.js の App Router 構造を活用して、直感的なルーティングを実現しています。

## 実装のポイント

### 1. レスポンシブなナビゲーションメニュー

モバイルフレンドリーな設計にするため、レスポンシブなナビゲーションメニューを実装しました。

```tsx
// src/components/NavBar.tsx の一部
'use client';

import { useState, useEffect } from 'react';
import { Menu, X } from 'lucide-react';

export default function NavBar() {
  const [isOpen, setIsOpen] = useState(false);
  const [isScrolled, setIsScrolled] = useState(false);
  
  useEffect(() => {
    const handleScroll = () => {
      setIsScrolled(window.scrollY > 20);
    };
    
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);
  
  return (
    <nav 
      className={`w-full z-50 transition-all duration-300 ${
        isScrolled ? 'py-2 fixed bg-white shadow-md' : 'py-4 bg-transparent'
      }`}
    >
      {/* ナビゲーション内容 */}
      {/* モバイルメニュートグル */}
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="md:hidden"
      >
        {isOpen ? <X /> : <Menu />}
      </button>
      
      {/* モバイルメニュー */}
      {isOpen && (
        <div className="md:hidden">
          {/* メニュー項目 */}
        </div>
      )}
    </nav>
  );
}
```

スクロールに応じてヘッダーの外観を変える実装や、モバイル表示時のハンバーガーメニューを実装しました。この部分は React のステート管理と useEffect を使ったイベントリスナーの実装がポイントです。

### 2. アニメーションとインタラクション

ユーザー体験を向上させるために、Intersection Observer API を使ったスクロールアニメーションを実装しました。

```tsx
// src/components/ServiceSection.tsx の一部
'use client';

import { useEffect, useState } from 'react';

export function ServiceSection() {
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        setIsVisible(entry.isIntersecting);
      },
      { threshold: 0.1 }
    );

    const element = document.getElementById('service-section');
    if (element) observer.observe(element);

    return () => {
      if (element) observer.unobserve(element);
    };
  }, []);

  return (
    <section id="service-section" className="py-24 bg-white">
      <div 
        className={`transform transition-all duration-700 ${
          isVisible ? 'translate-y-0 opacity-100' : 'translate-y-4 opacity-0'
        }`}
      >
        {/* コンテンツ */}
      </div>
    </section>
  );
}
```

各セクションが画面に入ってきたときにフェードインするアニメーションを実装しました。これにより、ユーザーの注目を集めつつ、視覚的にも魅力的なサイトになりました。

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

お問い合わせフォームは、Next.js の API Routes と Nodemailer を使って実装しました。

```tsx
// src/app/api/contact/route.ts
import { NextResponse } from 'next/server';
import nodemailer from 'nodemailer';

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const { company, name, email, phone, message, type } = body;

    // 入力検証
    if (!company || !name || !email || !message) {
      return NextResponse.json(
        { error: '必須項目が入力されていません' },
        { status: 400 }
      );
    }

    // メール転送用のトランスポーター設定
    const transporter = nodemailer.createTransport({
      host: process.env.SMTP_HOST,
      port: Number(process.env.SMTP_PORT) || 587,
      secure: process.env.SMTP_SECURE === 'true',
      auth: {
        user: process.env.SMTP_USER,
        pass: process.env.SMTP_PASSWORD,
      },
    });

    // メール送信
    await transporter.sendMail({
      from: process.env.MAIL_FROM,
      to: process.env.MAIL_TO,
      subject: `【お問い合わせ】${company}様`,
      text: `
【お問い合わせ内容】
会社名: ${company}
お名前: ${name}
メール: ${email}
電話番号: ${phone || '未入力'}
お問い合わせ種別: ${type || '未選択'}

【メッセージ】
${message}
      `,
      html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
  <h2>お問い合わせがありました</h2>
  
  <table style="width: 100%; border-collapse: collapse;">
    <tr>
      <th style="text-align: left; padding: 8px;">会社名</th>
      <td style="padding: 8px;">${company}</td>
    </tr>
    <!-- 他のフィールド -->
  </table>
  
  <h3>メッセージ</h3>
  <div style="white-space: pre-wrap; background: #f9f9f9; padding: 15px; border-radius: 4px;">
    ${message.replace(/\n/g, '<br>')}
  </div>
</div>
      `,
    });

    return NextResponse.json({
      message: 'お問い合わせを受け付けました',
      success: true,
    });
  } catch (error) {
    console.error('メール送信エラー:', error);
    return NextResponse.json(
      { error: 'お問い合わせの送信に失敗しました' },
      { status: 500 }
    );
  }
}
```

フロントエンド側では、フォームの状態管理とバリデーションを実装しました：

```tsx
// src/app/contact/page.tsx の一部
'use client';

import { useState } from 'react';

export default function ContactPage() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    company: '',
    phone: '',
    message: '',
    type: 'general'
  });
  const [formErrors, setFormErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitStatus, setSubmitStatus] = useState('idle');

  // 入力検証関数
  const validateForm = () => {
    const errors = {};
    
    if (!formData.name.trim()) {
      errors.name = 'お名前を入力してください';
    }
    
    if (!formData.email.trim()) {
      errors.email = 'メールアドレスを入力してください';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      errors.email = '有効なメールアドレスを入力してください';
    }
    
    // その他のバリデーション
    
    setFormErrors(errors);
    return Object.keys(errors).length === 0;
  };

  // フォーム送信処理
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!validateForm()) {
      return;
    }
    
    setIsSubmitting(true);
    
    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData),
      });
      
      const result = await response.json();
      
      if (response.ok) {
        setSubmitStatus('success');
        // フォームのリセット
      } else {
        setSubmitStatus('error');
      }
    } catch (error) {
      setSubmitStatus('error');
    } finally {
      setIsSubmitting(false);
    }
  };

  // JSX フォーム実装
}
```

この実装により、入力検証から送信完了までのフローをスムーズに提供できました。また、エラー状態の適切な処理も重視しました。

### 4. Firebaseへのデプロイ

プロジェクトを Firebase Hosting にデプロイするために、以下の設定を行いました：

```json
// firebase.json
{
  "hosting": {
    "public": "out",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  },
  "functions": [
    {
      "source": "functions",
      "codebase": "default",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log",
        "*.local"
      ],
      "predeploy": [
        "npm --prefix \"$RESOURCE_DIR\" run lint",
        "npm --prefix \"$RESOURCE_DIR\" run build"
      ]
    }
  ]
}
```

Next.js の静的エクスポート設定を有効にするために、`next.config.ts` も調整しました：

```typescript
// next.config.ts
import { NextConfig } from 'next';

const config: NextConfig = {
  // ESLintチェックを無効化
  eslint: {
    ignoreDuringBuilds: true,
  },
  // TypeScriptの型チェックを無効化
  typescript: {
    ignoreBuildErrors: true,
  },
  // 静的エクスポートを有効化（Firebase Hostingにデプロイする場合に推奨）
  output: 'export',
  // 画像最適化を無効化（静的エクスポート時に必要）
  images: {
    unoptimized: true,
  },
  // その他の設定
  reactStrictMode: true,
};

export default config;
```

デプロイコマンドは以下のように実行しました：

```bash
npm run build
firebase deploy
```

## 苦労したポイントと解決策

### 1. 動的コンテンツと静的生成の両立

Next.js の SSG を使いながらも、問い合わせフォームのような動的な機能を実装するのに苦労しました。最終的に、フロントエンドはすべて静的に生成し、API 部分だけを Firebase Functions として実装することで解決しました。

### 2. パフォーマンス最適化

大量のアニメーションとインタラクションを実装しつつも、パフォーマンスを落とさないための工夫として：

1. 画像の最適化
2. コンポーネントの適切な分割とメモ化
3. 無駄なリレンダリングの削減

これらを意識して実装しました。特に、Intersection Observer を使ったレイジーローディングは効果的でした。

### 3. TypeScriptの型定義

文系出身ということもあり、TypeScript の厳格な型システムには苦労しました。特に、フォームの状態管理における型定義は何度も調べながら実装しました：

```typescript
// フォームの型定義
interface FormData {
  name: string;
  email: string;
  company: string;
  phone: string;
  message: string;
  type: string;
}

interface FormErrors {
  name?: string;
  email?: string;
  company?: string;
  message?: string;
}
```

## 学んだこと

このプロジェクトを通じて、以下のことを学びました：

1. クライアントの要望をしっかりとヒアリングし、技術的な実装に落とし込む力
2. モダンなフロントエンド開発における状態管理の重要性
3. パフォーマンスとユーザー体験のバランスの取り方
4. TypeScriptによる型安全なコーディングの利点

特に、文系出身の私にとって TypeScript の型システムは最初は障壁でしたが、今では型安全な開発の恩恵を強く実感しています。

## 今後の改善点

プロジェクトを振り返って、今後改善したい点としては以下が挙げられます：

1. コンテンツ管理システムの導入 (Contentful や microCMS など)
2. パフォーマンス計測とさらなる最適化
3. アクセシビリティの向上
4. テスト自動化の導入

## おわりに

企業の採用サイト制作という、責任の大きいプロジェクトを通じて、技術的にも人間的にも大きく成長できたと感じています。

Next.js と Firebase の組み合わせは、特にフロントエンド寄りの開発者にとって強力なスタックだと実感しました。素早くプロトタイプを作成でき、本番環境へのデプロイも簡単なため、今後も積極的に活用していきたいと思います。

最後までお読みいただき、ありがとうございました。何か質問やコメントがあれば、ぜひ下記に投稿してください。

## 参考リンク

- [Next.js 公式ドキュメント](https://nextjs.org/docs)
- [Firebase Hosting](https://firebase.google.com/docs/hosting)
- [TailwindCSS](https://tailwindcss.com/docs)
- [Intersection Observer API](https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API)