メインコンテンツへスキップ
見出し画像

「マイ遠征手帳」開発記①Next.js (App Router) でページが表示されない(404)時の原因とフォルダ構成【src移行・[id]・競合】

    Next.jsのフォルダ階層ルール、コンポーネントとルーティングファイルの混在、さらにインポートパスのズレなどが重なると、どこが間違っているのかをデバッグするだけで数時間を溶かしてしまいます。

    この記事では、開発をはじめたモバイルファーストのWebアプリケーション「マイ遠征手帳(仮)」の開発中に実際に直面したフォルダ階層エラー(404)の罠をベースに、
    ・どういうミスをしていたのか
    ・どう解消できるのか
    ・公式ドキュメントではどう定義されているか
    を徹底解説します。

    同じように「Next.js使ってみたけど404で絶望…」という同志の参考になれば幸いです!


    1. アプリ概要と開発環境

    今回開発しているアプリと環境は以下の通りです。

    • 開発アプリ: 「マイ遠征手帳(仮)」

      • コンセプト: 自分自身の遠征・ライブ体験と教訓(座席の見え方、耳栓の有無、規制退場時間、動線メモなど)を安全にストックする、完全自分専用のプライベートメモ手帳

      • SNSや一般共有機能は一切持たない、実用情報蓄積アプリ

    • 開発環境:

      • OS: macOS (Apple Silicon)

      • エディタ: VSCode

    • フロントエンド技術スタック:

      • フレームワーク: Next.js (App Router)

      • 言語: TypeScript

      • スタイリング: Tailwind CSS

      • アイコン: Lucide React

      • 地図描画: Leaflet / React-Leaflet

    • 共同開発AI:

      • Gemini Pro

    2. 今回踏み抜いた「404エラー」の原因と解決策

    開発初期、Next.js App Routerの階層の強固な制約に慣れていなかったため、私のフォルダツリーはルーティング規則とUIパーツが混ざり合い、404エラーと戦う羽目になりました・・・。

    404エラー原因:プロジェクト直下と src/ 配下の app フォルダが競合した

    Next.jsには、以下の2つのフォルダ管理スタイルがあります。

    1. プロジェクト直下に app/ フォルダを置く

    2. src/app/ のように、すべてのソースファイルを src/ の中にまとめる

    私の場合、最初ここを理解しておらず、最初の段階でデフォルトで自動作成されたルート直下の app/ が存在しているにもかかわらず、手動で src/app/ を作ってそこに新しい画面ファイルを開発していました。

    Next.jsはルート直下に app/ フォルダを検知するとそちらを優先するため、src/app/ の中にどれだけ丁寧にページを構築しても完全にシカトされ、404を吐き続ける原因になっていました。

    画像

    超初心者ミス!

    解決策:src/ 配下への完全統合(公式ドキュメント『srcフォルダ』に準拠)

    プロジェクト内のファイル管理の競合を防ぐため、すべてのソースファイルを src/ の中に統合しました。

    画像

    Next.js公式日本語ドキュメントの[File Conventions]>[src]の章には、以下のように記載されています。

    プロジェクトのルートに特別な Next.js の app または pages ディレクトリを配置する代わりに、Next.js は src フォルダ内にアプリケーションコードを配置するという一般的なパターンもサポートしています。
    ・・・ (中略)・・・
    src フォルダを使用するには、app Router フォルダまたは pages Router フォルダを、それぞれ src/app または src/pages に移動します。

    https://nextjsjp.org/docs/app/api-reference/file-conventions/src-folder より引用

    このドキュメントのルールに則り、プロジェクト直下から app/ を完全に排除し、src/app/ のみに一本化することでお引越しさせました。

    これにより、設定ファイル(tsconfig.json や package.json などの設定類)と、実コードが綺麗に分離されル状態になりました

    3. 公式ドキュメントの定義と、フォルダ地獄の解決策(どう解消できるのか)

    最も大きかったのは上記のsrcディレクトリの使い方ですが、そのほかいくつか気をつけるべきだと思ったことがあるので書いておきます。

    ポイント①:役割によるフォルダ階層の徹底分離

    公式日本語ドキュメントの「プロジェクト構造」および「レイアウトとページ」に基づき、URLのパスを定義するルーティング層と、画面パーツを描画するコンポーネント層の曖昧さをなくして分離しました。

    • src/app/ (ルーティング層): URLパスを定義する特別な場所。画面の起点となる page.tsx や layout.tsx のみを置きます。

    • src/components/ (UIコンポーネント層): 具体的な画面を構成する「パーツ」を置きます。

    整理後のフォルダツリーは以下のようになりました

    my-event-notebook/
    ├── src/
    │   ├── app/                  (★URLパスのみを管理するルーティング層)
    │   │   ├── globals.css
    │   │   ├── layout.tsx
    │   │   ├── page.tsx          (※ / ホーム画面の起点)
    │   │   ├── trips/
    │   │   │   └── [id]/
    │   │   │       └── page.tsx  (※ /trips/[id] 遠征詳細画面の起点)
    │   │   └── venues/
    │   │       └── page.tsx      (※ /venues マップ画面の起点)
    │   ├── components/           (★URLに一切影響しない、具体的なUIパーツを管理する層)
    │   │   ├── trips/
    │   │   │   ├── PackingTab.tsx
    │   │   │   ├── SeatArchiveTab.tsx
    │   │   │   ├── TimelineTab.tsx
    │   │   │   └── TripDetailLayout.tsx
    │   │   ├── ui/
    │   │   │   └── BottomNav.tsx
    │   │   └── venues/  
    │   │       ├── MapContent.tsx       (※地図コンポーネント)
    │   │       └── VenueBottomSheet.tsx (※ボトムシート)
    │   ├── mocks/
    │   │   └── mockData.ts
    │   └── types/
    │       └── index.ts
    

    このように棲み分けることで、src/app/venues/page.tsx に配置したページは、純粋にUIコンポーネント(components/venues/MapContent)をクライアント限定で動的にインポートして読み込む役割に特化できるようになります。

    画像
    画像

    結果的に404エラーやインポートエラーを劇的に減らすことができています。

    4. Next.js App Routerは、どういう仕組みでURLを作っているのか?

    Next.js App Routerがどのようにしてフォルダツリーから動的URLを生成するのか、公式仕様を読み解いてみました。

    ① フォルダツリー = URL(ファイルシステムルーティング)

    Next.js公式ドキュメントに記載されている通り、src/app フォルダ配下のディレクトリ名そのものがそのままブラウザのアドレスバーのパスにバインドされます。

    • src/app/venues/page.tsx を配置
       → 自動的にURLが /venues に対応。

    • 画面をルーティングとして有効化するためには、必ず小文字の page.tsx をそのフォルダ内に作成。

    ② 角カッコ [id] は「動的なパラメータ(変数)」を受け取る仕組み

    特定の「遠征データID」ごとに画面を変化させたい(例:/trips/trip-2026-osaka)場合、フォルダ名を半角の角カッコ [id] で囲みます。

    Dynamic Segmentは、フォルダ名を角括弧で囲むことで作成できます。例えば、ブログでは以下のようなルート app/blog/[slug]/page.js を含めることができます。ここで [slug] はブログ投稿用のDynamic Segmentです。

    https://nextjsjp.org/docs/app/api-reference/file-conventions/dynamic-routes より

    Next.jsはこのように角括弧で囲まれた [id] のようなフォルダを検出すると、動的な値が格納される変数(パラメータ)として自動的にバインドします。

    実装コード例(Next.js 15+以降の「非同期 params」対応)

    Next.js 15以降では、URLのパラメータを受け取る params の取り扱いが Promise型 に変更され、非同期(async/await)で解決する必要があります。

    以下は、今回「マイ遠征手帳」で構築した、非同期 params を処理してモックデータからデータをバインドする src/app/trips/[id]/page.tsx の完全なコード例です。

    import React from 'react';
    import { MOCK_TRIPS } from '../../../mocks/mockData';
    import { TripDetailLayout } from '../../../components/trips/TripDetailLayout';
    
    // Next.js 15+仕様に合わせ、ParamsをPromise型として受け取るインターフェース定義
    interface Props {
      params: Promise<{ id: string }>;
    }
    
    export default async function TripDetailPage({ params }: Props) {
    
      // await を使って非同期でURLの「id」パラメータ(例: trip-2026-osaka)を解決
      const resolvedParams = await params;
    
      // 解決したIDに一致するデータをモックデータから検索
      const trip = MOCK_TRIPS.find(t => t.id === resolvedParams.id);
    
      // データが見つからなかった場合の安全なフォールバック
      if (!trip) {
        return (
          <div className="flex h-screen items-center justify-center text-gray-500 font-bold">
            遠征データが見つかりませんでした。
          </div>
        );
      }
    
      // 取得したデータを、UIコンポーネント層(TripDetailLayout)に引き渡して描画
      return <TripDetailLayout trip={trip} />;
    }
    
    画像
    画像

    このアプローチを取ることで、1つの page.tsx のみで、/trips/trip-2026-osaka や /trips/trip-2026-yokohama といった動的URLから、該当する公演情報や座席データを安全にバインド・表示することが可能になりました。

    5. まとめ

    Next.jsのApp Routerを初めて使うので、フォルダ構成の複雑化による404エラーにかなり悩みました!

    公式日本語ドキュメントの「プロジェクト構造」「srcフォルダ」の設計思想を読みにいくことでなんとか、「ルーティングにのみ責任を持つ層(src/app)」と「純粋にUIパーツを描画する層(src/components)」を徹底的に分けるという結論に辿り着けました。
    慣れるとわかりやすいのかもしれませんがしばらく格闘しそうです。

    自戒も込めて!開発中に404エラーに出会ったら、まずは以下の「チェックリスト」で確認してみようと思います。

    • プロジェクトのルート直下と src/ 配下で app/ フォルダが二重に競合していないか?

    • 動的ルートは、の書き方が適切か

    • 動的パラメータの処理は、Next.js 15+以降の「非同期Promise」に準拠した await で解決できているか?

    • フォルダ整理の後、ターミナルで npm run dev による再起動は試したか?

    他にもAIと共同開発を進めています!ぜひご一読ください!


     
     
    5年間の教員経験を経て、20代でITエンジニアへ転身 現在はAWS勉強中 「挫折しない勉強法」や「生成AIを相棒にした仕事術」メインに発信しています。 自身のアップデートを楽しみながら、学びを形にするプロセスを共有します! ※「楽天アフィリエイト」プログラムに参加しています。

    あなたへのおすすめ