4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

📚【学習・個人利用向け】Vercel Hobbyプランで作るNext.jsサイト開発ガイド|商用移行の判断基準も解説

4
Last updated at Posted at 2025-03-10

こんにちは、@YushiYamamotoです!

⚠️ 重要:ご利用前に必ずご確認ください
Vercel Hobby プランは個人・非商用利用専用です。
クライアントワーク・企業サイト・収益化を伴うプロジェクトには
Pro plan($20/月)以上が必要です。
公式: Fair Use Guidelines

「Next.jsでサイトを作りたいけど、学習用途でまずコストをかけずに試したい」
という方に向けて、Vercel Hobby プランの活用方法を体系的にまとめました。

本記事の対象読者

  • 個人学習・ポートフォリオ・非商用プロジェクトを構築したい方
  • Vercel × Next.js の基礎を実践的に習得したい開発者
  • 将来的な商用移行の判断基準を知りたい方

📋 目次

  1. Vercel Hobbyプランの可能性と制約
  2. サーバーレスアーキテクチャの基礎知識
  3. 開発環境のセットアップ
  4. サイトの実装手順
  5. Hobbyプランの制限を理解するテクニック
  6. パフォーマンス最適化
  7. Pro planへの移行判断基準
  8. まとめ

Vercel Hobbyプランの可能性と制約

Vercelは Next.js の開発元として知られるプラットフォームです。Hobby プランは個人・非商用利用に限定された無料プランであり、学習や個人プロジェクトには非常に強力な環境を提供しています。

Hobbyプランで利用できる主な機能 🎁

  • 自動デプロイとプレビュー環境: GitHubとの連携によるCICD
  • グローバルCDN: 世界各地に分散したエッジネットワーク
  • サーバーレス関数: APIルートやサーバーサイドレンダリング
  • SSL証明書: 自動更新される無料のSSL対応
  • カスタムドメイン: 独自ドメインの接続
  • 基本的なアナリティクス: デプロイごとの基本パフォーマンス測定

Hobbyプランの利用制限 🚧

項目 制限値
デプロイ回数 月100回まで
サーバーレス関数の実行時間 10秒まで
同時サーバーレス関数実行 最大6つ
帯域幅 月100GB
ビルド時間 最大45分
利用用途 個人・非商用のみ

⛔ Hobbyプランで禁止されている用途

VercelのFair Use Guidelinesに基づき、以下はHobbyプランでの利用が禁止されています:

  • 商品・サービスの販売告知・広告目的のサイト
  • 決済・課金機能を持つサービス
  • 報酬を受けてのクライアントワーク(コード執筆・ホスティングを含む)
  • アフィリエイトリンクが主目的のサイト
  • Google AdSense等の広告掲載
  • 企業のコーポレートサイト

上記に該当する場合は Pro plan($20/月)以上 が必要です。

サーバーレスアーキテクチャの基礎知識

従来のサーバー管理と比較して、サーバーレスアーキテクチャがいかに開発者の負担を軽減するかを簡単な図で説明します:

【従来のアーキテクチャ】
開発者 → サーバー構築 → OS設定 → ミドルウェア → アプリケーション → デプロイ → 監視・保守

【サーバーレスアーキテクチャ】
開発者 → アプリケーション → デプロイ

サーバーレスのメリット 🚀

  1. インフラ管理不要: サーバー構築・保守の負担がゼロ
  2. スケーラビリティ: トラフィックに応じて自動スケール
  3. コスト効率: 使った分だけ支払う(学習用途なら完全無料)
  4. セキュリティ: インフラレベルのセキュリティをプラットフォームが担保

Vercelと相性の良いフレームワーク 💻

  • Next.js: Vercel開発の最強React Framework
  • Nuxt.js: Vue.jsベースのフレームワーク
  • SvelteKit: Svelteベースのフレームワーク
  • Astro: 高パフォーマンスなスタティックサイトジェネレータ

今回は最も統合が進んだ Next.js で実装していきます。

開発環境のセットアップ

1. プロジェクトの初期化 🏗️

# Next.jsプロジェクトの作成
npx create-next-app@latest my-site
cd my-site

# 必要なパッケージのインストール
npm install @vercel/analytics

2. GitHubリポジトリの設定 📁

git init
git add .
git commit -m "Initial commit"

git remote add origin https://github.com/yourusername/my-site.git
git push -u origin main

3. Vercelアカウントの作成とプロジェクト連携 🔄

  1. Vercel公式サイトでアカウント作成(GitHubアカウント連携推奨)
  2. ダッシュボードから「New Project」をクリック
  3. GitHubリポジトリをインポート
  4. 基本設定を確認してデプロイ

4. next.config.jsの最適化 ⚙️

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: ['images.unsplash.com', 'via.placeholder.com'],
    formats: ['image/avif', 'image/webp'],
  },
  experimental: {
    optimizeCss: true,
    scrollRestoration: true,
    legacyBrowsers: false,
  },
}

module.exports = nextConfig

サイトの実装手順

1. プロジェクト構造の設計 📂

my-site/
├── public/
│   ├── images/
│   └── favicon.ico
├── src/
│   ├── app/
│   ├── components/
│   │   ├── layout/
│   │   ├── sections/
│   │   └── ui/
│   ├── lib/
│   ├── styles/
│   └── types/
├── next.config.js
└── package.json

2. 共通レイアウトの実装 📱

// src/app/layout.tsx
import './globals.css'
import { Inter } from 'next/font/google'
import Header from '@/components/layout/Header'
import Footer from '@/components/layout/Footer'
import { Analytics } from '@vercel/analytics/react'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'My Site | ポートフォリオ',
  description: '個人ポートフォリオサイトです。',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <body className={inter.className}>
        <Header />
        <main>{children}</main>
        <Footer />
        <Analytics />
      </body>
    </html>
  )
}

3. ヒーローセクション 🖼️

// src/components/sections/HeroSection.tsx
import Image from 'next/image'
import Link from 'next/link'

export default function HeroSection() {
  return (
    <section className="relative h-[80vh] flex items-center">
      <div className="absolute inset-0 z-0">
        <Image
          src="/images/hero-bg.jpg"
          alt="ヒーロー画像"
          fill
          priority
          sizes="100vw"
          style={{ objectFit: 'cover' }}
          quality={85}
        />
        <div className="absolute inset-0 bg-black opacity-50" />
      </div>

      <div className="container mx-auto px-4 z-10 text-white">
        <h1 className="text-4xl md:text-6xl font-bold mb-6">
          ポートフォリオサイトへようこそ
        </h1>
        <p className="text-xl md:text-2xl mb-8 max-w-2xl">
          Next.js と Vercel で構築した個人サイトです。
        </p>
        <Link
          href="/contact"
          className="bg-white text-blue-900 px-8 py-3 rounded-full font-bold text-lg hover:bg-blue-100 transition duration-300"
        >
          お問い合わせ
        </Link>
      </div>
    </section>
  )
}

4. サーバーレス関数を活用したお問い合わせフォーム 📨

// src/app/api/contact/route.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const { name, email, message } = body

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

    // 外部サービス(SendGrid等)との連携箇所
    // const response = await fetch('https://api.sendgrid.com/v3/mail/send', {...})

    if (process.env.NODE_ENV === 'development') {
      console.log('お問い合わせを受信:', { name, email, message })
    }

    return NextResponse.json(
      { success: true, message: 'お問い合わせを受け付けました' },
      { status: 200 }
    )
  } catch (error) {
    console.error('Error:', error)
    return NextResponse.json(
      { error: 'エラーが発生しました' },
      { status: 500 }
    )
  }
}
// src/components/sections/ContactForm.tsx
'use client'
import { useState, FormEvent } from 'react'

export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  })

  const [status, setStatus] = useState({
    isSubmitting: false,
    isSubmitted: false,
    isError: false,
    message: ''
  })

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault()
    setStatus({ ...status, isSubmitting: true })

    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      })

      const data = await response.json()
      if (!response.ok) throw new Error(data.error || '送信に失敗しました')

      setStatus({
        isSubmitting: false,
        isSubmitted: true,
        isError: false,
        message: 'お問い合わせありがとうございます。'
      })

      setFormData({ name: '', email: '', message: '' })

    } catch (error) {
      setStatus({
        isSubmitting: false,
        isSubmitted: true,
        isError: true,
        message: error instanceof Error ? error.message : 'エラーが発生しました'
      })
    }
  }

  return (
    <form onSubmit={handleSubmit} className="max-w-lg mx-auto">
      {status.isSubmitted && (
        <div className={`p-4 mb-4 rounded ${status.isError ? 'bg-red-100' : 'bg-green-100'}`}>
          {status.message}
        </div>
      )}

      <div className="mb-4">
        <label className="block mb-2">お名前</label>
        <input
          type="text"
          value={formData.name}
          onChange={(e) => setFormData({...formData, name: e.target.value})}
          required
          className="w-full p-3 border border-gray-300 rounded-md"
        />
      </div>

      <div className="mb-4">
        <label className="block mb-2">メールアドレス</label>
        <input
          type="email"
          value={formData.email}
          onChange={(e) => setFormData({...formData, email: e.target.value})}
          required
          className="w-full p-3 border border-gray-300 rounded-md"
        />
      </div>

      <div className="mb-4">
        <label className="block mb-2">メッセージ</label>
        <textarea
          value={formData.message}
          onChange={(e) => setFormData({...formData, message: e.target.value})}
          required
          rows={5}
          className="w-full p-3 border border-gray-300 rounded-md"
        />
      </div>

      <button
        type="submit"
        disabled={status.isSubmitting}
        className="w-full bg-blue-600 text-white py-3 rounded-md hover:bg-blue-700 transition"
      >
        {status.isSubmitting ? '送信中...' : '送信する'}
      </button>
    </form>
  )
}

Hobbyプランの制限を理解するテクニック

1. サーバーレス関数の最適化 ⚡

10秒の実行時間制限を意識した実装:

export async function POST(request: NextRequest) {
  const body = await request.json()

  // 即座にレスポンスを返す
  const response = NextResponse.json(
    { success: true, message: 'リクエストを受け付けました' },
    { status: 200 }
  )

  // 非同期で軽量な後処理(10秒超過に注意)
  processDataAsync(body).catch(console.error)

  return response
}

async function processDataAsync(data: unknown) {
  // 軽量な後処理のみ
}

2. 画像最適化の戦略 🖼️

// next.config.js
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'res.cloudinary.com', // 外部CDN活用で最適化回数を節約
      },
    ],
    formats: ['image/avif', 'image/webp'],
  },
}

3. デプロイ回数の節約 🚀

# ローカルで本番環境を十分に検証してからデプロイ
npm run build
npm run start

# 複数変更をまとめてプッシュ(デプロイ回数削減)
git commit -m "バッチ更新: コンテンツ追加とバグ修正"
git push origin main

パフォーマンス最適化

1. Core Web Vitalsの最適化 📊

// src/app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  preload: true,
})

2. バンドルサイズの縮小 📦

// next.config.js
const nextConfig = {
  swcMinify: true,
  webpack: (config, { dev, isServer }) => {
    if (!dev && !isServer) {
      config.optimization.usedExports = true
    }
    return config
  },
}

3. ISRとSWRの活用 🔄

// src/app/blog/[slug]/page.tsx
export const revalidate = 3600 // 1時間ごとに再生成

// components/BlogContent.tsx
'use client'
import useSWR from 'swr'

export default function BlogContent({ initialData, slug }) {
  const { data } = useSWR(`/api/blog/${slug}`, fetcher, {
    fallbackData: initialData,
    revalidateOnFocus: false,
  })

  return (
    <article>
      <h1>{data.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: data.content }} />
    </article>
  )
}

Pro planへの移行判断基準

Hobby プランで学習・検証を終えたら、以下の基準でPro plan移行を検討してください。

判断基準 Hobby(無料) Pro($20/月〜)
用途 個人・非商用のみ 商用利用OK
クライアントワーク ❌ 禁止 ✅ 可能
企業サイト ❌ 禁止 ✅ 可能
収益化サービス ❌ 禁止 ✅ 可能
チーム開発 ❌ 不可 ✅ 可能
サーバーレス実行時間 10秒 60秒
帯域幅 100GB/月 1TB/月

移行タイミングの目安

  • クライアントから報酬を受けるプロジェクトが発生した時点で即時移行
  • 月間収益がサイトから発生し始めた時点
  • チームでの開発が始まった時点

Pro plan の $20/月 はクライアントワーク1件で十分回収できるコストです。適切なプランを選択することが長期的な運用・信頼性の担保につながります。

まとめ

本記事のポイント 📌

  1. Hobby プランは非商用・個人利用専用: 商用用途は必ずPro plan以上を使用すること
  2. 学習・検証環境として非常に強力: 個人ポートフォリオや技術習得には最適
  3. Next.js との深い統合: Vercel開発元のプラットフォームならではのDX
  4. Pro移行の判断基準を明確に: 商用利用が発生した時点で迷わず移行

Vercel Hobby プランは個人開発者の学習・検証環境として非常に優れています。ただし商用利用は規約で明確に禁止されているため、クライアントワークや収益化を伴うプロジェクトには必ず Pro plan 以上をご利用ください。

次回は「Next.js ISR を活用したハイパフォーマンスブログの構築方法」をご紹介する予定です。乞うご期待!

4
7
2

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
4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?