Next.jsでつくる動的ページ

チュートリアルをもとに、Next.jsでの動的ページ作成方法を見ていきます。

チュートリアル用のリポジトリをクローンしておきましょう。

```console
$ git clone https://github.com/zeit/next-learn-demo.git
$ cd next-learn-demo
```

インストールとルーティングの基礎は[こちら](https://qiita.com/otsukayuhi/items/3ea0c40acbcf5cf68f2b)からどうぞ。


## クエリパラメーターで記事を表示する

サンプルのソースコードディレクトリに移動し、`npm i`しておきます。

```console
$ cd 3-create-dynamic-pages
$ npm i
```

`pages/index.js`では投稿リストを表示し、遷移先の`pages/post.js`ではクエリパラメーターをもとに記事タイトルを表示します。
以下のように書き換えてみてください。

```jsx
// pages/index.js
import Layout from '../components/MyLayout.js'
import Link from 'next/link'

// 各記事タイトルと遷移先のコンポーネント
const PostLink = props => (
  <li>
    <Link href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
)

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        {/* title属性がクエリパラメーターとして渡される */}
        <PostLink title="Hello Next.js" />
        <PostLink title="Learn Next.js is awesome" />
        <PostLink title="Deploy apps with Zeit" />
      </ul>
    </Layout>
  )
}
```

```jsx
// pages/post.js
import { withRouter } from 'next/router'
import Layout from '../components/MyLayout.js'

const Page = withRouter(props => (
  <Layout>
    {/* props.router.query で ?title={hoge}を取得できます*/}
    <h1>{props.router.query.title}</h1>
    <p>This is the blog post content.</p>
  </Layout>
))

export default Page
```

`pages/post.js`の`<Layout>`の中身をコンポーネントにしたい場合は、以下のようにします。

```jsx
// pages/post.js
import { withRouter } from 'next/router'
import Layout from '../components/MyLayout.js'

const Content = withRouter(props => (
  <div>
    <h1>{props.router.query.title}</h1>
    <p>This is the blog post content.</p>
  </div>
))

const Page = props => (
  <Layout>
    <Content />
  </Layout>
)

export default Page
```

## ルートマスキングによるクリーンなURL

Next.jsのルートマスキング機能を使うことにより、クリーンなURLを作成できます。

ディレクトリを移動して、`npm i`しておきましょう。

```console
$ cd ../4-clean-urls
$ npm i
```

`pages/index.js`を以下のように書き換えます。

```jsx
// pages/index.js
import Layout from '../components/MyLayout.js'
import Link from 'next/link'

// 各記事タイトルと遷移先のコンポーネント
const PostLink = props => (
  <li>
    {`/* asを使うことにより、hrefの記述をシンプルなものにすることができる */`}
    <Link as={`/p/${props.id}`} href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
)

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        <PostLink id="hello-nextjs" title="Hello Next.js" />
        <PostLink id="learn-nextjs" title="Learn Next.js is awesome" />
        <PostLink id="deploy-nextjs" title="Deploy apps with Zeit" />
      </ul>
    </Layout>
  )
}
```

`<Link>`コンポーネントの`as`に`<PostLink>`コンポーネントの`id`を、`href`に`title`をそれぞれ渡しています。

```jsx
// pages/post.jsから抜粋
const Page = withRouter(props => (
  <Layout>
    {/* props.router.query で ?title={hoge}を取得できます*/}
    <h1>{props.router.query.title}</h1>
    <p>This is the blog post content.</p>
  </Layout>
))
```

`withRouter`メソッドの引数`props`の`props.router.asPath`をみると、`pages/index.js`の`as`を受け取っていることがわかります。`href`で渡しているクエリーパラメーターは`?title={hoge}`なので、`props.router.query.title`では、`{hoge}`の部分が表示されます。

ただし、この方法ですとクライアントサイドでのレンダリング結果なので、`/p/{hoge}`ページで再読込をすると404になってしまいます。それを避けるために、カスタムサーバーAPIを使います。

## カスタムサーバーAPI

例によって、チュートリアルのディレクトリを移動しましょう。また、Expressを利用するので、一緒にインストールしてください。

```console
$ cd 5-clean-urls-ssr
$ npm i
$ npm install --save express
```

`server.js`を作成します。

```js
const express = require('express')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app
  .prepare()
  .then(() => {
    const server = express()

    server.get('/p/:id', (req, res) => {
      const actualPage = '/post'
      const queryParams = { title: req.params.id }
      app.render(req, res, actualPage, queryParams)
    })

    server.get('*', (req, res) => {
      return handle(req, res)
    })

    server.listen(3000, err => {
      if (err) throw err
      console.log('> Ready on http://localhost:3000')
    })
  })
  .catch(ex => {
    console.error(ex.stack)
    process.exit(1)
  })
```

また、package.jsonのnpm scriptsを以下のように書き換えます。

```json
{
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
  }
}
```

今度は、`/p/{hoge}`で再読込しても404にならなくなりました。しかし、`pages/index.js`から遷移してきたときと表示が異なってしまっているはずです。これは、`/p/{hoge}`ページだけでは`pages/index.js`から`?title={hoge}`を受け取ることができないからです。外部からAPIで情報を受け取る場合は、どちらも共通のIDを利用するため、これは問題にならないかと思われます。

## 外部APIから情報を取得する
チュートリアルではバットマンのAPIから情報を取得しているので、こちらもバットマンを呼んでみましょう。

ディレクトリを移動して、`npm i`しておきましょう。また、データを取得するために`isomorphic-unfetch`もインストールしておきましょう。

```console
$ cd ../6-fetching-data
$ npm i
$ npm install --save isomorphic-unfetch
```

`pages/index.js`を以下のように書き換えます。

```jsx
// pages/index.js
import Layout from '../components/MyLayout.js'
import Link from 'next/link'
import fetch from 'isomorphic-unfetch'

const Index = props => (
  <Layout>
    <h1>Batman TV Shows</h1>
    <ul>
      {props.shows.map(show => (
        <li key={show.id}>
          <Link as={`/p/${show.id}`} href={`/post?id=${show.id}`}>
            <a>{show.name}</a>
          </Link>
        </li>
      ))}
    </ul>
  </Layout>
)

Index.getInitialProps = async function() {
  const res = await fetch('https://api.tvmaze.com/search/shows?q=batman')
  const data = await res.json()

  console.log(`Show data fetched. Count: ${data.length}`)

  return {
    shows: data.map(entry => entry.show)
  }
}

export default Index
```

`getInitialProps`メソッドは、静的な非同期関数です。初回の読み込み時、`getInitialProps`はサーバーサイドで実行され、クライアント側のルーティングで遷移してきた場合はクライアントサイドで実行されます。関数内のconsole.logが、どのコンソールで表示されているかで確認できます。

`pages/post.js`側も対応しましょう。`server.js`を以下に書き換えます。

```js
// server.js
const express = require('express')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app
  .prepare()
  .then(() => {
    const server = express()

    server.get('/p/:id', (req, res) => {
      const actualPage = '/post'
      const queryParams = { id: req.params.id } // title: を id: に変更
      app.render(req, res, actualPage, queryParams)
    })

    server.get('*', (req, res) => {
      return handle(req, res)
    })

    server.listen(3000, err => {
      if (err) throw err
      console.log('> Ready on http://localhost:3000')
    })
  })
  .catch(ex => {
    console.error(ex.stack)
    process.exit(1)
  })
```

`pages/post.js`も書き換えます。

```jsx
// pages/post.js
import Layout from '../components/MyLayout.js'
import fetch from 'isomorphic-unfetch'

const Post = props => (
  <Layout>
    <h1>{props.show.name}</h1>
    <p>{props.show.summary.replace(/<[/]?p>/g, '')}</p>
    <img src={props.show.image.medium} />
  </Layout>
)

Post.getInitialProps = async function(context) {
  const { id } = context.query
  const res = await fetch(`https://api.tvmaze.com/shows/${id}`)
  const show = await res.json()

  console.log(`Fetched show: ${show.name}`)

  return { show }
}

export default Post
```

`pages/post.js`でもバットマンの情報が表示されるようになりました。

前回の[エントリー](https://qiita.com/otsukayuhi/items/3ea0c40acbcf5cf68f2b)と合わせて、Next.jsの大枠は見えてきたかと思います。