T-CREATOR

Nuxt で始める静的サイト生成(SSG)入門

Nuxt で始める静的サイト生成(SSG)入門

Web サイトの高速化と SEO 対策が重要視される現代において、静的サイト生成(SSG)は多くの開発者にとって魅力的な選択肢となっています。特に Nuxt を使った SSG は、Vue.js の豊富なエコシステムを活用しながら、パフォーマンスと開発体験の両方を追求できる素晴らしい技術です。

この記事では、Nuxt を使った静的サイト生成の基礎から実践まで、段階的に学んでいきます。初心者の方でも安心して取り組めるよう、実際に発生するエラーとその解決方法も含めて詳しく解説していきます。

静的サイト生成(SSG)とは

従来の動的サイトとの違い

従来の Web サイトは、ユーザーがアクセスするたびにサーバーで HTML を生成する動的サイトが主流でした。しかし、この方式にはいくつかの課題があります。

動的サイトの課題:

  • サーバー負荷が高い
  • ページ読み込みが遅い
  • SEO 対策が複雑
  • サーバーコストが高い

一方、静的サイト生成(SSG)では、ビルド時に HTML ファイルを事前に生成します。これにより、ユーザーがアクセスした際には既に完成した HTML ファイルを配信するだけで済むため、大幅な高速化が実現できます。

SSG のメリットとデメリット

メリット:

  • ページ読み込みが高速
  • SEO に有利(検索エンジンがクロールしやすい)
  • サーバーコストが安い
  • セキュリティリスクが低い
  • CDN での配信が容易

デメリット:

  • 動的なコンテンツ更新が困難
  • ビルド時間が長くなる場合がある
  • リアルタイム機能の実装が複雑

どのようなサイトに適しているか

SSG は以下のようなサイトに特に適しています:

  • ブログサイト
  • 企業のコーポレートサイト
  • ポートフォリオサイト
  • ドキュメントサイト
  • ランディングページ

Nuxt の基本概念

Nuxt とは何か

Nuxt は、Vue.js ベースのフルスタックフレームワークです。特に静的サイト生成において、優れた機能を提供しています。

Nuxt の特徴:

  • ファイルベースルーティング
  • 自動的なコード分割
  • SEO 最適化
  • 豊富なモジュールエコシステム
  • 開発体験の向上

Nuxt 2 と Nuxt 3 の違い

現在、Nuxt には 2 つのメジャーバージョンが存在します。

Nuxt 2 の特徴:

  • 安定性が高い
  • 豊富なドキュメントとコミュニティ
  • 多くのプラグインが利用可能

Nuxt 3 の特徴:

  • Vue 3 と Composition API をサポート
  • TypeScript のネイティブサポート
  • より高速なビルド
  • モダンな開発体験

新規プロジェクトでは Nuxt 3 の使用を推奨しますが、既存のプロジェクトがある場合は段階的な移行を検討してください。

ファイルベースルーティングの仕組み

Nuxt の最大の特徴の一つが、ファイルベースルーティングです。pages ディレクトリ内のファイル構造が、そのまま URL 構造になります。

bashpages/
├── index.vue          → /
├── about.vue          → /about
├── blog/
│   ├── index.vue      → /blog
│   └── [id].vue       → /blog/123
└── contact.vue        → /contact

この仕組みにより、複雑なルーティング設定を書く必要がなく、直感的にページ構造を設計できます。

開発環境の構築

Node.js と Yarn のインストール

まず、Node.js と Yarn をインストールする必要があります。

Node.js のインストール: Node.js の公式サイトから最新の LTS 版をダウンロードしてインストールしてください。

Yarn のインストール: Node.js をインストール後、以下のコマンドで Yarn をインストールします。

bashnpm install -g yarn

インストールの確認:

bashnode --version
yarn --version

Nuxt プロジェクトの作成

Yarn を使って Nuxt プロジェクトを作成します。

bashyarn create nuxt-app my-nuxt-ssg

プロジェクト作成時に以下の質問が表示されます:

yamlProject name: my-nuxt-ssg
Programming language: JavaScript
Package manager: Yarn
UI framework: Tailwind CSS
Nuxt.js modules: Axios
Linting tools: ESLint, Prettier
Testing framework: Jest
Rendering mode: Universal (SSR/SSG)
Deployment target: Static (Static/JAMstack hosting)
Version control system: Git

重要なポイント:

  • Rendering modeUniversal (SSR​/​SSG) を選択
  • Deployment targetStatic (Static​/​JAMstack hosting) を選択

これにより、静的サイト生成に最適化された設定が自動的に適用されます。

開発サーバーの起動

プロジェクトディレクトリに移動して開発サーバーを起動します。

bashcd my-nuxt-ssg
yarn dev

正常に起動すると、以下のようなメッセージが表示されます:

csharpℹ Listening on: http://localhost:3000/

ブラウザで http:​/​​/​localhost:3000 にアクセスして、Nuxt のデフォルトページが表示されることを確認してください。

基本的なページ作成

ページファイルの作成方法

Nuxt では、pages ディレクトリ内に .vue ファイルを作成するだけで、自動的にルーティングが生成されます。

基本的なページの作成:

vue<!-- pages/about.vue -->
<template>
  <div class="about-page">
    <h1>About Us</h1>
    <p>私たちについての情報です。</p>
  </div>
</template>

<script>
export default {
  name: 'AboutPage',
  head() {
    return {
      title: 'About Us - My Nuxt Site',
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: 'About us page',
        },
      ],
    };
  },
};
</script>

<style scoped>
.about-page {
  padding: 2rem;
  max-width: 800px;
  margin: 0 auto;
}
</style>

このファイルを作成すると、​/​about にアクセスできるようになります。

レイアウトの活用

Nuxt では、layouts ディレクトリ内にレイアウトファイルを作成できます。これにより、共通のヘッダーやフッターを効率的に管理できます。

デフォルトレイアウトの作成:

vue<!-- layouts/default.vue -->
<template>
  <div>
    <header class="header">
      <nav>
        <NuxtLink to="/" class="nav-link">Home</NuxtLink>
        <NuxtLink to="/about" class="nav-link"
          >About</NuxtLink
        >
        <NuxtLink to="/blog" class="nav-link"
          >Blog</NuxtLink
        >
      </nav>
    </header>

    <main class="main-content">
      <Nuxt />
    </main>

    <footer class="footer">
      <p>&copy; 2024 My Nuxt Site. All rights reserved.</p>
    </footer>
  </div>
</template>

<style scoped>
.header {
  background: #f8f9fa;
  padding: 1rem;
  border-bottom: 1px solid #e9ecef;
}

.nav-link {
  margin-right: 1rem;
  text-decoration: none;
  color: #333;
}

.nav-link:hover {
  color: #007bff;
}

.main-content {
  min-height: calc(100vh - 120px);
  padding: 2rem;
}

.footer {
  background: #f8f9fa;
  padding: 1rem;
  text-align: center;
  border-top: 1px solid #e9ecef;
}
</style>

コンポーネントの作成と使用

再利用可能なコンポーネントは components ディレクトリに作成します。

カードコンポーネントの例:

vue<!-- components/Card.vue -->
<template>
  <div class="card">
    <img
      v-if="image"
      :src="image"
      :alt="title"
      class="card-image"
    />
    <div class="card-content">
      <h3 class="card-title">{{ title }}</h3>
      <p class="card-description">{{ description }}</p>
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Card',
  props: {
    title: {
      type: String,
      required: true,
    },
    description: {
      type: String,
      default: '',
    },
    image: {
      type: String,
      default: '',
    },
  },
};
</script>

<style scoped>
.card {
  border: 1px solid #e9ecef;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: transform 0.2s;
}

.card:hover {
  transform: translateY(-2px);
}

.card-image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.card-content {
  padding: 1rem;
}

.card-title {
  margin: 0 0 0.5rem 0;
  font-size: 1.25rem;
  font-weight: 600;
}

.card-description {
  color: #6c757d;
  margin: 0;
}
</style>

コンポーネントの使用:

vue<!-- pages/index.vue -->
<template>
  <div class="home">
    <h1>Welcome to My Nuxt Site</h1>
    <div class="cards-grid">
      <Card
        title="Getting Started"
        description="Learn how to get started with Nuxt SSG"
        image="/images/getting-started.jpg"
      >
        <button class="btn">Read More</button>
      </Card>

      <Card
        title="Advanced Features"
        description="Explore advanced Nuxt features"
        image="/images/advanced.jpg"
      >
        <button class="btn">Explore</button>
      </Card>
    </div>
  </div>
</template>

<script>
export default {
  name: 'HomePage',
};
</script>

<style scoped>
.home {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

.cards-grid {
  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(300px, 1fr)
  );
  gap: 2rem;
  margin-top: 2rem;
}

.btn {
  background: #007bff;
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  cursor: pointer;
  margin-top: 1rem;
}

.btn:hover {
  background: #0056b3;
}
</style>

静的サイト生成の設定

nuxt.config.js の設定

Nuxt プロジェクトの設定は nuxt.config.js ファイルで管理します。静的サイト生成に最適化された設定を見ていきましょう。

javascript// nuxt.config.js
export default {
  // 静的サイト生成の設定
  target: 'static',

  // モード設定
  ssr: true,

  // ヘッド設定
  head: {
    title: 'My Nuxt SSG Site',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'My Nuxt static site' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },

  // CSS設定
  css: [
    '~/assets/css/main.css'
  ],

  // プラグイン設定
  plugins: [
  ],

  // モジュール設定
  modules: [
    '@nuxtjs/axios',
    '@nuxtjs/sitemap'
  ],

  // ビルド設定
  build: {
    // ビルド最適化
    optimization: {
      splitChunks: {
        chunks: 'all',
        automaticNameDelimiter: '.',
        name: undefined,
        maxSize: 244000
      }
    }
  },

  // 静的サイト生成の詳細設定
  generate: {
    // フォールバックページの設定
    fallback: true,

    // ビルド時のエラーハンドリング
    exclude: [
      /^(?!/admin).*/ // admin パスを除外
    ]
  },

  // サイトマップ設定
  sitemap: {
    hostname: 'https://yourdomain.com',
    gzip: true
  }
}

ビルド時の最適化

静的サイト生成時のパフォーマンスを向上させるための設定を追加します。

画像最適化の設定:

javascript// nuxt.config.js に追加
export default {
  // ... 既存の設定

  // 画像最適化モジュール
  modules: [
    '@nuxtjs/axios',
    '@nuxtjs/sitemap',
    '@nuxt/image',
  ],

  // 画像設定
  image: {
    provider: 'static',
    quality: 80,
    format: ['webp', 'jpg', 'png'],
  },

  // ビルド最適化
  build: {
    // コード分割の最適化
    optimization: {
      splitChunks: {
        chunks: 'all',
        automaticNameDelimiter: '.',
        name: undefined,
        maxSize: 244000,
      },
    },

    // 圧縮設定
    extend(config, { isDev, isClient }) {
      if (!isDev && isClient) {
        config.optimization.minimize = true;
      }
    },
  },
};

静的ファイルの出力

静的サイトをビルドするには、以下のコマンドを実行します。

bashyarn generate

このコマンドを実行すると、dist ディレクトリに静的ファイルが生成されます。

ビルド時のよくあるエラーと解決方法:

エラー 1: 動的ルートの生成エラー

csharpError: Could not resolve dynamic route

解決方法:

javascript// nuxt.config.js
export default {
  generate: {
    routes() {
      return ['/blog/1', '/blog/2', '/blog/3'];
    },
  },
};

エラー 2: 画像パスの解決エラー

javascriptError: Cannot resolve module './image.jpg'

解決方法:

vue<!-- 正しい画像の参照方法 -->
<template>
  <img src="~/assets/images/logo.png" alt="Logo" />
</template>

実践的なサイト構築

ブログサイトの作成例

実際のブログサイトを作成しながら、Nuxt SSG の実践的な使い方を学びましょう。

ブログ記事のデータ構造:

javascript// content/blog/
// article-1.md
---
title: "Nuxt SSG で高速なブログを作成"
description: "Nuxt を使った静的サイト生成でブログを作成する方法"
date: "2024-01-15"
tags: ["nuxt", "ssg", "vue"]
---

# Nuxt SSG で高速なブログを作成

この記事では、Nuxt を使った静的サイト生成でブログを作成する方法を解説します...

ブログ一覧ページの作成:

vue<!-- pages/blog/index.vue -->
<template>
  <div class="blog-list">
    <h1>Blog Posts</h1>
    <div class="posts-grid">
      <article
        v-for="post in posts"
        :key="post.slug"
        class="post-card"
      >
        <NuxtLink :to="`/blog/${post.slug}`">
          <img
            v-if="post.image"
            :src="post.image"
            :alt="post.title"
            class="post-image"
          />
          <div class="post-content">
            <h2 class="post-title">{{ post.title }}</h2>
            <p class="post-excerpt">
              {{ post.description }}
            </p>
            <div class="post-meta">
              <span class="post-date">{{
                formatDate(post.date)
              }}</span>
              <span class="post-tags">
                <span
                  v-for="tag in post.tags"
                  :key="tag"
                  class="tag"
                >
                  {{ tag }}
                </span>
              </span>
            </div>
          </div>
        </NuxtLink>
      </article>
    </div>
  </div>
</template>

<script>
export default {
  name: 'BlogList',
  async asyncData({ $content }) {
    const posts = await $content('blog')
      .sortBy('date', 'desc')
      .fetch();

    return { posts };
  },
  methods: {
    formatDate(date) {
      return new Date(date).toLocaleDateString('ja-JP');
    },
  },
  head() {
    return {
      title: 'Blog - My Nuxt Site',
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: 'Blog posts',
        },
      ],
    };
  },
};
</script>

<style scoped>
.blog-list {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

.posts-grid {
  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(350px, 1fr)
  );
  gap: 2rem;
  margin-top: 2rem;
}

.post-card {
  border: 1px solid #e9ecef;
  border-radius: 8px;
  overflow: hidden;
  transition: transform 0.2s;
}

.post-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.post-image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.post-content {
  padding: 1.5rem;
}

.post-title {
  margin: 0 0 0.5rem 0;
  font-size: 1.25rem;
  color: #333;
}

.post-excerpt {
  color: #6c757d;
  margin: 0 0 1rem 0;
  line-height: 1.6;
}

.post-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.875rem;
}

.post-date {
  color: #6c757d;
}

.tag {
  background: #e9ecef;
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  margin-left: 0.5rem;
  font-size: 0.75rem;
}
</style>

画像の最適化

Nuxt では、画像の最適化を自動的に行うことができます。

画像最適化の実装:

vue<!-- components/OptimizedImage.vue -->
<template>
  <div class="optimized-image">
    <img
      :src="optimizedSrc"
      :alt="alt"
      :loading="loading"
      :class="imageClass"
      @load="onImageLoad"
      @error="onImageError"
    />
  </div>
</template>

<script>
export default {
  name: 'OptimizedImage',
  props: {
    src: {
      type: String,
      required: true,
    },
    alt: {
      type: String,
      default: '',
    },
    loading: {
      type: String,
      default: 'lazy',
    },
    imageClass: {
      type: String,
      default: '',
    },
  },
  computed: {
    optimizedSrc() {
      // 画像の最適化処理
      if (this.src.startsWith('http')) {
        return this.src;
      }
      return this.src;
    },
  },
  methods: {
    onImageLoad() {
      this.$emit('load');
    },
    onImageError() {
      this.$emit('error');
    },
  },
};
</script>

<style scoped>
.optimized-image {
  position: relative;
  overflow: hidden;
}

.optimized-image img {
  width: 100%;
  height: auto;
  transition: transform 0.3s ease;
}

.optimized-image img:hover {
  transform: scale(1.05);
}
</style>

SEO 対策の実装

SEO 対策は静的サイト生成の大きなメリットの一つです。Nuxt では、簡単に SEO 対策を実装できます。

SEO 最適化の実装:

vue<!-- layouts/default.vue に追加 -->
<script>
export default {
  head() {
    return {
      htmlAttrs: {
        lang: 'ja',
      },
      meta: [
        { charset: 'utf-8' },
        {
          name: 'viewport',
          content: 'width=device-width, initial-scale=1',
        },
        {
          hid: 'description',
          name: 'description',
          content:
            this.$nuxt.$options.head.meta.find(
              (m) => m.hid === 'description'
            )?.content || 'Default description',
        },
        {
          hid: 'og:title',
          property: 'og:title',
          content:
            this.$nuxt.$options.head.title ||
            'Default title',
        },
        {
          hid: 'og:description',
          property: 'og:description',
          content:
            this.$nuxt.$options.head.meta.find(
              (m) => m.hid === 'description'
            )?.content || 'Default description',
        },
        {
          hid: 'og:type',
          property: 'og:type',
          content: 'website',
        },
        {
          hid: 'og:url',
          property: 'og:url',
          content: this.$route.fullPath,
        },
        {
          hid: 'twitter:card',
          name: 'twitter:card',
          content: 'summary_large_image',
        },
      ],
      link: [
        {
          rel: 'canonical',
          href: `https://yourdomain.com${this.$route.path}`,
        },
      ],
    };
  },
};
</script>

構造化データの追加:

vue<!-- components/StructuredData.vue -->
<template>
  <script
    type="application/ld+json"
    v-html="structuredData"
  ></script>
</template>

<script>
export default {
  name: 'StructuredData',
  props: {
    data: {
      type: Object,
      required: true,
    },
  },
  computed: {
    structuredData() {
      return JSON.stringify(this.data);
    },
  },
};
</script>

ページでの使用例:

vue<!-- pages/blog/[slug].vue -->
<template>
  <article class="blog-post">
    <StructuredData :data="structuredData" />
    <h1>{{ post.title }}</h1>
    <div class="post-meta">
      <time :datetime="post.date">{{
        formatDate(post.date)
      }}</time>
    </div>
    <div class="post-content" v-html="post.body"></div>
  </article>
</template>

<script>
export default {
  name: 'BlogPost',
  async asyncData({ $content, params }) {
    const post = await $content(
      `blog/${params.slug}`
    ).fetch();
    return { post };
  },
  computed: {
    structuredData() {
      return {
        '@context': 'https://schema.org',
        '@type': 'BlogPosting',
        headline: this.post.title,
        description: this.post.description,
        datePublished: this.post.date,
        author: {
          '@type': 'Person',
          name: 'Your Name',
        },
      };
    },
  },
  head() {
    return {
      title: `${this.post.title} - My Blog`,
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: this.post.description,
        },
        {
          hid: 'og:title',
          property: 'og:title',
          content: this.post.title,
        },
        {
          hid: 'og:description',
          property: 'og:description',
          content: this.post.description,
        },
      ],
    };
  },
};
</script>

デプロイと運用

Vercel へのデプロイ

Vercel は Nuxt の静的サイト生成に最適化されたプラットフォームです。

Vercel でのデプロイ手順:

  1. Vercel CLI のインストール:
bashyarn global add vercel
  1. プロジェクトの設定:
json// vercel.json
{
  "buildCommand": "yarn generate",
  "outputDirectory": "dist",
  "framework": "nuxt",
  "routes": [
    {
      "src": "/admin/(.*)",
      "dest": "/admin/index.html"
    }
  ]
}
  1. デプロイの実行:
bashvercel

よくあるデプロイエラーと解決方法:

エラー 1: ビルドエラー

javascriptError: Build failed

解決方法:

bash# 依存関係の再インストール
rm -rf node_modules yarn.lock
yarn install

# キャッシュのクリア
yarn cache clean

エラー 2: 環境変数の設定エラー

javascriptError: Environment variable not found

解決方法: Vercel のダッシュボードで環境変数を設定するか、.env ファイルを作成します。

bash# .env
NUXT_ENV_API_URL=https://api.example.com

GitHub Pages での公開

GitHub Pages でも簡単にデプロイできます。

GitHub Actions の設定:

yaml# .github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '18'

      - name: Install dependencies
        run: yarn install

      - name: Build
        run: yarn generate

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

nuxt.config.js の設定:

javascript// nuxt.config.js
export default {
  // ... 既存の設定

  // GitHub Pages 用の設定
  router: {
    base:
      process.env.NODE_ENV === 'production'
        ? '/your-repo-name/'
        : '/',
  },

  // 静的サイト生成の設定
  generate: {
    fallback: true,
  },
};

継続的デプロイメントの設定

継続的デプロイメントを設定することで、コードの変更が自動的に本番環境に反映されます。

Netlify での設定:

toml# netlify.toml
[build]
  command = "yarn generate"
  publish = "dist"

[build.environment]
  NODE_VERSION = "18"

[[redirects]]
  from = "/admin/*"
  to = "/admin/index.html"
  status = 200

環境変数の管理:

bash# .env.local
NUXT_ENV_API_URL=https://api.example.com
NUXT_ENV_ANALYTICS_ID=GA_TRACKING_ID

ビルド時の最適化:

javascript// nuxt.config.js
export default {
  // ... 既存の設定

  // ビルド最適化
  build: {
    // 圧縮設定
    extractCSS: true,

    // 最適化設定
    optimization: {
      splitChunks: {
        chunks: 'all',
        automaticNameDelimiter: '.',
        name: undefined,
        maxSize: 244000,
      },
    },

    // キャッシュ設定
    cache: true,

    // 並列処理
    parallel: true,
  },
};

まとめ

この記事では、Nuxt を使った静的サイト生成の基礎から実践まで、段階的に学んできました。

学んだことの振り返り:

  1. 静的サイト生成の概念とメリット

    • 高速なページ読み込み
    • SEO への有利な影響
    • コスト効率の良さ
  2. Nuxt の基本概念

    • ファイルベースルーティング
    • コンポーネントシステム
    • レイアウト機能
  3. 開発環境の構築

    • Node.js と Yarn の設定
    • Nuxt プロジェクトの作成
    • 開発サーバーの起動
  4. 実践的な開発

    • ページとコンポーネントの作成
    • 画像最適化
    • SEO 対策の実装
  5. デプロイと運用

    • 各種プラットフォームでのデプロイ
    • 継続的デプロイメント
    • パフォーマンス最適化

次のステップ:

  • 高度な機能の学習: 動的ルーティング、API との連携、状態管理
  • パフォーマンス最適化: コード分割、遅延読み込み、キャッシュ戦略
  • セキュリティ対策: CSP の設定、HTTPS の強制、セキュリティヘッダー
  • 監視と分析: Google Analytics、エラー監視、パフォーマンス測定

Nuxt を使った静的サイト生成は、現代の Web 開発において非常に強力なツールです。この記事で学んだ基礎を土台に、さらに高度な機能を活用して、ユーザーにとって素晴らしい体験を提供する Web サイトを作成してください。

開発の過程でエラーに遭遇した際は、この記事で紹介した解決方法を参考にしていただき、Nuxt の豊富なドキュメントやコミュニティリソースも活用してください。

関連リンク