【Next.js】404 ページの作り方を not-found.tsx と notFound() の使い方まで解説

公開日: 2026年10月8日 · 読了目安 8 分

Next.js の App Router で、独自の 404 ページを作る手順です。app/not-found.tsx の置き方、データがないときの notFound() の呼び方、フォルダごとの 404、タイトルと noindex の扱い、ページに置く中身、多言語サイトでの注意点まで、コード付きで解説します。

Next.js の 404 ページを自分のデザインに変える

Next.js でサイトを作り、試しに存在しない URL を開いた。すると白い画面に「404」と「This page could not be found.」だけが出た。

サイトの見た目と合わないうえ、読者はどこへ戻ればいいか分かりません。

直し方の答えは短いです。app フォルダの直下に not-found.tsx を1つ置けば、それが 404 ページになります。

記事や商品のデータが見つからないときは、ページの中で notFound() を呼びます。すると同じ 404 ページに切り替わります。

この記事で分かることは次のとおりです。

  • app/not-found.tsx で作る、いちばん基本の 404 ページ

  • データがないときに notFound() で 404 ページを出す方法

  • ブログなど、フォルダごとに別の 404 ページを出す方法

  • 404 ページに置く中身と、控えめなアニメーション

  • 多言語サイト([locale])での注意点

404 ページを作り込む3つの理由

404 ページに来るのは、リンク切れや打ち間違いでたどり着いた人です。サイトに興味を持って来たのに、行き止まりにぶつかった状態です。

  • 離脱を減らす:ホームや人気ページへのリンクがあれば、そのまま読み進めてもらえる

  • ブランドを保つ:ヘッダーや色がサイトと揃っていれば、壊れたサイトに見えない

  • 検索エンジンに正しく伝える:ページがないことを 404 として返せば、存在しない URL が検索結果に残りにくい

見た目だけ整えても、データがないページが普通のページとして返ると意味が薄れます。この記事では、見た目と仕組みの両方を順に作ります。

Step1:app/not-found.tsx で 404 ページを作る

not-found.tsx は、Next.js が役割を決めている特別なファイル名です。app の直下に置くと、どのページにも一致しない URL でこの画面が出ます。

サーバーで組み立てる部品(サーバーコンポーネント)のままで書けます。ルートのレイアウト(app/layout.tsx)の中に表示されるので、ヘッダーやフッターもそのまま付きます。

app/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    <main className="mx-auto flex min-h-[70vh] max-w-xl flex-col items-center justify-center px-6 py-24 text-center">
      <p className="text-sm font-semibold tracking-widest text-zinc-500">404</p>
      <h1 className="mt-3 text-3xl font-semibold text-zinc-900">ページが見つかりません</h1>
      <p className="mt-4 text-zinc-600">
        URL が変わったか、ページが削除された可能性があります。
      </p>
      <Link
        href="/"
        className="mt-8 inline-flex min-h-11 items-center rounded-full bg-zinc-900 px-6 text-sm font-medium text-white hover:bg-zinc-700"
      >
        ホームに戻る
      </Link>
    </main>
  );
}

404 ページのタイトルを metadata で付ける

not-found.tsx でも、ページと同じように metadata を書き出せます。

ブラウザのタブに「ページが見つかりません」と出れば、読者も状況がすぐ分かります。

app/not-found.tsx(先頭に追加)
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "ページが見つかりません",
};

404 ページのステータスと noindex は自動で付く

not-found.tsx が表示されるとき、Next.js はふつう 404 のステータスコードを返します。ステータスコードは、ページの有無をサーバーが伝える番号です。

あわせて、検索結果に載せないための noindex の meta タグも自動で入ります。自分で robots の設定を書く必要はありません。

例外は、loading.tsx などで画面を先に送り始めたあとに 404 になった場合です。このときステータスは 200 になりますが、noindex のタグは入ります。

Step2:データがないときに notFound() で 404 ページを出す

ブログの app/blog/[slug]/page.tsx は、どんな文字列の URL にも一致します。存在しない記事の URL でも、このページが開いてしまいます。

そこで、データが見つからないときは next/navigation の notFound() を呼びます。呼んだ時点で描画が止まり、404 ページに切り替わります。

app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { cache } from "react";
import { getPostBySlug } from "@/lib/posts"; // 記事がなければ undefined を返す関数

// generateMetadata と page の取得を1回にまとめる
const getPost = cache(getPostBySlug);

type Props = { params: Promise<{ slug: string }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) notFound();
  return { title: post.title, description: post.excerpt };
}

export default async function BlogPostPage({ params }: Props) {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) notFound();

  return (
    <article className="mx-auto max-w-2xl px-6 py-24">
      <h1 className="text-3xl font-semibold">{post.title}</h1>
      <div className="mt-8 leading-relaxed text-zinc-700">{post.body}</div>
    </article>
  );
}

notFound() のコードのポイント

  • Next.js 15 では params が Promise なので、await してから slug を取り出す

  • notFound() の後ろは実行されない。TypeScript も、その先では post があるものとして扱う

  • generateMetadata の中でも、同じように notFound() を呼べる

  • React の cache で包むと、タイトル用と本文用の取得が1回で済む

ブログだけ別の 404 ページにする

not-found.tsx は、フォルダごとに置けます。app/blog/not-found.tsx を置くと、ブログの中で notFound() を呼んだときはこちらが使われます。

いちばん近いフォルダのファイルが選ばれるので、記事一覧へ戻るリンクなど、場所に合った案内を出せます。

ただし、どのページにも一致しない URL を受け持つのは app 直下の not-found.tsx です。

例外は、15.4 以降の実験的な global-not-found.tsx を使う場合です。

app/blog/not-found.tsx
import Link from "next/link";

export default function BlogNotFound() {
  return (
    <main className="mx-auto max-w-xl px-6 py-24 text-center">
      <h1 className="text-2xl font-semibold text-zinc-900">記事が見つかりません</h1>
      <p className="mt-4 text-zinc-600">記事の URL が変わったか、公開を終了した可能性があります。</p>
      <Link
        href="/blog"
        className="mt-8 inline-flex min-h-11 items-center rounded-full bg-zinc-900 px-6 text-sm font-medium text-white"
      >
        記事の一覧へ
      </Link>
    </main>
  );
}

Step3:404 ページに置く中身を決める

404 ページの役目は、迷った人を次の行き先へ戻すことです。凝った演出より先に、次の3つを揃えます。

  • 何が起きたかを短く伝える見出し(「ページが見つかりません」)

  • ホームへ戻るボタン

  • サイト内検索か、よく読まれているページへのリンク

検索は、GET で送るだけのフォームならサーバーで組み立てる部品のまま置けます。送信先の /search は、自分のサイトの検索ページに合わせて変えてください。

app/not-found.tsx(中身を足した版)
import Link from "next/link";

const popular = [
  { href: "/blog", label: "ブログ記事の一覧" },
  { href: "/pricing", label: "料金プラン" },
  { href: "/contact", label: "お問い合わせ" },
];

export default function NotFound() {
  return (
    <main className="mx-auto max-w-xl px-6 py-24 text-center">
      <p className="text-sm font-semibold tracking-widest text-zinc-500">404</p>
      <h1 className="mt-3 text-3xl font-semibold text-zinc-900">ページが見つかりません</h1>
      <p className="mt-4 text-zinc-600">URL が変わったか、ページが削除された可能性があります。</p>

      <form action="/search" method="get" role="search" className="mt-8 flex gap-2">
        <label htmlFor="q" className="sr-only">サイト内を検索</label>
        <input
          id="q"
          name="q"
          type="search"
          placeholder="キーワードで探す"
          className="min-h-11 flex-1 rounded-full border border-zinc-300 px-4 text-base"
        />
        <button type="submit" className="min-h-11 rounded-full bg-zinc-900 px-5 text-sm font-medium text-white">
          検索
        </button>
      </form>

      <ul className="mt-10 space-y-1 text-sm">
        {popular.map((item) => (
          <li key={item.href}>
            <Link href={item.href} className="inline-flex min-h-11 items-center underline underline-offset-4">
              {item.label}
            </Link>
          </li>
        ))}
      </ul>

      <Link
        href="/"
        className="mt-8 inline-flex min-h-11 items-center rounded-full border border-zinc-300 px-6 text-sm font-medium"
      >
        ホームに戻る
      </Link>
    </main>
  );
}

404 ページの文言は読者を責めない

「URL が間違っています」と書くと、読者のせいに聞こえます。リンク切れは、サイト側の都合でも起きます。

「見つかりません」「移動した可能性があります」のように、事実だけを伝えます。

イラストや遊びを入れる場合も、戻るためのリンクは最初に見える位置に置きます。

Step4:404 ページに控えめなアニメーションを付ける

大きな 404 の数字を、ふわっと浮かび上がらせるだけでも印象は変わります。動きは1回きりで、0.5秒前後に抑えます。

アニメーションには Motion(旧 Framer Motion)を使います。動く部分だけを別ファイルにして、先頭にブラウザで動かす印の "use client" を書きます。

components/NotFoundNumber.tsx
"use client";

import { motion, useReducedMotion } from "motion/react";

export function NotFoundNumber() {
  const reduceMotion = useReducedMotion();

  return (
    <motion.p
      aria-hidden="true"
      initial={reduceMotion ? false : { opacity: 0, y: 24 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.6, ease: "easeOut" }}
      className="text-[8rem] font-semibold leading-none tracking-tight text-zinc-900 sm:text-[12rem]"
    >
      404
    </motion.p>
  );
}

404 ページのアニメーションのポイント

  • not-found.tsx 自体はサーバーで組み立てる部品のままにして、この部品だけを読み込む

  • 動きを減らす設定の人には initial を false にして、最初から表示した状態にする

  • 大きな数字は飾りなので aria-hidden を付け、読み上げは h1 の見出しに任せる

Motion の基本は https://design-layer.com/ja/articles/nextjs-framer-motion-guide で解説しています。

多言語サイト([locale])で 404 ページを出すときの注意点

URL の先頭に言語が入るサイトでは、app/[locale] の下にページを置きます。この形では、404 ページの扱いが少し変わります。

app/[locale]/not-found.tsx は、その中で notFound() を呼んだときに表示されます。しかし、どのページにも一致しない URL では使われません。

そこで、残りの URL をすべて受け止めるページを1つ作り、その中で notFound() を呼びます。next-intl などの多言語ライブラリの説明でも使われている方法です。

なお、not-found.tsx は params を受け取れません。表示する言語の決め方は、使っている多言語ライブラリの方法に合わせてください。

app/[locale]/[...rest]/page.tsx
import { notFound } from "next/navigation";

// どのページにも一致しない URL をここで受け止め、[locale] の not-found.tsx を出す
export default function CatchAllPage() {
  notFound();
}

404 ページでよくある失敗と公開前チェック

  • データがないのに空のページを返す。notFound() を呼ばないと、中身のないページが 200 のまま検索エンジンに届く

  • 404 ページからホームへ自動で転送する。読者は何が起きたか分からず、ページがないことも伝わりにくい

  • 戻るリンクがない、または画面の下の方にしかない

  • 404 ページ全体に "use client" を書く。動く部分だけを切り出す

  • 移動しただけのページを 404 にする。移動先があるなら next.config の redirects で転送する

公開前に、存在しない URL を開いて表示を確かめます。next build のあとの本番と同じ状態で試すと確実です。

ステータスは curl -I https://example.com/no-such-page のように確かめると、404 が返っているか分かります。

完成した 404 ページをコピーして使う方法

DesignLayer の「スタチュー 404」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる
DesignLayer の「スタチュー 404」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる

ここまでの手順で、仕組みとしての 404 ページはでき上がります。ただ、次のような見た目まで作り込むと時間がかかります。

  • 紙のような質感の背景に、大きな 404 を置く

  • 中央に石像の切り抜きを重ねる

  • 文言やアクセント色をサイトに合わせる

そんなときは、完成した 404 画面をブラウザで確かめてから、コードを自分の Next.js に載せる方法もあります。

DesignLayer の「スタチュー 404」は、ざらついた紙地に巨大な 404 と石像の切り抜きを重ねたエラー画面です。文言・像の画像・アクセント色を差し替えて、not-found.tsx に置けます。

詳細ページの「Cursor 用にコピー」を押すと、統合の指示・ライブプレビューの URL・本体の TSX・npm の依存がまとめてコピーされます。Cursor や Claude に貼って、組み込みを頼めます。

閲覧とライブプレビューは無料で、アカウントもカードも要りません。無料のソースコピーは日本時間で1日3回です。

カタログは https://design-layer.com/ja/components から開けます。

Next.js の 404 ページについてよくある質問

  • App Router では、app フォルダの直下に not-found.tsx を置きます。Pages Router の pages/404.tsx とはファイルの場所が違います。

まとめ:Next.js で 404 ページを作る手順

  • app 直下に not-found.tsx を置くと、一致しない URL でその画面が 404 ページとして出る

  • データがないときは notFound() を呼ぶ。404 のステータスと noindex は Next.js が付ける

  • フォルダごとに not-found.tsx を置くと、場所に合った 404 ページを出せる

  • 中身は見出し・ホームへのリンク・検索か人気ページの3つを先に揃える

  • [locale] のサイトでは、残りの URL を受け止めるページで notFound() を呼ぶ

まずは Step1 のコードを app 直下に置き、存在しない URL を開いてみてください。

データを待つあいだの画面は https://design-layer.com/ja/articles/nextjs-loading-screen-guide で解説しています。

【Next.js】404 ページの作り方を not-found.tsx と notFound() の使い方まで解説 | DesignLayer