Next.jsに入門した


以下のチュートリアルをやっていきます
https://nextjs.org/learn/dashboard-app

htmlとJavaScriptがだいたい分かっている人向け記事です


# 1. 実行/開発環境について


Reactと同様、以下の3つの実行環境が使えます

A: ビルドしてサーバーに置いて実行する
B: 開発用サーバーを立てて実行する
C: オンブラウザで実行する

Aは本番用で、ビルドすればミニファイ済みのコードが手に入りますので本番環境ではこれを使うべきなんですが、わりと面倒なので本番以外では使わないと思います。BはReactの開発用サーバーを立てて使うやり方で、ライブラリをnpm installしておいてimportして使うという書き方が出来るので通常はこれで開発するんじゃないでしょうか。Cはhtmlのscriptタグでインポートして使うやり方で、既存サイトにちょい足ししたい時や今回のようなチュートリアル的用途に適していると思われます


# 2. React基本編


Next.jsはReactベースのフレームワークなので、基本的な書き方はReactと共通であるようです


# 2-1. JSXでDOM操作を書く


JSXはJavaScriptの中にXMLを書けるように拡張した仕組みで、これを使うとタグを追加する度にcreateElementしてcreateTextNodeしてappendChildしてsetAttributeをattributeの数だけやって、というのをやらずにタグをそのまま書くことが出来ます

<a href="https://nextjs.org/learn/foundations/how-nextjs-works/compiling">チュートリアルページ</a>に分かりやすい図画ありましたので貼っておきます
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/c1f9b454-31b0-b1c1-a4df-5949eaf0af26.png" width=80%>


例えば以下のような感じで書くことができます


```html:index.html
<html>
  <body>
    <div id="app"></div>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script type="text/jsx">
      const app = document.getElementById('app');
      ReactDOM.render(<h1>Develop. Preview. Ship. 🚀</h1>, app);
    </script>
  </body>
</html>
```


scriptタグでreactとreact-domとbabelをインポートしてReactDOMとJSXが使えるようにして、ReactDOM.render()に書いた内容をid="app"であるタグに追加しています

上記htmlファイルをブラウザで開くと以下のように表示される筈です


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/cd818155-82a9-3762-dd39-3d12e30b80f2.png" width=50%>


## 2-2. 関数で書く


タグ名の書き出しを大文字にすると関数を実行してくれます
関数から更に関数を呼ぶこともできますので、機能ごとに記述を分けて可読性を高めることができます


```html:index.html
<html>
  <body>
    <div id="app"></div>
    
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script type="text/jsx">
      const app = document.getElementById('app');
      
      function Header() {
        return <h1>Develop. Preview. Ship. 🚀</h1>;
      }
      
      function HomePage() {
        return (
          <div>
            {/* Nesting the Header component */}
            <Header />
          </div>
        );
      }
      
      ReactDOM.render(<HomePage />, app);
    </script>
  </body>
</html>
```


## 2-3. 変数を使う


関数ですので引数を渡すこともできます
以下ではtitle="React 💙"を引数として渡しています


```html:index.html
<html>
  <body>
    <div id="app"></div>
    
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script type="text/jsx">
    const app = document.getElementById('app');

    function Header(props) {
        return <h1>{props.title}</h1>
    }

    function HomePage() {
        return (
            <div>
                <Header title="React 💙" />
            </div>
        );
    }
        
    ReactDOM.render(<HomePage />, app)

    </script>
  </body>
</html>
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/0988f0aa-a583-4b0b-e9de-7b450c5b721b.png" width=40%>


引数はオブジェクトとして受け渡されるので、変数に直接代入していくこともできます


```javascript:引数を直接変数に受け取る
function Header({ title }) {
    return <h1>{title}</h1>;
}
```


テンプレートリテラルや関数を呼ぶ書き方もできます


```javascript:テンプレートリテラル
function Header({ title }) {
    return <h1>{`Cool ${title}`}</h1>;
}
```

```javascript:関数を呼ぶ
function createTitle(title) {
    if (title) {
        return title;
    } else {
        return 'Default title';
    }
}
  
function Header({ title }) {
    return <h1>{createTitle(title)}</h1>;
}
```


条件演算子も使えます


```javascript:条件演算子を使う
function Header({ title }) {
  return <h1>{title ? title : 'Default Title'}</h1>;
}
```


## 2-4. 繰り返し処理


map()のarrow関数の中にXMLを書くと順番に置いていってくれます


```html:見出しの列挙
<html>
  <body>
    <div id="app"></div>
    
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script type="text/jsx">
        
        const app = document.getElementById('app');

        function Header({ title }) {
            return <h1>{title}</h1>;
        }
            
        function HomePage() {
            const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'];
          
            return (
                <div>
                    <Header title="Develop. Preview. Ship. 🚀" />
                    <ul>
                        {names.map((name) => (
                            <li>{name}</li>
                        ))}
                    </ul>
                </div>
            );
        }

        ReactDOM.render(<HomePage />, app)

    </script>
  </body>
</html>
```


「え？どういう仕組みでそうなる？？」と戸惑いながらも便利さに震えます


## 2-5. eventListenerを設定する


htmlに直接書くときと同じようにonClick={}でeventListenerを追加できます


```html:index.html
<html>
  <body>
    <div id="app"></div>
    
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script type="text/jsx">
        
        const app = document.getElementById('app');

        function Header({ title }) {
            return <h1>{title}</h1>;
        }
            
        function HomePage() {
            function handleClick() {
                console.log('increment like count');
            }
          
            return (
                <div>
                    <button onClick={handleClick}>Like</button>
                </div>
            );
        }

        ReactDOM.render(<HomePage />, app)

    </script>
  </body>
</html>
```


生成されたhtmlを見るとonClickは書かれていません
あくまでそういう書き方をするだけで、JSXが解釈してaddEventListenerしてくれるようです


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/88320d3c-2c0b-15e8-5f87-c14664f55445.png" width=30%>


## 2-6. useStateを使う


JavaScriptでUIを動かすときにはパーツごとに状態を表現する変数をもたせておいて、変数に更新があったら自動的に再描画するというのをやりますが、ReactのuseStateを使うとこれを自動でやってくれます

以下ではlikesという変数を保持しておいて、setLikes()で値が更新される度に再描画するように書いています。React.useState()がこの変数と変数更新用の関数を作ってくれます


```html:index.html
<html>
  <body>
    <div id="app"></div>
    
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script type="text/jsx">
        
        const app = document.getElementById('app');

        function Header({ title }) {
            return <h1>{title}</h1>;
        }
            
        function HomePage() {
            const [likes, setLikes] = React.useState(0);
          
            function handleClick() {
                setLikes(likes + 1);
            }
          
            return (
                <div>
                    <button onClick={handleClick}>Likes ({likes})</button>
                </div>
            );
        }
          
        ReactDOM.render(<HomePage />, app)

    </script>
  </body>
</html>
```


https://beta.reactjs.org/learn/render-and-commit
https://beta.reactjs.org/learn/referencing-values-with-refs
https://beta.reactjs.org/learn/managing-state
https://beta.reactjs.org/learn/passing-data-deeply-with-context
https://beta.reactjs.org/apis/react


# 3. Next.js基本編


ここからようやくNext.jsを使っていきます


## 3-1. 開発用サーバーのセットアップ


nodejsが入っている環境の適当なフォルダでnpx create-next-appを実行すればプロジェクトを作成できます。プロジェクト名でフォルダが作成されるので適当な名前をつけて、TypeScriptとESLintはどちらも有効にしておきます


```terminal:terminal
> npx create-next-app

Need to install the following packages:
  create-next-app@13.0.2
Ok to proceed? (y) y
√ What is your project named? ... my-app
√ Would you like to use TypeScript with this project? ... No / Yes
√ Would you like to use ESLint with this project? ... No / Yes
Creating a new Next.js app in C:\Users\jjaka\マイドライブ\python\_blog\react2\my-app.
```


最低限の構成までは自動で作ってくれますので、ここで開発用サーバーを起動すればサンプルページが http://localhost:3000/ に表示されます


```terminal:terminal
npm run dev
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/60910936-1a54-e0b5-5298-836d0f94221a.png" width=50%>


## 3-2. コードをNext.jsに書き換える


先ほどのnpx create-next-appで作ったプロジェクトフォルダにはフォルダやファイルがあれこれ作られています。Next.jsはpagesフォルダ内にあるフォルダ名をPATHとして使うので、pages/index.tsxがトップページになります


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/290df510-941c-784d-b718-a778db6f2fc1.png" width=25%>


先ほどまで書いていたコードと同じものを作ってみましょう

内容はほぼ同じですが、react.useStateをimport { useState } from 'react'で読み込んでいるのと、ページを書き出す元関数をexport defaultにしているところが変更点です


```javascript:pages/index.tsx
import { useState } from 'react';

function Header({ title }) {
  return <h1>{title ? title : 'Default title'}</h1>;
}

export default function HomePage() {
  const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'];
  const [likes, setLikes] = useState(0);

  function handleClick() {
    setLikes(likes + 1);
  }

  return (
    <div>
      <Header title="Develop. Preview. Ship. 🚀" />
      <ul>
        {names.map((name) => (
          <li key={name}>{name}</li>
        ))}
      </ul>

      <button onClick={handleClick}>Like ({likes})</button>
    </div>
  );
}
```


上記で書き換えると生成されるhtmlが以下のようになります。headタグやbodyタグについては何も書いてないんですが適宜やってくれるようです


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/929f8f5f-ef5a-531c-db8b-73173b3a9c74.png" width=50%>


表示されるのがこれですね


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/fa5418a1-3560-92fb-0c59-5760698877c3.png" width=70%>


## 3-3. ページを追加する


pages/posts/first-post.tsx を追加して以下のように記入すると http://localhost:3000/posts/first-post に新しいページが追加されます



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


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/d7ef33ef-7b17-4d9a-70ef-76af0e68e83e.png" width=50%>


## 3-4. リンクを設置する


next/link.Linkを使えばフォルダPATHでリンクを指定することができます


```javascript: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>
    </>
  );
}
```


Back to homeのところにトップページへのリンクが付いています


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/927cad06-d40c-871c-e153-fadfe5430f6c.png" width=35%>


## 3-5. 画像を表示する


publicフォルダ内のファイルであれば、どのページからでも参照することができます


```javascript
import Image from 'next/image';

const YourComponent = () => (
  <Image
    src="/images/profile.jpg" // Route of the image file
    height={144} // Desired size with correct aspect ratio
    width={144} // Desired size with correct aspect ratio
    alt="Your Name"
  />
);
```


## 3-6. headerを書く


next/head.Headを使うとheadタグ内の項目を書くことができます
以下ではheadタグ内にtitleを書いています


```javascript: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>
    </>
  );
}
```


## 3-7. scriptの読み込み


next/script.Scriptを使うと読み込みのタイミングを指定してJavascriptファイルを読み込めます

strategyによって読み込みタイミングが変わり、beforeInteractiveならページがインタラクティブになる前の自分が書いたJavascriptコードが実行される前に実行、afterInteractiveならページがインタラクティブになったすぐ後に実行、"lazyOnload"ならインタラクティブになった後にアイドル状態になったら実行という順番で優先順位をつけて読み込むことができますので、すぐに読む必要がないスクリプトは後回しにする事でページ表示を早めることができます。ボット検出やcookieなどの同意管理などはbeforeInteractive、タグマネージャーや分析系のタグはafterInteractiveにするのが一般的なようです。

読み込み終了後の処理をonLoadに書くことも出来ますので遅延読み込みへの対応もやりやすいようです


```javascript: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(`script loaded correctly, window.FB has been populated`)
        }
      />
      <h1>First Post</h1>
      <h2>
        <Link href="/">← Back to home</Link>
      </h2>
    </>
  );
}
```


## 3-8. cssの適用


cssの適用もスマートにやってくれます


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


```javascript:components/layout.js
import styles from './layout.module.css';

export default function Layout({ children }) {
    return <div className={styles.container}>{children}</div>;
}
```


```javascript:pages/posts/first-post.tsx
import Head from 'next/head';
import Link from 'next/link';
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>
  );
}
```


## 3-9. global CSS


pages/_app.tsxでCSSを読みこめば、すべてのページに共通して適用できます


```javascript:pages/_app.tsx
import '../styles/globals.css'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}
```


```javascript:styles/globals.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/277233/9894ca8a-0599-4c42-237e-e75ff5f15131.png" width=50%>



# 4. SSG, SSR


## 4-1. SSG (Static Site Generation)


### 4-1-1. SSGとは


ブログのようにコンテンツがある程度固定的なサイトの場合、サーバー側でhtmlを作成 (pre-rendering) しておいてリクエストが来たときにhtmlを送るだけにしておけば、表示までの時間を最小化することができます。この仕組みをSSG (Static Site Generation) と呼ぶんだそうで、これを使えることがNext.jsを採用したいという理由でNext.jsを選択する人も多いと思います


### 4-1-2. blogサイト用の記事読み込み機能を追加する


ここではblogを題材に作業しますので、以下からファイル一式をダウンロードしてきてプロジェクトに上書きして使いましょう
https://github.com/vercel/next-learn/tree/master/basics/data-fetching-starter


これで開発サーバーを立ち上げると http://localhost:3000/ に以下が表示される筈です

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/9f8ae355-e608-130c-f0c8-3be319394c10.png" width=50%>



このサイトについてSSGで配信するように変更を加えていきます
まず、各ページの情報を読み取る為にgray-masterをプロジェクトにインストールしましょう


```terminal:terminal
my-app> npm install gray-matter
```


続いてpostsフォルダにblogの記事データに相当するものを置いていきます
実際の実装では適当なバックエンドに送ってもらうか、ヘッドレスCMSサービスから受け取るかすると思いますが、とりあえずの動作デモ用に用意したものだと思います


```markdown:posts/pre-training.md（記事その1）
---
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 let's 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.
```





```javascript:ssg-ssr.md（記事その2）
---
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.

On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.

In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
```


このファイルを読む為の関数を定義します
記事はYAML Front Matterで書かれているので、gray-matterによりparseします
fs と path は Node.js のライブラリでファイルやPATHの操作をやってくれるものです


```javascript:lib/posts.js
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';

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

export function getSortedPostsData() {
  // Get file names under /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,
    };
  });
  // Sort posts by date
  return allPostsData.sort(({ date: a }, { date: b }) => {
    if (a < b) {
      return 1;
    } else if (a > b) {
      return -1;
    } else {
      return 0;
    }
  });
}
```


これをindex.tsxから読み出せるようにする為に読み出し用の関数を追加します
getStaticProps()をHome()から呼んでないのにこれで読み込みできるのが何故なのかよく分からなかったので、後日分かったら追記したいです


```javascript: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'

export default function Home({ allPostsData }) {
  return (
    <Layout home>
      <Head>…</Head>
      <section className={utilStyles.headingMd}>…</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>
  )
}

export async function getStaticProps() {
  const allPostsData = getSortedPostsData()
  return {
    props: {
      allPostsData
    }
  }
}
```


表示すると http://localhost:3000/ に以下が表示される筈です


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/9183444d-5316-a97a-3b02-7532162176e1.png" width=50%>


記事が表示されるようになりました


### 4-1-3. DBや外部フェッチから取得する


先程は記事をフォルダに置いて扱いましたが、APIエンドポイントから取得する場合はnode-fetchなどを使ってフェッチしてきたものをreturnしてやれば良いようです


```javascript:lib/posts.jsの書き方のイメージ
import fetch from 'node-fetch'

export async function getSortedPostsData() {
  // ファイルシステムのかわりに、
  // 外部の API エンドポイントから投稿データを取得する
  const res = await fetch('..')
  return res.json()
}
```


DBから読む場合は当該DBのSDKなどを使ってクエリを送って取得したものをreturnします


```javascript:lib/posts.jsの書き方のイメージ
import someDatabaseSDK from 'someDatabaseSDK'

const databaseClient = someDatabaseSDK.createClient(...)

export async function getSortedPostsData() {
  // ファイルシステムのかわりに
  // データベースから投稿データを取得する
  return databaseClient.query('SELECT posts...')
}
```


### 4-1-4. pre-renderingを設定する


Next.jsはデフォルト設定のままだと、すべてのページでpre-renderingしますので、じつは既にSSGとしてデプロイされています。開発環境ではnpm run devする度にpre-renderingが実行されますのでユーザー操作で情報が増えたような場合は一度開発サーバーを止めて再度npm run devする必要があります。実行環境の場合はビルドした際に1度だけpre-renderingされますので、ブログで記事が増えたときに再度ビルドする仕組みを仕込むなどしないといけないと思います

また、ユーザーが押した良いねボタンやコメントなどは即時い反映させたい場合はその部分だけSSRやCSRにしておく必要があります


## 4-2. SSR


SNSのようにユーザーリクエストがある度に新たにページをrenderingしたい場合はSSRまたはCSRを使うことになります。クライアント側のスペックが低くてクライアント側で処理するのが厳しい場合はSSRが選択されるようです。サーバーサイドでhtmlをrenderingするんであればフロントエンドフレームワークを使わない10年前ぐらいのアプリと同じじゃないかと思ったんですが、部分ごとにSSG/SSR/CSRのどれを使うかを選択できて、それらすべてを同じNext.jsで書けることがメリットになるようです

先程使ったgetStaticProps() ではなく getServerSideProps() を使うとSSRとして実行してくれるようです
例えば先程のindex.tsxの場合は以下のようになります


```javascript: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'

export default function Home({ allPostsData }) {
  return (
    <Layout home>
      <Head>…</Head>
      <section className={utilStyles.headingMd}>…</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>
  )
}

export async function getServerSideProps() {
  const allPostsData = getSortedPostsData()
  return {
    props: {
      allPostsData
    }
  }
}
```


## 4-3. CSR


CSRで書く場合はレンダリング終了後に必要な情報をフェッチして描画するという従来のSPA的なコードを自分で書く必要があります。React.jsでよく作られるSPAの動作がCSRそのものですので、従来通り書くことでCSRが実現できます

Next.jsとしては、データフェッチ用Reactフックを作成していて使用を推奨しているようです


```javascript:pages/index.tsx
import useSWR from 'swr'

function Profile() {
  const { data, error } = useSWR('/api/user', fetch)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}
```


# 5. 動的ルーティング


ブログの記事ページのように、動的に作成されるページのURLを指定する為の仕組みが動的ルーティングです。やり方は簡単で、"[id].js"のように[]を含むファイル名をpagesフォルダ内に置くとそこを変数で置き換えたPATHにページをルーティングしてくれます。getStaticPaths()でファイル名を渡して、getStaticProps()で書き出すページの内容を渡すと当該ページを生成してくれるようです


```javascript:pages/posts/[id].js
import Layout from '../../components/layout'
import { getAllPostIds, getPostData } from '../../lib/posts'

export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
    </Layout>
  )
}

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

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


```javascript:lib/posts.js
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

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

export function getSortedPostsData() {
  // Get file names under /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
    }
  })
  // 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)
  return fileNames.map(fileName => {
    return {
      params: {
        id: fileName.replace(/\.md$/, '')
      }
    }
  })
}

export function getPostData(id) {
  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)

  // Combine the data with the id
  return {
    id,
    ...matterResult.data
  }
}
```


これで以下に記事ページができている筈です
http://localhost:3000/posts/ssg-ssr
http://localhost:3000/posts/pre-render



# 6. 404ページをカスタマイズ


404のページは単に pages/404.tsx を作れば良いようです


```javascript:pages/404.tsx
export default function Custom404() {
  return <h1>404 - Page Not Found</h1>
}
```


# 7. APIルート


ウェブAPIをつくることも出来るようです


```javascript:pages/api/hello.tsx
export default (req, res) => {
    res.status(200).json({ text: 'Hello' })
}
```


http://localhost:3000/api/hello
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/520b8e54-bcef-94f9-c2f6-6fc1c2a900de.png" width=40%>


# 8. デプロイ


## 8-1. Vercelでデプロイする


### 8-1-1. githubにpushする


gitにリポジトリを作ってpushします


```terminal:terminal
git init
git add .
git commit -m 'first commit'
git remote add origin 追加したいリポジトリ名
git push -u origin main
```


### 8-1-2. Vercelにデプロイする


Vercelにアクセスしてログインします
https://vercel.com/


ログインしたらcreate new project → GitHub → 先程pushしたリポジトリを選択と進みます


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/907928af-2112-bd32-2930-02088ff02275.png" width=50%>


正常にインポートできたら自動でどのフレームワークで作られているか判別してくれます
Deployボタンを押すとビルドがはじまります


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/3ae21c7b-d5c4-c5b8-7c28-af4adcb312d2.png" width=50%>


上手くいったようです


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/9183444d-5316-a97a-3b02-7532162176e1.png" width=50%>


## 8-2. ビルドしてデプロイする


公式チュートリアルではNode.jsでデプロイするやり方も紹介されています
https://nextjs.org/learn/basics/deploying-nextjs-app/other-hosting-options


デプロイする環境にファイル一式を置いて以下を実行します


```terminal:terminal
npm run build
```


上手くビルド出来たら以下でサーバーを起動します


```terminal:terminal
npm run start
```




# 9. ヘッドレスCMSでブログをつくる


microCMSの公式チュートリアルをやっていきます
https://blog.microcms.io/microcms-next-jamstack-blog/


## 9-1. プロジェクトの作成


適当なフォルダに移動して以下を実行します


```terminal:terminal
npx create-next-app blog-with-microcms
```


## 9-2. microCMSアカウントをつくる


microCMSにアクセスします
https://app.microcms.io/


まだアカウントがない人はつくります


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/52505ea4-f2d7-bad5-6cb6-6547ce9d23fe.png" width=50%>



## 9-3. サービスをつくる


サービス名は自分が分かる名前、サービスIDは乱数でもなんでも良いけど他の人がまだ使ってない文字列にします


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/fceaad07-1dee-0d5a-e960-2a4cfad29f69.png" width=50%>


## 9-4. APIをつくる


ウェブAPIをつくります
今回はブログなのでブログを選択します


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/e1f0cc8e-16f7-43ae-cf09-977ce470fdf5.png" width=50%>


できました


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/1ab5098a-7a66-fd0f-285a-ef892f4e56ff.png" width=50%>


サンプル記事が既に作成されていますのでクリックして開くと、内容を編集できるようになっています


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/2c0d281b-403d-06a1-e62a-ff1c58c6ce8a.png" width=50%>


右上にあるAPIプレビューをクリックすると、microCMSのsdkでの取得例が見られます


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/a4e9477b-8e6d-6b7a-a996-bf4384ea52d2.png" width=35%>


## 9-5. 記事一覧ページをつくる


APIキーはコンソールの左下に書かれている「1個のAPIキー」をクリックして、COPYボタンをクリックすると取得できます


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/921acf74-666e-f856-b42b-ff61e5a6d1ba.png" width=50%>


APIキーがあると記事を自由に読み書き改変できるようになってしまうので、うっかりgithubにアップしてしまわないように.env.localなどのgitがアップロードしないファイルに書いておく必要があります

今回はNext.jsプロジェクトフォルダのトップに.env.localファイルを作ってAPIキーを記入します


```javascript:blog-with-microcms/.env.local
API_KEY = 3e80a65f4d6a54f89a65f4d6a54f45f2e8f4
```


続いて、右上にあるAPI設定を開いてドメインを確認します


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/2a83ad42-d7fe-e0d6-a4c4-a7dfe17bdc07.png" width=50%>


libs/client.jsファイルにmicroCMSクライアントをつくっていきます
serviceDomainは先程確認したドメイン、apiKeyは先程.env.localに置いたAPI_KEYを使います


```javascript:blog-with-microcms/libs/client.js
import { createClient } from "microcms-js-sdk";

export const client = createClient({
    serviceDomain: "blog-tutorial-nextjs",
    apiKey: process.env.API_KEY,
})
```


取得した情報をindex.tsxで表示してみましょう。/libs/client.jsからclientをインポートして、getStaticProps()で先程作成したAPIからデータを取得します。endpointはAPIをつくるときに自分で決めたものですがコンソールの左側にある基本情報から確認できます。clientがget()したオブジェクトの内容がどうなっているかはAPIプレビューから確認できます


```javascript:blog-with-microcms/index.tsx
import Link from 'next/link';
import { client } from '../libs/client';

export async function getStaticProps() {
  const data = await client.get({
    endpoint: 'blogs',
  });

  return {
    props: {
      data,
    },
  };
};

export default function Home({ data }) {
  return (
    <div>
      { data.contents.map((content) => (
        <li key={ content.id }>
          <Link href={`/blog/${content.id}`}>
            { content.title }<br />
          </Link>
        </li>
      ))}
    </div>
  );
};
```


まだ記事が1つしかないので1行だけ表示される筈です


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/a2248ee9-211c-861f-2538-cda65d2721a0.png)


## 9-6. 記事ページをつくる


getStaticPaths()でSSGするURLを指定し、getStaticProps()で取得した値を使ってページを生成します

受け取ったhtmlをhtmlとしてそのまま貼り付けたいのでdangerouslySetInnerHTMLで貼っていますが、何故dangerouslyと付いているかというと、ここにJavascriptを仕込んだらコードが実行されてしまうからです。ユーザーから受け取った文字列を放り込んだりするとXSS脆弱性を付いた攻撃が可能になってしまいますので避けるべきですが、今回は自分が用意して自分が書き込んだヘッドレスCMSからの文字列しか来ない筈なので大丈夫だろうという使い方で、<a href="https://blog.microcms.io/microcms-next-jamstack-blog/">microCMSのチュートリアル</a>でもこの書き方になっています


```javascript:blog-with-microcms/pages/blog/[id].js
import { client } from '../../libs/client';

//SSG
export async function getStaticProps(context) {
    const id = context.params.id;
    const data = await client.get({
      endpoint: 'blogs',
      contentId: id,
    });
  
    return {
      props: {
        blog: data,
      },
    };
  };

export async function getStaticPaths() {
    const data = await client.get({ endpoint: "blogs" });
    const paths = data.contents.map((content) => `/blog/${content.id}`);
    return {
        paths,
        fallback: false,
    };
};
  
export default function BlogId({ blog }) {
    return (
        <main>
            <h1>{blog.title}</h1>
            <p>{blog.createdAt}</p>
            <div dangerouslySetInnerHTML={{ __html: `${blog.content}` }}></div>
        </main>
    );
};
```


できました


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/c92ea605-ac7f-f14e-a092-f395f5dedc0c.png" width=50%>


## 9-7. デザインを整える


microCMSからの取得はちゃんと出来たので、デザインを整えていきます


```terminal:terminal
npm install sass
```






```sass:blog-with-microcms/styles/Home.module.scss
.main {
  width: 960px;
  margin: 0 auto;
}

.title {
  margin-bottom: 20px;
}

.publishedAt {
  margin-bottom: 40px;
}

.post {
  & > h1 {
    font-size: 30px;
    font-weight: bold;
    margin: 40px 0 20px;
    background-color: #eee;
    padding: 10px 20px;
    border-radius: 5px;
  }

  & > h2 {
    font-size: 24px;
    font-weight: bold;
    margin: 40px 0 16px;
    border-bottom: 1px solid #ddd;
  }

  & > p {
    line-height: 1.8;
    letter-spacing: 0.2px;
  }

  & > ol {
    list-style-type: decimal;
    list-style-position: inside;
  }
}
```


```javascript:blog-with-microcms/
import { client } from '../../libs/client';
import styles from '../../styles/Home.module.scss';

//SSG
export async function getStaticProps(context) {
    const id = context.params.id;
    const data = await client.get({
      endpoint: 'blogs',
      contentId: id,
    });
  
    return {
      props: {
        blog: data,
      },
    };
};

export async function getStaticPaths() {
    const data = await client.get({ endpoint: "blogs" });
    const paths = data.contents.map((content) => `/blog/${content.id}`);
    return {
        paths,
        fallback: false,
    };
};
  
export default function BlogId({ blog }) {
    return (
        <main className={styles.main}>
            <h1 className={styles.title}>{blog.title}</h1>
            <p className={styles.publishedAt}>{blog.createdAt}</p>
            <div dangerouslySetInnerHTML={{
                 __html: `${blog.content}` 
            }}
            className={styles.post}></div>
        </main>
    );
};
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/011fddda-1844-30d2-a869-bb7e11b26a11.png" width=50%>


## 9-8. ビルドする


```terminal:terminal
npm run build
```


## 9-9. githubにpushする


gitにリポジトリを作ってpushします


```terminal:terminal
git init
git add .
git commit -m 'first commit'
git remote add origin 追加したいリポジトリ名
git push -u origin main
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/61882580-b5a0-5c4c-cf45-6a51353faae9.png" width=50%>


## 9-10. Vercelにデプロイする


Vercelにアクセスしてログインします
https://vercel.com/


ログインしたらcreate new project → GitHub → 先程pushしたリポジトリを選択と進みます


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/907928af-2112-bd32-2930-02088ff02275.png" width=50%>


正常にインポートできたら自動でどのフレームワークで作られているか判別して表示してくれている筈です。ここでDeployボタンを押すとビルドがはじまるんですが、.gitignoreで.env.localをアップロードしなくした結果としてAPI_KEYが取得できなくなっているので、ここのメニューで環境変数としてAPI_KEYを渡してやります


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/1fe61f65-eab5-97a2-f970-5aef5b2b1d95.png" width=50%>


上手くいったら以下みたいに表示される筈です


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/f57f1117-8a94-65f8-67d6-390837e275df.png" width=50%>


左側にトップページが表示されていますが、ここをクリックすると現時点でデプロイされているURLに移動できます。この時点では "https://リポジトリ名.vercel.app" に表示されていますが、自分のドメインで表示したい筈ですので、右側にあるAdd Domainのボタンをクリックしてドメインの指定に進みたいと思います


## 9-11. microCMSとVercelを結びつける


microCMS側で記事を追加したときに自動でVercelが再ビルドする仕組みがあるので設定していきます


### 9-11-1. Deploy Hookをつくる


まずVercel側にここをつつけば再ビルドしてくれる、というHookをつくります
Vercel上で先ほど作ったプロジェクトのSettingsを開き、Git欄にあるDeploy Hooksに適当なHook名、Githubのブランチ (通常はmainだと思います) を入力してCreate HookボタンをクリックしたらHookをつくってくれます


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/40992b13-b564-6a3a-bdab-e5f894284fad.png)


### 9-11-2. microCMSにHookを登録する


API設定のWebhook欄で追加を選び、Vercelを選択、適当なHook名と先程つくったVercelのDeploy Hookを貼り付けます


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/51848c0a-cb6b-c5c3-50c0-e37da7adf595.png" width=50%>


microCMSで記事を追加してみると、Vercel側はいじらなくても自動的に再ビルドしてくれました


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/7b2bcd66-471a-50cc-e4ba-b8bf574d1b4f.png" width=20%>



## 9-12. 独自ドメインでアクセスできるようにする


Vercelの設定メニューでDomainsを選ぶと自分のドメインを設定できます


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/fd1d79c9-ed4b-f43e-cba5-694504c363f4.png" width=50%>


テキストボックスに設定したいURLを入力してADDボタンを押すとDNSサーバーに設定する内容が表示されますので、これを自分のDNSサーバーに設定すればOKです


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/429baf6e-ef4c-47cc-5205-54538f7ffc8b.png" width=50%>


お名前どっとこむの場合はDNS設定からDNSレコード設定に入ってType(AとかCNAMEとか)とValueを記入します


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/a12de57d-91c1-c7b7-d5a6-c300a7b415a5.png" width=50%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/8f1ee9ac-2532-3da4-9af2-b5759d62778a.png" width=50%>


しばらく待つと、VercelのDomain設定ページで登録したURLが有効になっているのを確認できます


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/35b89e9c-3974-5429-ca94-ae87c7ab6d90.png" width=50%>


問題なく表示できるのを確認したら、要らなくなった仮のURLの方を削除して完成です