【意訳】Next.jsのチュートリアルをTypeScriptでする

Next.jsのチュートリアルをTypeScriptでなぞってみます。
今回の記事ではデプロイは対象外としてます。
公式はこちらです。
https://nextjs.org/learn/basics/create-nextjs-app

### Create a Next.js App

プロジェクトを作成します。
すべてデフォルトを選択しました。

```terminal
npx create-next-app nextjs-blog
✔ Would you like to use TypeScript with this project? … No / Yes
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ Would you like to use experimental `app/` directory with this project? … No / Yes
✔ What import alias would you like configured? … @/*
```

プロジェクトを作成できたら以下のコマンドで開発サーバーを立てることができます。

```terminal
npm run dev
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/3e089be6-efed-c6f2-cb04-3501eefe6ff5.png" width="500" />

### Navigate Between Pages

#### Pages in Next.js
Next.jsにおけるpagesディレクトリの説明です。
`pages/index.js`は`/`に、
`pages/posts/first-post.js`は`/posts/first-post`に該当します。

#### Create a New Page
pagesディレクトリ直下にpostsディレクトリを作成して、その直下にfirst-posts.tsxファイルを作成します。

```tsx:pages/posts/first-post.tsx
export default function FirstPost() {
    return (
        <h1>First Post</h1>
    )
}
```

`posts/first-post`にアクセスすると画像のような表示になります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/e3a44db1-5afc-0879-22b4-6dfb0ad47f78.png" width="500" />


#### Link Component
ページ間の画面線に着目します。
クライアントサイドのナビゲーションを可能にします。

トップページから`FirstPost`へ遷移できるようにしましょう。

```tsx:pages/index.tsx
import Link from "next/link"

export default function Home() {
    return (
        <h1 className="title">
            Read <Link href="/posts/first-post">this page!</Link>
        </h1>
    )
}

```

`FirstPost`からトップページへ戻れるようにします。

```tsx:pages/posts/first-post.tsx
import Link from 'next/link';

export default function FirstPost() {
    return (
        <>
            <h1>First Post</h1>
            <h2>
                <Link href="/">Back to home</Link>
            </h2>
        </>
    );
}
```

### Assets, Metadata, and CSS
#### Adding Head to first-post.js
`FirstPost`ページのタイトル属性を変更してみましょう。
`Head`コンポーネントを使うことで実現できます。

```tsx:pages/posts/first-post.tsx
import Link from 'next/link';
import Head from 'next/head';

export default function FirstPost() {
    return (
        <>
            <Head>
                <title>First Post</title>
            </Head>

            <h1>First Post</h1>
            <h2>
                <Link href="/">Back to home</Link>
            </h2>
        </>
    );
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/865907b0-5d4f-d6c8-ce3e-9d6dbcb59e6d.png" width="500" />

#### Third-Party JavaScript
プロジェクトのサードパーティ製のJavaScriptを追加します。
Next.jsには`Script`というコンポーネントをが用意されています。
`first-post.tsx`に使ってみましょう。

```tsx:pages/posts/first-post.tsx
import Link from 'next/link';
import Head from 'next/head';
import Script from 'next/script';

export default function FirstPost() {
    return (
        <>
            <Head>
                <title>First Post</title>
            </Head>
            <Script
                src='https://connect.facebook.net/en_US/sdk.js'
                strategy='lazyOnload'
                onLoad={() => {
                    console.log('loaded');
                }}
            />

            <h1>First Post</h1>
            <h2>
                <Link href="/">Back to home</Link>
            </h2>
        </>
    );
}
```

`Script`にはさまざまなオプションが用意されています。
`strategy`に`lazyonload`を指定するとブラウザがアイドル状態の時にJavaScriptを読み込んでくれます。
`onLoad`に処理を記載することで、読み込んだ後に実行したい処理を指定できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/3aace7d4-cd89-8222-ba16-3b39866dbc76.png" width="500" />

サードパーティJavaScriptの導入はブログ作成とは関係ないので、確認したら削除してください。

#### Layout Component
複数の共通のパーツを使いまわしたい時があると思います。
`layout.tsx`というファイルを作成してみましょう。

```tsx:components/layout.tsx
import { ReactNode } from 'react'

interface Props {
    children: ReactNode
}

export default function Layout({ children }: Props) {
    return (
        <div>
            {children}
        </div>
    )
}
```

作成したファイルを`first-post.tsx`で使います。

```tsx:pages/first-post.tsx
import Link from 'next/link';
import Head from 'next/head';
import Layout from '@/components/layout';

export default function FirstPost() {
    return (
        <Layout>
            <Head>
                <title>First Post</title>
            </Head>

            <h1>First Post</h1>
            <h2>
                <Link href="/">Back to home</Link>
            </h2>
        </Layout>
    );
}
```

webページのヘッダーやフッターは同じパーツを使うことで統一感が出てわかりやすいと思います。

#### Adding CSS
先ほど作成した`layout.tsx`にスタイルを適用しましょう。
`layout.module.css`を作成します。

```css:components/layout.module.css
.container {
    max-width: 36rem;
    padding: 0 1rem;
    margin: 3rem auto 6rem;
}
```

cssを`layout.tsx`に適用します。

```tsx:components/layout.tsx
import { ReactNode } from 'react'
import styles from './layout.module.css'

interface Props {
    children: ReactNode
}

export default function Layout({ children }: Props) {
    return (
        <div className={styles.container}>
            {children}
        </div>
    )
}
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/67a66079-9434-722e-211d-cb55896aa7ea.png" width="500" />

#### Global Styles
`global.css`を編集することでサイト全体にスタイルを適用できます。
リセットCSSなどを読み込むのが一般的かと思います。
`global.css`を以下のように編集してみましょう。

```css:styles/global.css
html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
    Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
  line-height: 1.6;
  font-size: 18px;
}

* {
  box-sizing: border-box;
}

a {
  color: #0070f3;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

img {
  max-width: 100%;
  display: block;
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/f1df6e8a-cbc9-7897-4819-e7f472999af8.png" width="500" />

#### Polishing Layout
CSSモジュールを使ってレイアウトを更新していきます。

```css:components/layout.module.css
.container {
  max-width: 36rem;
  padding: 0 1rem;
  margin: 3rem auto 6rem;
}

.header {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.backToHome {
  margin: 3rem 0 0;
}
```

```css:styles/utils.module.css
.heading2Xl {
  font-size: 2.5rem;
  line-height: 1.2;
  font-weight: 800;
  letter-spacing: -0.05rem;
  margin: 1rem 0;
}

.headingXl {
  font-size: 2rem;
  line-height: 1.3;
  font-weight: 800;
  letter-spacing: -0.05rem;
  margin: 1rem 0;
}

.headingLg {
  font-size: 1.5rem;
  line-height: 1.4;
  margin: 1rem 0;
}

.headingMd {
  font-size: 1.2rem;
  line-height: 1.5;
}

.borderCircle {
  border-radius: 9999px;
}

.colorInherit {
  color: inherit;
}

.padding1px {
  padding-top: 1px;
}

.list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.listItem {
  margin: 0 0 1.25rem;
}

.lightText {
  color: #666;
}
```

```tsx:component/layout.tsximport Head from 'next/head';
import Image from 'next/image';
import styles from './layout.module.css';
import utilStyles from '../styles/utils.module.css';
import Link from 'next/link';
import { ReactNode } from 'react';

const name = 'Your Name';
export const siteTitle = 'Next.js Sample Website';

interface Props {
    children: ReactNode
    home: boolean
}

export default function Layout({ children, home }: Props) {
    return (
        <div className={styles.container}>
            <Head>
                <link rel="icon" href="/favicon.ico" />
                <meta
                    name="description"
                    content="Learn how to build a personal website using Next.js"
                />
                <meta
                    property="og:image"
                    content={`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`}
                />
                <meta name="og:title" content={siteTitle} />
                <meta name="twitter:card" content="summary_large_image" />
            </Head>
            <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>
    );
}
```

```tsx:pages/index.tsx
import Head from 'next/head';
import Layout, { siteTitle } from '../components/layout';
import utilStyles from '../styles/utils.module.css';

export default function Home() {
    return (
        <Layout home>
            <Head>
                <title>{siteTitle}</title>
            </Head>
            <section className={utilStyles.headingMd}>
                <p>Hello, World</p>
                <p>
                    (This is a sample website - you’ll be building a site like this on{' '}
                    <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
                </p>
            </section>
        </Layout>
    );
}
```

見た目が整えられてきました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/5a0f5329-be10-1608-c66b-aee4aba510fd.png" width="500" />

### Pre-rendering and Data Fetching
今まではファイル分割の仕方やスタイルの当て方などUIに関わること主にみてきました。
ここからはロジックの箇所を詳しく学習します。

#### Pre-rendering
Next.jsの特徴の一つがプリレンダリングです。
プリレンダリングはサーバー側でHTMLを作成することで、ファーストビューでUIが見えるというメリットがあります。

プリレンダリングにはSSGとSSRという二つの種類があります。
SSGはビルド時のみHTMLを作成します。
SSRはリクエストのたびにHTMLを作成します。

SSGの方が高速なため、主にSSGを使用して要件に合わせてSSRを使うのが良さそうです。

#### Creating the markdown files
今回作成するブログではプロジェクトにマークダウンファイルを作成して、そのファイルをブログの記事とします。
またSSGを使ってブログシステムを構築します。
まずマークダウンファイルを追加しましょう。

```md:posts/pre-rendering.md
---
title: 'Two Forms of Pre-rendering'
date: '2020-01-01'
---

Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.

- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.

Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
```

```md:posts/ssg-ssr.md
---
title: 'When to Use Static Generation v.s. Server-side Rendering'
date: '2020-01-02'
---

We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.

You can use Static Generation for many types of pages, including:

- Marketing pages
- Blog posts
- E-commerce product listings
- Help and documentation

You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
```

#### Creating the utility function to read the file system
マークダウンファイルを追加できたら、それらを表現するモデルを作成します。

```ts:types/post.ts
export class Post {
    id: string;
    date: string;
    title: string;
    content: string;

    constructor(id: string, date: string, title: string, content: string) {
        this.id = id;
        this.date = date;
        this.title = title;
        this.content = content;
    }
}
```

ファイルシステムから読み込んで`Post`の配列を返す関数を作成します。

```ts:lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { Post } from '@/types/post';

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

export function getSortedPostsData(): Post[] {
  const fileNames = fs.readdirSync(postsDirectory);
  const allPostsData = fileNames.map((fileName) => {
    const id = fileName.replace(/\.md$/, '');

    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');
    const matterResult = matter(fileContents);

    return new Post(
      id,
      matterResult.data.date,
      matterResult.data.title,
      matterResult.content,
    )
  });

  const sorted = allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1;
    } else {
      return -1;
    }
  })

  return JSON.parse(JSON.stringify(sorted));
}
```

#### Using Static Generation (getStaticProps())
`getStaticProps()`を使います。
HTMLのビルドに必要な外部データを取得することができます。
`index.tsx`で使ってみましょう。

```tsx:pages/index.tsx
import Head from 'next/head';
import Layout, { siteTitle } from '../components/layout';
import utilStyles from '../styles/utils.module.css';
import { getSortedPostsData } from '@/lib/posts';
import { Post } from '@/types/post';

interface Props {
    allPostsData: Post[]
}

export async function getStaticProps(): Promise<{ props: Props }> {
    const allPostsData = getSortedPostsData();
    return {
        props: {
            allPostsData
        }
    }
}

export default function Home({ allPostsData }: Props) {
    return (
        <Layout home>
            <Head>
                <title>{siteTitle}</title>
            </Head>
            <section className={utilStyles.headingMd}>
                <p>Hello, World</p>
                <p>
                    (This is a sample website - you’ll be building a site like this on{' '}
                    <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
                </p>
            </section>

            <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
                <h2 className={utilStyles.headingLg}>Blog</h2>
                <ul className={utilStyles.list}>
                    {allPostsData.map(({ id, date, title }) => (
                        <li className={utilStyles.listItem} key={id}>
                            {title}
                            <br />
                            {id}
                            <br />
                            {date}
                        </li>
                    ))}
                </ul>
            </section>
        </Layout>
    );
}
```
取得したデータは`props`として`Home`コンポーネントに渡すことができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/14c7ec0e-4681-298c-4ff8-05bf52d6e6aa.png" width="500" />

### Dynamic Routes
投稿の詳細画面へ遷移できるようにします。
詳細画面のパスは`/posts/ブログ名`です。
`pages/posts/[id].tsx`を作成することで該当の画面へ遷移できます。
このような動的ルーティングを実現するためには`getStaticPaths`と`getStaticProps`を実装します。

#### Implement getStaticPaths
ここではファイル名の一覧を返してあげます。
まず`lib/posts.ts`にファイルの一覧を返す関数を追加しましょう。

```ts:lib/posts.ts
export function getAllPostIds() {
  const fileNames = fs.readdirSync(postsDirectory);
  return fileNames.map((fileName) => {
    return {
      params: {
        id: fileName.replace(/\.md$/, ''),
      },
    }
  })
}
```

ここで実装した関数を`[id].tsx`の`getStaticPaths`で呼び出します。

```tsx:pages/posts/[id].tsx
export async function getStaticPaths() {
    const paths = getAllPostIds();
    return {
        paths,
        fallback: false
    }
}
```

#### Implement getStaticProps
次に詳細画面で表示したいデータを取得します。
ファイル名をキーにブログの詳細を取得します。

`lib/posts.ts`にメソッドを追加しましょう。

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

  const matterResult = matter(fileContents);

  const post = new Post(
    id,
    matterResult.data.date,
    matterResult.data.title,
    matterResult.content,
  )
  return JSON.parse(JSON.stringify(post))
}
```

`getPostData`を`[id].tsx`の`getStaticProps`で実行します。

```tsx:pages/posts/[id].tsx
import Layout from "@/components/layout";
import { getAllPostIds, getPostData } from "@/lib/posts";
import { Post } from "@/types/post";

export async function getStaticPaths() {
    const paths = getAllPostIds();
    return {
        paths,
        fallback: false
    }
}

export async function getStaticProps({ params }: { params: { id: string } }) {
    const postData = await getPostData(params.id);
    return {
        props: {
            postData
        }
    }
}

interface Props {
    postData: Post
}

export default function PostDetail({ postData }: Props) {
    return (
        <Layout home={false}>
            {postData.title}
            <br />
            {postData.id}
            <br />
            {postData.date}
        </Layout>
    );
}
```

取得した内容を画面に表示すると画像のようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/bbab7036-a7ee-07e4-e000-4188961a92f7.png" width="500" />

#### Render Markdown
ブログの本文を画面に表示しましょう。
マークダウンをHTMLへ変換するライブラリとして`remark`と`remark-html`をインストールします。

```console
npm install remark remark-html
```

Postの詳細情報を取得するときにHTMLの文字列を取得するように修正します。
HTML文字列をそのまま画面に表示します。

まずPost詳細情報取得の箇所を修正します。

```ts:lib/posts.ts
export async function getPostData(id: string): Promise<Post> {
  const fullPath = path.join(postsDirectory, `${id}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  const matterResult = matter(fileContents);

  const processedContent = await remark()
    .use(html)
    .process(matterResult.content)

  const contentHtml = processedContent.toString()

  const post = new Post(
    id,
    matterResult.data.date,
    matterResult.data.title,
    contentHtml,
  )

  return JSON.parse(JSON.stringify(post))
}
```

取得してきた文字列を詳細画面で表示します。
`dangerouslySetInnerHTML`に文字列を指定することで実現できます。

```tsx:pages/posts/[id].tsx
export default function PostDetail({ postData }: Props) {
    return (
        <Layout home={false}>
            {postData.title}
            <br />
            {postData.id}
            <br />
            {postData.date}
            <br />
            <div dangerouslySetInnerHTML={{ __html: postData.content }} />
        </Layout>
    );
}
```

#### Adding title to the Post Page
詳細画面のタイトルを設定しましょう。
`pages/posts/[id].tsx`にも`Head`コンポーネントを追加します。

```tsx:pages/posts/[id].tsx
export default function PostDetail({ postData }: Props) {
    return (
        <Layout home={false}>
            <Head>
                <title>{postData.title}</title>
            </Head>
            <article>
                <h1 className={utilStyles.headingXl}>{postData.title}</h1>
                <div className={utilStyles.lightText}>
                    <Date dateString={postData.date} />
                </div>
                <div dangerouslySetInnerHTML={{ __html: postData.content }} />
            </article>
        </Layout>
    );
}
```

#### Formatting the Date
日付の表示を整えます。
`January 2, 2020`という表示になるようにします。
`date-fns`というライブラリをインストールします。

```console
npm install date-fns
```

`date-fns`を使って日付を表示するコンポーネントを実装します。

```tsx:components/date.tsx
import { format, parseISO } from "date-fns"

interface Props {
    dateString: string
}

export default function Date({ dateString }: Props) {
    const date = parseISO(dateString)
    return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}
```

`Date`コンポーネントを`pages/posts/[id].tsx`で使います。

```tsx:pages/posts/[id].tsx
export default function PostDetail({ postData }: Props) {
    return (
        <Layout home={false}>
            <Head>
                <title>{postData.title}</title>
            </Head>
            <article>
                <h1 className={utilStyles.headingXl}>{postData.title}</h1>
                <div className={utilStyles.lightText}>
                    <Date dateString={postData.date} />
                </div>
                <div dangerouslySetInnerHTML={{ __html: postData.content }} />
            </article>
        </Layout>
    );
}
```

表示は画像のような感じです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/639e7a09-2dc3-1871-d8cb-bc89d934a817.png" width="500" />

#### Polishing the Index Page
ホームページから各投稿の詳細へ遷移できるようにします。
既存の`li`要素をに`Link`コンポーネントを使って対応します。

```tsx:pages/index.tsx
export default function Home({ allPostsData }: Props) {
    return (
        <Layout home>
            <Head>
                <title>{siteTitle}</title>
            </Head>
            <section className={utilStyles.headingMd}>
                <p>Hello, World</p>
                <p>
                    (This is a sample website - you’ll be building a site like this on{' '}
                    <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
                </p>
            </section>

            <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
                <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>
    );
}
```

以上でブログアプリは完成です。
ソースコードはこちらです。
https://github.com/h-taro/nextjs-blog