Next.js公式チュートリアルで学ぶ：App Routerの導入と基本的な使い方


どうもこんにちはたくびー(@takubii)です。
今回はNext.js 13から新しく利用できるようになったApp Routerについて書きたいと思います。
Next.jsの公式チュートリアルではPage Routerが使われているので、こちらをApp Routerに置き換えていきます。
公式チュートリアルに沿って置き換えていくので、参照しながら進めてください。

https://nextjs.org/learn/basics/create-nextjs-app

各章ごとにPage RouterとApp Routerでの実装方法の違いを簡単に述べながら進めていきます。
全体の完成コードはこちらです。

https://github.com/takubii/nextjs-blog-tutorial

各章をブランチごとに実装し進めたので、詳細はそちらを参照しながらご覧ください。

## install
インストールは以下のような設定で行いました。
バージョンが変わって手順が違う可能性もありますので、参考程度にしてください。

```zsh:terminal
✔ Would you like to use TypeScript with this project? … No / Yes → Yes
✔ Would you like to use ESLint with this project? … No / Yes → Yes
✔ Would you like to use Tailwind CSS with this project? … No / Yes → No
✔ Would you like to use `src/` directory with this project? … No / Yes → Yes
✔ Use App Router (recommended)? … No / Yes → Yes
✔ Would you like to customize the default import alias? … No / Yes → No
```

## Create a Next.js App

今回はインストール時に`src`フォルダを使用する設定にしたので、以降プロジェクト直下に作成されるファイル、フォルダは`src`フォルダ内に作成してください。

### stylesフォルダ
`src`直下に`styles`フォルダを作成し、`global.css`、`page.module.css`を移動してください。

### layout.tsx
`<Head>`コンポーネントは使用できなくなっているので、`layout.tsx`の`metadata`に追加します。
faviconは`src/app`以下にあれば自動的に認識されるようです。
metadataに含まれていたdescriptionは削除してください。
また、next font関連のコードは削除しましょう。

### page.module.css
`Home.module.css`は`page.module.css`に変更してください。

### global.css
`<style jsx>, <style jsx global>`内のCSSはglobal.cssに移動してください。(`page.tsx`に書いてあると`use client`が要求されます。)
チュートリアルにあった元の`global.css`の内容は使われていないので削除しても問題ありません。

### page.tsx
`index.js`は`page.tsx`に変更します。

### 1章まとめ
チュートリアルの最初から大きく変わったところは`page.tsx`の使用の部分です。
App Routerではフォルダがパスの役割を果たし、その配下のファイルには各々規定の名前を持ったファイルが配置されます。
詳しくはこちらでどういったファイルが配置されるのか確認できます。今回使用しているのは`page.tsx`と`layout.tsx`の２種類です。

https://nextjs.org/docs/app/api-reference/file-conventions

## Navigate between Pages

### posts/first-post/page.tsx
`pages/posts/first-post.js`の代わりに作成しています。
先述の通り、App Routerではフォルダ構成がAPIのルートとなり、その配下にpage.tsxでページ、layout.tsxで共通レイアウトを作成します。

### 2章まとめ
ファイルを作成し、ページのルーティングを確認しているだけの章なので、App RouterとPage Routerの違いが一番分かる章なのではないでしょうか。
App Routerでは基本的にフォルダ数が多くなる傾向ですが、慣れてくるとファイルの規則などで分かりやすくなります。

## Assets, Metadata, and CSS

### components/layout.tsx
`next/head`は使えないので、`export const metaData ...`というオブジェクトを作り、トップページにmetadataを追加しています。
※metadataは後々`app/page.tsx`に移動しました。とりあえずチュートリアル通りに進め最後にmetadataをリファクタします。

### public/next.svg, public/vercel.svg, styles/page.module.css
これらの使わないものは削除します。

### 3章まとめ
こちらで分かりにくいなと思った点は画像のパスがどこにあるのかです。
`public`がルートとなり、画像の指定は次のように`/images/profile.jpg`となります。
faviconに関しては`src/app`配下に`favicon.ico`ファイルがあれば自動的に認識されるようです。

## Pre-rendering and Data Fetching

### lib/posts.ts
`postsDirectory`のパスは`path.join(process.cwd(), '/src/posts')`にする必要があります。（srcディレクトリを使用している場合）
Typescriptで記述する際に`allPostsData`の戻り値`return { id, ...matterResult.data }`だと型が適用されず、ソートの際にエラーが出るので、こちらを`return { id, ...(matterResult as { title: string; date: string }) }`とすることで解決しました。

### app/page.tsx
`getStaticProps`はPage Routerの機能なので使用できません。
App Routerでは直接コンポーネントの中にデータ取得メソッドを書くだけで問題ないです。

### 4章まとめ
App Routerにすることで`getStaticProps`などのPage Routerで使われていた特定の関数を書く必要がなくなりました。
このあたりが特にApp Routerが初心者向けになった部分なのかなと感じています。個人的にはとても分かりやすくなっていると思います。

## Dynamic Routes

### lib/posts.ts
`getAllPostIds`関数の戻り値は、チュートリアルでは`[{ params: { id: '1' }, ... }]`となっていましたが、`generateStaticParams`では`params`オブジェクトではなく、`[{ id: '1' }, ...]`となるため、変更してください。

### app/posts/[id]/page.tsx
`getStaticPaths`を`generateStaticParams`に変更します。
`getStaticProps`で行っていたデータ取得処理はコンポーネント内に直接記述してください。
コンポーネントの引数が`params`に変更されます。
`getPostData`を非同期関数にした場合、`Post`コンポーネントを非同期コンポーネントに変更してください。（例：`export default async function Post(...) {...}`）
`next/head`の`<Head>`タグは使えないので、動的なメタデータの追加に`generateMetadata`を使用します。

### 5章まとめ
`getStaticPaths`が`generateStaticParams`に変わっていたり、メタデータを動的作成するために`generateMetadata`関数を使ったりとこの章では新しい関数が多く出てきたと思います。
また、`getStaticPaths`と`generateStaticParams`では返すオブジェクトの形が少し変わっていたりしたので、Page Routerと差異が大きい部分だなと感じました。

## API Routes
Api Routeのフォルダ構成は`pages/api/hello.js`を`app/api/hello/route.ts`に変更します。
内容も`handler`関数ではなく、HTTPメソッドごとに関数を作成してください。（GET,POST,PUT,DELETEなど）
今回は`GET`関数を作成し、レスポンスは`NextResponse`で作成しています。

### 6章まとめ
App Routerで最も変更されている箇所だと思っているのがこのApi Routeです。
チュートリアルでは主題ではなかったので、Jsonを返すだけの簡単なAPIの作成で終わっていますが、機能としてはかなり大きく変わっています。
今回はGETメソッドの実装のために`app/api/hello/route.ts`に`GET()`関数を作成しました。
必要に応じてPOSTやDELETEメソッドも作成できるので、興味のある方は[こちら](https://nextjs.org/docs/app/api-reference/file-conventions/route)を詳しくご覧ください。

## ソースコード
今回のチュートリアルのApp Router対応で使ったソースコードを記載します。
CSSファイルやMDファイルは一緒なので、Typescriptファイルを載せますので、参考にしてください。

### ディレクトリ構成
```zsh
nextjs-blog-tutorial
├── README.md
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── public
│   └── images
│       └── profile.jpg
├── src
│   ├── app
│   │   ├── api
│   │   │   └── hello
│   │   │       └── route.ts
│   │   ├── favicon.ico
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── posts
│   │       └── [id]
│   │           └── page.tsx
│   ├── components
│   │   ├── date.tsx
│   │   ├── layout.module.css
│   │   └── layout.tsx
│   ├── lib
│   │   └── posts.ts
│   ├── posts
│   │   ├── pre-rendering.md
│   │   └── ssg-ssr.md
│   └── styles
│       ├── globals.css
│       └── utils.module.css
└── tsconfig.json
```

### 各ソースコード一覧
```typescript:src/app/layout.tsx
import '../styles/globals.css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang='en'>
      <body>{children}</body>
    </html>
  );
}

```

```typescript:src/app/page.tsx
import Link from 'next/link';

import { getSortedPostsData } from '@/lib/posts';
import Date from '@/components/date';
import Layout from '@/components/layout';
import utilStyles from '@/styles/utils.module.css';

export const siteTitle = 'Next.js Sample Website';

export const metadata = {
  title: siteTitle,
  description: 'Learn how to build a personal website using Next.js',
  openGraph: {
    images: [
      {
        url: `https://og-image.vercel.app/${encodeURI(
          siteTitle
        )}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`,
      },
    ],
    title: siteTitle,
  },
  twitter: {
    card: 'summary_large_image',
  },
};

export default function Home() {
  const allPostsData = getSortedPostsData();

  return (
    <Layout home>
      <section className={utilStyles.headingMd}>
        <p>[Your Self Introduction]</p>
        <p>
          (This is a sample website - you&apos;ll be building a site like this on{' '}
          <a href='https://nextjs.org/learn'>our Next.js tutorial</a>.)
        </p>
      </section>
      <section>
        <h2 className={utilStyles.headingLg}>Blog</h2>
        <ul className={utilStyles.list}>
          {allPostsData.map(({ id, date, title }) => (
            <li className={utilStyles.listItem} key={id}>
              <Link href={`/posts/${id}`}>{title}</Link>
              <br />
              <small className={utilStyles.lightText}>
                <Date dateString={date} />
              </small>
            </li>
          ))}
        </ul>
      </section>
    </Layout>
  );
}

```

```typescript:src/app/posts/[id]/page.tsx
import { Metadata } from 'next';

import Date from '@/components/date';
import Layout from '@/components/layout';
import { getAllPostIds, getPostData } from '@/lib/posts';
import utilStyles from '@/styles/utils.module.css';

// getStaticPathsのfallback: falseの代替手段
export const dynamicParams = false;

export async function generateStaticParams() {
  return getAllPostIds();
}

export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
  const postData = await getPostData(params.id);

  return { title: postData.title };
}

export default async function Post({ params }: { params: { id: string } }) {
  const postData = await getPostData(params.id);
  return (
    <Layout>
      <article>
        <h1 className={utilStyles.headingXl}>{postData.title}</h1>
        <div className={utilStyles.lightText}>
          <Date dateString={postData.date} />
        </div>
        <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
      </article>
    </Layout>
  );
}

```

```typescript:src/app/components/layout.tsx
import Image from 'next/image';
import Link from 'next/link';

import styles from './layout.module.css';
import utilStyles from '../styles/utils.module.css';

const name = 'Your Name';

export default function Layout({ children, home }: { children: React.ReactNode; home?: boolean }) {
  return (
    <div className={styles.container}>
      <header className={styles.header}>
        {home ? (
          <>
            <Image
              priority
              src='/images/profile.jpg'
              className={utilStyles.borderCircle}
              height={144}
              width={144}
              alt=''
            />
            <h1 className={utilStyles.heading2Xl}>{name}</h1>
          </>
        ) : (
          <>
            <Link href='/'>
              <Image
                priority
                src='/images/profile.jpg'
                className={utilStyles.borderCircle}
                height={108}
                width={108}
                alt=''
              />
            </Link>
            <h2 className={utilStyles.headingLg}>
              <Link href='/' className={utilStyles.colorInherit}>
                {name}
              </Link>
            </h2>
          </>
        )}
      </header>
      <main>{children}</main>
      {!home && (
        <div className={styles.backToHome}>
          <Link href='/'>← Back to home</Link>
        </div>
      )}
    </div>
  );
}

```
```typescript:src/app/components/date.tsx
import { parseISO, format } from 'date-fns';

export default function Date({ dateString }: { dateString: string }) {
  const date = parseISO(dateString);

  return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>;
}

```

```typescript:src/app/lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';

const postsDirectory = path.join(process.cwd(), '/src/posts');

export function getSortedPostsData() {
  // Get file names under /src/posts
  const fileNames = fs.readdirSync(postsDirectory);
  const allPostsData = fileNames.map((fileName) => {
    // Remove ".md" from file name to get id
    const id = fileName.replace(/\.md$/, '');

    // Read markdown file as string
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');

    // Use gray-matter to parse the post metadata section
    const matterResult = matter(fileContents);

    // Combine the data with the id
    return {
      id,
      ...(matterResult.data as { title: string; date: string }),
    };
  });

  // Sort posts by date
  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1;
    } else {
      return -1;
    }
  });
}

export function getAllPostIds() {
  const fileNames = fs.readdirSync(postsDirectory);

  // Returns an array that looks like this:
  // [{ id: 'ssg-ssr' }, { id: 'pre-rendering' },]
  return fileNames.map((fileName) => {
    return {
      id: fileName.replace(/\.md$/, ''),
    };
  });
}

export async function getPostData(id: string) {
  const fullPath = path.join(postsDirectory, `${id}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  // Use gray-matter to parse the post metadata section
  const matterResult = matter(fileContents);

  // Use remark to convert markdown into HTML string
  const processedContent = await remark().use(html).process(matterResult.content);
  const contentHtml = processedContent.toString();

  // Combine the data with the id and contentHtml
  return {
    id,
    contentHtml,
    ...(matterResult.data as { title: string; date: string }),
  };
}

```

```typescript:src/app/api/hello/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  return NextResponse.json({ text: 'Hello' });
}

```

## 終わりに
Next.js13からApp Routerが使えるようになり、とあるサイトのハンズオンで作ってみてから何か作ってみたいなと思いました。

Next.jsの公式チュートリアルを見てみるとまだPage Routerで書かれていたので、いい機会なのでPage RouterからApp Routerに置き換えるとどう言った部分で違いが出てくるのかといった点を実験してみました。
こちらの記事が新しくApp Routerを使う方の参考になれば幸いです。

では、今回はこのあたりで締めようと思います。
ここまで読んでくださりありがとうございます。
また機会があればお会いしましょう。