テクノロジー

【実務レベル】Next.jsを一般的な共有サーバーで公開する完全手順|SSG出力の注意点と対策

【実務レベル】Next.jsを一般的な共有サーバーで公開する完全手順|SSG出力の注意点と対策

「Next.jsで作りたいが、すでにロリポップやエックスサーバーの契約がある」「Vercelに移すほど予算も運用も変えられない」。現場ではよく聞く話です。共有サーバーは基本的にPHP前提で、Node.jsを常駐させるSSRやISRは使えません。

結論から言うと、共有サーバーでNext.jsを公開するなら SSG(静的エクスポート)が前提です。output: 'export' でHTMLを書き出し、outpublic_html に置く構成です。

この記事では、フリーランスのWebディレクター兼エンジニアとして、実際に共有サーバーへNext.jsを納品した手順と、そこで必ず踏む罠(画像最適化、API Routes、404)をまとめています。

なぜ共有サーバー × Next.js(SSG)なのか

VercelやCloudflareは便利ですが、既存契約を残したい案件では「今あるサーバーに静的ファイルを置く」方が早いです。

  • サーバー契約を増やさず、固定費を抑えられる
  • HTML配信なので表示が速い
  • WordPressを残したまま、フロントだけモダン化できる

実務では「Nodeが動かない」は欠点ではなく、制約です。制約を先に設計に入れると、後からISR前提のコードを書いて詰むことが減ります。

方式

共有サーバー

向いていること

SSR / ISR

不可(Node常駐が必要)

Vercelなど

SSG(output: 'export'

コーポレート、ブログ、ポートフォリオ

構築手順

1. 静的エクスポートを有効にする

App Routerでも Pages Routerでも、設定の核は同じです。共有サーバー向けは次の3点がセットです。

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "export",
  distDir: "out",
  trailingSlash: true,
  images: {
    unoptimized: true,
  },
};

export default nextConfig;
  • output: "export" … サーバーなしで動くHTMLを出す
  • trailingSlash: true/about/about/index.html にする。共有サーバーの404対策でほぼ必須
  • images.unoptimized: true … 後述する next/image の罠を避ける

2. ビルドして out を作る

pnpm build
# または
npm run build

成功するとプロジェクト直下に out ができます。中身は index.html と各ページのフォルダです。このディレクトリだけをサーバーに上げます。node_modules は不要です。

{
  "scripts": {
    "build": "next build",
    "export": "next build"
  }
}

3. 共有サーバーへアップロードする

アップロード先はサーバーによって名前が違いますが、公開ディレクトリはだいたい次のどれかです。

  • mixhost: public_html
  • エックスサーバー: ドメイン名/public_html

実務での手順は次のとおりです。

  1. ローカルで pnpm build する
  2. SFTP(FileZilla、Cyberduck、VS Code / Cursor のSFTP拡張)で接続する
  3. out の中身を public_html にコピーする(out フォルダごとではなく中身)
  4. ブラウザでトップと下層URLを確認する

out/about/index.html はサーバー上では public_html/about/index.html です。out ごと上げると example.com/out/ になって事故ります。

SSHが使えるプランなら、手元でビルドして rsync する方が差分更新できて安全です。

pnpm build
rsync -avz --delete ./out/ user@example.com:~/public_html/

現場でハマりがちなポイント

next/image がそのまま使えない

Next.jsの画像最適化は、実行時にNodeが画像を処理する仕組みです。SSGにはそのサーバーがありません。設定しないとビルドエラー、または本番で画像が壊れます。

実務では次のどちらかです。

  1. images.unoptimized: true にして、<Image> を普通のimgとして出す
  2. microCMSやImgixのURLパラメータでリサイズする(例: ?w=800&q=80
import Image from "next/image";

export default function Hero({ src, alt }: { src: string; alt: string }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={450}
    />
  );
}

microCMSの画像なら、ドメイン許可は静的エクスポートではほぼ関係なくなります。unoptimized にした時点で最適化プロキシを通らないためです。代わりにCMS側のリサイズを使います。

const src = `${eyecatch.url}?w=800&q=80&fm=webp`;

API Routes が使えない

app/api/pages/api/ はNodeサーバーです。静的エクスポートではビルド時点で弾かれます。問い合わせフォームをNextのAPIに投げていると、ここで初めて気づくことが多いです。

代替は次です。

  • お問い合わせ: Formspree、Googleフォーム、サーバー側のPHP、またはWordPressのREST
  • 記事取得: ビルド時にmicroCMSからfetchする(閲覧時にAPIを叩かない)
  • Webhook再生成: 共有サーバー単体では難しい。GitHub Actionsでビルドしてrsyncする
import { createClient } from "microcms-js-sdk";

const client = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN!,
  apiKey: process.env.MICROCMS_API_KEY!,
});

export async function getBlogs() {
  return client.getList({
    endpoint: "blogs",
    queries: { limit: 10, orders: "-publishedAt" },
  });
}

APIキーはビルド環境(GitHub ActionsのSecrets)にだけ置きます。静的HTMLにキーを埋め込まないこと。クライアントコンポーネントからmicroCMSを直接叩くとキーが漏れます。

Trailing Slash を忘れると404になる

共有サーバーのApacheは、だいたい「ディレクトリ+index.html」でページを返します。trailingSlash: false のままだと、Nextは about.html を出します。一方ブラウザや内部リンクは /about/about/ を要求し、サーバー側の設定と食い違って404になります。

実務の対策はシンプルです。

  • trailingSlash: true にする
  • 内部リンクは /about/ に揃える
  • 必要なら .htaccess で末尾スラッシュへ寄せる
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !(.*)/$
RewriteRule ^(.*)$ /$1/ [L,R=301]

動的ルートはビルド時に全部出す

generateStaticParams(App Router)または getStaticPaths(Pages Router)で、公開するURLをビルド時に列挙します。fallback: 'blocking' や ISRは共有サーバーでは動きません。記事を追加したら、もう一度ビルドしてアップロードが必要です。

import { getBlogs, getBlog } from "@/lib/microcms";
import { notFound } from "next/navigation";

export async function generateStaticParams() {
  const { contents } = await getBlogs();
  return contents.map((post) => ({ id: post.id }));
}

export default async function PostPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getBlog(id).catch(() => null);
  if (!post) notFound();

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

公開前チェックリスト

  • output: "export"trailingSlash: true が入っている
  • images.unoptimized: true が入っている
  • app/apipages/api が残っていない
  • ビルドがローカルで通る
  • out の中身を public_html 直下に置いている
  • 下層URL(/posts/xxxx/)が404にならない

おすすめの共有サーバー

SSGは「HTMLを速く返せるか」が勝負です。次の系統は、静的配信との相性がよく、WordPress併用の案件でも使いやすいです。

  • mixhost: 国内向けの管理画面が分かりやすく、小〜中規模のコーポレートに使いやすい
  • エックスサーバー: 実績が多く、SSHやrsync運用まで見据えるなら候補に入りやすい

どちらも「Next.js専用」ではありません。PHPサイトと静的フロントを同じ契約で分けて置く、という使い方が実務では多いです。

既存WordPressからの移行・デプロイ支援

WordPressを残しつつフロントだけNext.jsにする、共有サーバーへSSGで載せる、画像とフォームの代替設計まで、設計から実装まで対応しています。「Vercelには出せないが、表示は速くしたい」という場合は特にこの構成が向いています。

ご相談はご相談・お問い合わせフォームからお気軽にどうぞ。サーバー契約を変えない前提で、移行手順から整理できます。