45
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Next.jsチュートリアル ノート ( +日本語翻訳)

Last updated at Posted at 2021-03-12

概要

英語版しか存在していないNext.jsのチュートリアル(v10.0.0)について、機械翻訳などを参考にしつつ、以下の通り実行した内容や翻訳した内容などメモしながら進めてみたのでその記録です。

チュートリアル中の冗長な部分を短くまとめたりと本家と異なる部分もありますが、一通りの内容を網羅しています。

※ 訳が間違っている場合もありますので、英語版を読むのが確実です。

チュートリアル

1. Create a Next.js App

Next.jsはReactのフレームワーク。
Reactでアプリケーションを作るには、以下を考慮する必要がある。

  • バンドル(Webpack)、コンパイル(Babel)
  • コード分割による最適化
  • SEOのためのプリレンダリング、サーバーサイドレンダリング
  • サーバーサイドとの接続

Next.jsは以下の機能を提供する。

  • ページベースのルーティング
  • SSG、SSRのサポート
  • 自動コード分割
  • クライアントサイドルーティングにおけるプリフェッチの最適化
  • CSS(SCSS)のimportやCSS-in-JSのサポート。
  • Fast Refreshを持つ開発環境

Create a Next.js App

  • create-next-app

nextアプリケーションを作成するためのCLIツール。

アプリケーションの作成。
(以下、記事の内容に加えて行ったgit操作についてもメモ)

yarnを使いたいので、「--use-npm」オプションは外した。ディレクトリ名も「nextjs-blog」から「next-tutorial」に変更した。

npx create-next-app next-tutorial --example "https://github.com/vercel/next-learn/tree/master/basics/learn-starter"

コミットされてしまったので、「.git」を一旦削除した。

rm -rfd .git

ユーザー名を変更して、改めてコミット。

git init
git config user.name {{ユーザー名}}
git config user.email {{メールアドレス}}
git add .
git commit -m "Initial commit."

ビルド & 開発サーバーの立ち上げ

cd next-tutorial
yarn dev

Welcome画面が「http://localhost:3000/」 に表示される。

スクリーンショット 2021-03-06 17.06.34.png

「pages/index.js」を開き、「Welcome」を「Learn」に書き換える。
するとFast Refreshにより、即時に画面に反映される。リロードは必要ない。

         <h1 className="title">
-          Welcome to <a href="https://nextjs.org">Next.js!</a>
+          Learn to <a href="https://nextjs.org">Next.js!</a>
         </h1>

2. Navigate Between Pages

Pages in Next.js

この章では以下の内容に触れる。

  • ファイルシステムルーティング

  • Linkコンポーネント

  • コード分割、プリフェッチングの組み込みサポート

Create a New Page

「pages/posts」ディレクトリの作成

mkdir pages/posts

ファイルとURLは以下のように対応する。

  • pages/index.js: 「/」
  • pages/posts/first-post.js: 「posts/first-post」
// pages/posts/first-post.js

+export default function FirstPost() {
+  return <h1>First Post</h1>;
+}

URL: http://localhost:3000/posts/first-post にアクセスすると、「First Post」と表示される。
HTMLを書く代わりにこのようにJSXでReactコンポーネントを記述することによりページを作成する。

Link Component

外部サイトへのリンクは<a>タグを利用する。
一方、内部リンクにはLinkコンポーネントのタグを利用する。

first-post.jsを以下の通り書き換える。

// pages/posts/first-post.js

+import Link from 'next/link';
+
 export default function FirstPost() {
-  return <h1>First Post</h1>;
+  return (
+    <>
+      <h1>First Post</h1>
+      <h2>
+        <Link href="/">
+          <a>Back to home</a>
+        </Link>
+      </h2>
+    </>
+  )
 }

「Back to home」をクリックすると、Welcome画面に遷移する。

Client-Side Navigation

<Link>タグは「client-side navigation」を実現するコンポーネント。
これにより、ブラウザデフォルトの画面遷移ではなく、JavaScriptによって画面遷移しているように見せている。

※ よって、本当の意味での画面遷移ではないので、例えば、ブラウザの開発ツールで「first-post」画面のCSSを変更し、「Back to home」ボタンを押すと、Welcome画面にもCSSが適用されていることが確認できる。
一方、<a>タグでは従来どおりの画面遷移となるので、同じことをするとCSSはリセットされる。

  • コード分割

Next.jsは自動的にコード分割を行うので、画面表示に必要な部分のみデータを取得し、レンダリングされる。
これにより、素早く画面を表示できるメリットはあるが、あるページでエラーが起きた場合、エラーの起きていない部分は動き続けることになる。

  • プリフェッチ

本番ビルド時、プリフェッチが有効になる。
Linkコンポーネントがビューポートに表示されると、Next.jsではリンク先のコードをバックグラウンドでプリフェッチする。
これにより、画面遷移する際にはすでにページのコードが読み込まれていることになり、素早い画面遷移を実現している。

3. Assets, Metadata, and CSS

Next.jsはCSSとSCSSを組み込みサポートしている。
この章では、画像やメタタグ等の扱いについて学ぶ。

具体的には以下の通り。

  • 画像等の静的アセットの追加
  • headタグ内のカスタマイズ
  • CSSモジュールを使った再利用可能なReactコンポーネントの作成
  • グローバルなCSSの追加
  • 便利なスタイリング

Assets

robots.txtや画像等のアセットは「public」ディレクトリ以下に配置する。

imagesディレクトリの作成

mkdir public/images

適当な画像を配置する。
なければ以下を使って良いとある。

Download your profile picture in .jpg format (or use this file).

wget https://raw.githubusercontent.com/vercel/next-learn-starter/master/basics-final/public/images/profile.jpg

取得した画像を「public/images」ディレクトリに移動。

mv profile.jpg public/images/

Imageコンポーネント

画像をHTMLに組み込むにあたり、<img>タグを利用することもできるが、
これだと最適化を自力で行う必要がある。
そこで、Next.jsに組み込まれたImageコンポーネントの<Image>タグを利用する。

Imageコンポーネントはリサイズ、最適化、WebPへの変換などを自動的に行う。これらの処理はCMSにホストされたものに対してでも同様に実行される。

他の静的サイトジェネレータ等と異なり、Next.jsでは最適化をビルド時にではなくユーザーからのリクエスト時に実行する。これにより、ビルドが遅くなることが無い。

画像のレンダリングについて。画像はデフォルトでLazyLoadされ、さらにSEOに影響を与える「CLS (Cumulative Layout Shift)」が起きないようにレンダリングされる。

Metadata

<title>タグをどこに書けばよいか。
このような通常<head>タグ内に書くべき内容はHeadコンポーネントの<Head>タグ内に記述する。

// pages/first-post.js


 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>

Assets, Metadata, and CSS

CSSについて。
Next.jsでは"CSS-in-JS"ライブラリの「styled-jsx」を組み込みサポートしている。もちろん、styled-componentsとかemotion等のライブラリも追加すれば利用可能。また、それに加えてCSS, SCSSのimportもサポートしている。

Layout Component

プロジェクトのトップディレクトリ直下に「components」ディレクトリを作成する。

mkdir components

ファイル「components/layout.js」を作成し、以下のようにする。

components/layout.js

+
+export default function Layout({children}) {
+  return <div>{children}</div>
+}

「pages/first-post.js」を以下のように修正し、Layoutコンポーネントでラップするよう変更する。


 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>
@@ -13,6 +14,6 @@ export default function FirstPost() {
           <a>Back to home</a>
         </Link>
       </h2>
-    </>
+    </Layout>
   )
 }

Adding CSS

「components/layout.module.css」を作成し、以下のように修正する。
CSSモジュールを利用するには、このように拡張子を「.module.css」にする。

// components/layout.module.css

+.container {
+  max-width: 36rem;
+  padding: 0 1rem;
+  margin: 3rem auto 6rem;
+}

CSSを適用するため、「components/layout.js」を以下のように修正する。

// components/layout.js

-
+import styles from './layout.module.css';

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

CSS Modulesにより、クラス名が自動的に生成され、Layoutコンポーネントにのみ適用されるようになる。CSS Modulesで記述したスタイルについては、ビルド時にJavaScriptから抽出されて「.css」ファイルとして別途生成される。

Global Styles

CSS Modulesは便利だが、グローバルにスタイルを当てるにはどうすればよいか。
「pages/_app.js」を追加し、以下のようにAppコンポーネントを作成する。

// pages/_app.js

+export default function App({Component, pageProps}) {
+  return <Component {...pageProps} />;
+}

このAppコンポーネントを各ページでトップレベルのコンポーネントとして扱うようにし、これに対してグローバルなスタイルを当てるようにする。グローバルなCSSはこの「pages/\app.js」でしか読み込めないようになっている。

「styles」ディレクトリをトップディレクトリ直下に作成する。

mkdir styles

グローバルなCSSを記述する「styles/global.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;
+}
+

「pages/_app.js」にCSSを読み込むよう修正を入れる。

// pages/_app.js

+import '../styles/global.css';
+
 export default function App({Component, pageProps}) {
   return <Component {...pageProps} />;
 }

開発サーバーを再起動し、ブラウザから「/posts/first-post」を開くと、CSSが適用されて表示される。

yarn dev

スクリーンショット 2021-03-08 18.29.20.png

Polishing Layout

ここまで最小限のコンポーネントとCSSを実装したが、次の章でプリフェッチをより深く学ぶため、アップデートする。

「components/layout.module.css」を以下のように修正する。

/* components/layout.module.css */


   max-width: 36rem;
   padding: 0 1rem;
   margin: 3rem auto 6rem;
-}
\ No newline at end of file
+}
+
+.header {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.backToHome {
+  margin: 3rem 0 0;
+}

「styles/utils.module.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;
+}

「components/layout.js」を修正する。

// components/layout.js


+import 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';

-export default function Layout({children}) {
-  return <div className={styles.container}>{children}</div>
+const name = 'Your Name';
+export const siteTitle = 'Next.js Sample Website';
+
+export default function Layout({children, home}) {
+  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=?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
+              property
+              src="/images/profile.jpg"
+              className={utilStyles.borderCircle}
+              height={144}
+              width={144}
+              alt={name}
+            />
+            <h1 className={utilStyles.heading2Xl}>{name}</h1>
+          </>
+        ) : (
+          <>
+            <Link href="/">
+              <a>
+                <Image
+                  priority
+                  src="/images/profile.jpg"
+                  className={utilStyles.borderCircle}
+                  height={108}
+                  width={108}
+                  alt={name}
+                />
+              </a>
+            </Link>
+            <h2 className={utilStyles.headingLg}>
+              <Link href="/">
+                <a className={utilStyles.colorInherit}>{name}</a>
+              </Link>
+            </h2>
+          </>
+        )}
+      </header>
+      <main>
+        {children}
+      </main>
+      {!home && (
+        <div className={styles.backToHome}>
+          <Link href="/">
+            <a>← Back to home</a>
+          </Link>
+        </div>
+      )}
+    </div>
+  );
 }

「pages/index.js」の中身を以下の内容に変更する。

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>Nullam hendrerit faucibus arcu nec viverra. Mauris scelerisque arcu eget neque tincidunt, id vestibulum odio convallis. </p>
        <p>
          Vestibulum quis malesuada enim. Vestibulum sollicitudin dignissim magna, vel euismod tellus auctor eu. Integer tellus elit, mollis in nulla eget, placerat congue ex. Nullam dignissim arcu magna, tincidunt fermentum nisl pulvinar interdum. In posuere, nulla et cursus auctor, mauris lorem sagittis erat, nec convallis nibh tortor at tellus.
        </p>
      </section>
    </Layout>
  )
}

スクリーンショット 2021-03-08 19.13.24.png

Styling Tips

  • ライブラリ「classnames」を利用するとクラス名のトグルが楽に実装できる。

実装例

export default function Alert({ children, type }) {
  return (
    <div
      className={cn({
        [styles.success]: type === 'success',
        [styles.error]: type === 'error'
      })}
    >
      {children}
    </div>
  )
}
  • Next.jsはデフォルトでPostCSSを使ってコンパイルする。

参考:

postcss.config.jsonに設定を書くことで設定可能。

  • Sassに対応しているので、利用したければ単に拡張子を「.scss」や「.sass」にすればよい。

sassのインストールは以下の通り。

yarn install sass

4. Pre-rendering and Data Fetching

この章では以下のことを学ぶ。

  • Next.jsのプリレンダリング
  • Static Generationとサーバーサイドレンダリング(SSR)
  • データ有り/無しのStatic Generation
  • getStaticPropsをインデックスページで利用するには
  • getStaticPropsについて

Pre-rendering

データフェッチングの前に、まずはプリレンダリング(Static Generation, サーバーサイドレンダリング)のコンセプトについて。
Next.jsはデフォルトで全ページでプリレンダリングを行う。

プリレンダリングをするということは、つまり、JavaScriptによってクライアント側でのみ全てをレンダリングするのではなく、サーバー側で予めHTMLを生成しておく。これによりパフォーマンスとSEOに良い影響が出る。

生成したHTMLは最小限のJavaScriptに紐付けられる。ブラウザで画面をロードする際、それらのJavaScriptが実行されて、ページがインタラクティブなものになる。このプロセスを「hyderation」と呼ぶ。

Check That Pre-rendering Is Happening

プリレンダリングについて学ぶには、JSの実行を無効化してアクセスしてみるとよい。
Next.jsを使っていないプレーンなReact.jsアプリケーションではプリレンダリングは実行されない。
まとめると、

  • プレーンなReact: レンダリング済みのHTML表示 -> JS読み込み -> 初期化。インタラクティブに。
  • Next.js: レンダリングされていないHTML表示 -> JS読み込み -> 初期化。インタラクティブに。

Two Forms of Pre-rendering

Static Generationとサーバーサイドレンダリングの違いについて。

  • Static Generation: ビルド時にHTMLを生成するプリレンダリングの手法。HTMLはリクエスト毎に再利用される。
  • サーバーサイドレンダリング: リクエスト毎にHTMLを生成するプリレンダリングの手法。

Per-page Basis

Next.jsでは、Static Generationとサーバーサイドレンダリングのどちらを使用するか、あるいは両方利用したハイブリッド型を採用するか選択できる。

開発サーバーではStatic Generationを利用している場合でもリクエスト毎にページが生成される。

2つの手法はそれぞれいつ使うべきか。
Static Generationを利用してビルド済みのHTMLをCDNから配信すると、毎回リクエスト毎にページを生成するより高速化されるので、可能ならば利用することが推奨される。

Static Generationは以下のようなサイトに利用できる。

  • LP
  • ブログ
  • ECサイトの商品リスト
  • ヘルプ

一方、頻繁に更新されるページやリクエスト毎に内容が変更されるページでは使えないので、サーバーサイドレンダリングを利用してリクエスト毎にHTMLを生成するか、あるいはデータの部分のプリレンダリングは行わずにその部分のみJavaScriptで埋め込む方法が使える。

Static Generation with and without Data

データ無しの場合のStatic Generationについて。
ここまでで作成した内容については、外部データの取得は必要無かった。
このような場合、Productionビルドを実行するとHTMLが生成されるので、それを単に配信すればよい。

Static Generation with Data using getStaticProps

データ有りの場合のStatic Generationについて。
getStaticPropsを利用することで外部データを扱うことが可能。

ページコンポーネントのexportに加えてgetStaticProps関数をexportすることにより、ページリクエスト毎に実行される処理を定義することができる。getStaticPropsの実行により取得したデータをpropsとしてページコンポーネントに渡せる。

実装例

export default function Home(props) { ... }

export async function getStaticProps() {
  // Get external data from the file system, API, DB, etc.
  const data = ...

  // The value of the `props` key will be
  //  passed to the `Home` component
  return {
    props: ...
  }
}

Blog Data

Markdownのブログシステムを作成していく。

トップディレクトリに「posts」ディレクトリを作成する。

「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.

「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.

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.

記事中のtitle, dateはライブラリ「gray-matter」によりパースできる。

getStaticPropsを利用してこれらのデータを表示する。
以下を実装していく。

  • Markdownファイルからtitle, date, ファイル名を取得。ファイル名はURLで記事のidとして利用する。
  • インデックスページを日付でソート。

「gray-matter」をインストール

yarn add gray-matter

トップディレクトリに「lib」ディレクトリを作成

mkdir lib

「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() {
+  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 {
+      id,
+      ...matterResult.data
+    };
+  });
+  return allPostsData.sort((a, b) => {
+    return a.date < b.date ? 1 : -1;
+  });
+}

「pages/index.js」を以下のように修正する。

 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() {
+export default function Home({ allPostsData }) {
   return (
     <Layout home>
       <Head>
@@ -14,6 +15,29 @@ export default function Home() {
           Vestibulum quis malesuada enim. Vestibulum sollicitudin dignissim magna, vel euismod tellus auctor eu. Integer tellus elit, mollis in nulla eget, placerat congue ex. Nullam dignissim arcu magna, tincidunt fermentum nisl pulvinar interdum. In posuere, nulla et cursus auctor, mauris lorem sagittis erat, nec convallis nibh tortor at tellus.
         </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>
   )
 }
+
+export async function getStaticProps() {
+  const allPostsData = getSortedPostsData();
+  return {
+    props: {
+      allPostsData
+    }
+  };
+}

以上の修正により、ファイルシステムから取得した外部データを含むプリレンダリングが実現できた。
ブラウザから確認すると、以下のように表示される。

スクリーンショット 2021-03-09 19.04.27.png

getStaticProps Details

外部API・データベースへの問い合わせについても先程と同様に実装できる。
例えば、以下のようにクエリを書いても問題ない。
なぜなら、getStaticPropsはサーバーサイドでしか実行されず、クライアント側で実行されるJSにはバンドルされないので。

export async function getSortedPostsData() {
  // Instead of the file system,
  // fetch post data from a database
  return databaseClient.query('SELECT posts...')
}

Development vs. Production

  • 開発環境(yarn dev): getStaticPropsはリクエスト毎に実行される。
  • 本番環境: getStaticPropsはビルド時のみ実行される。この動作はgetStaticPathsを利用して拡張可能

Only Allowed in a Page

Reactはページのレンダリング前にデータを持っている必要があるため、getStaticPropsはページからしかexportできないようになっている。

What If I Need to Fetch Data at Request Time?

リクエスト毎にデータを取得したい場合はどうするか。
プリレンダリングできない場合にStatic Generationを使うのはよくない。
この場合はサーバーサイドレンダリングするか、プリレンダリングをスキップするか。

Fetching Data at Request Time

リクエスト毎にデータを取得したい場合にサーバーサイドレンダリングを利用できる。
この場合、getStaticPropsではなく、getServerSidePropsをexportする。

getServerSidePropsはリクエスト毎に呼び出される。
リクエスト毎にサーバー側でレンダリングしなくてはならないので、TTFB(Time to first byte)がgetStaticPropsより遅くなる。さらに、設定しないとCDNによりキャッシュされない。

参考:

Client-side Rendering

データのプリレンダリングが必要ない場合、クライアントサイドレンダリングを利用することもできる。

クライアントサイドレンダリング

  1. 外部データが必要ない部分をプリレンダリングする。
  2. ページが読み込まれたらJavaScriptによって外部データを取得し、残りの部分を埋める。

この方法はプリレンダリングされないためSEOに弱くなるが、それが関係ないダッシュボード画面等ではうまく機能する。

SWR

Next.jsではデータフェッチに便利なSWRというReact hookが存在するので、これを利用するとよい。
SWRはキャッシング、フォーカストラッキング、インターバルでのリフェッチなどの機能を持っている。

5. Dynamic Routes

動的ルーティングについて。
この章では、以下のことを学ぶ。

  • getStaticPathsを利用した動的ルーティング
  • getStaticPropsを利用したブログポストのデータ取得
  • remarkを利用したマークダウンのレンダリング
  • 日付文字列の整形表示
  • 動的ルーティングによるページリンク
  • 動的ルーティングTIPS

Page Path Depends on External Data

前回の章で外部データに依存するページを扱った。
この章では、外部データに依存するページパスについて学ぶ。

Next.jsでは動的URLを利用できる。動的URLとは、ビルド時に取得した外部データに応じたURLを持つページを作成するということ。

例えば、現在、ファイル「ssg-ssr.md」と「pre-rendering.md」が存在するが、これらをそれぞれURL「/posts/ssg-ssr.md」と「/posts/pre-rendering」に対応させたい。

Implement getStaticPaths

「pages/posts/[id].js」を作成し、以下のように修正する。
尚、「...」の部分は後ほど修正する。

// pages/posts/[id].js

+import Layout from "../../components/layout";
+
+export default function Post() {
+  return <Layout>...</Layout>;
+}

「lib/posts.js」を以下のように修正する。
getAllPostIds()は「.md」ファイルから拡張子部分を取り除いたファイル名のオブジェクトを返す。

   return allPostsData.sort((a, b) => {
     return a.date < b.date ? 1 : -1;
   });
-}

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

「pages/posts/[id].js」からgetAllPostIds()を呼び出すように修正する。

 import Layout from "../../components/layout";
+import { getAllPostIds } from "../../lib/posts";

 export default function Post() {
   return <Layout>...</Layout>;
 }
+
+export async function getStaticPaths() {
+  const paths = getAllPostIds();
+  return {
+    paths,
+    fallback: false
+  };
+}

Implement getStaticProps

「lib/posts.js」を以下の通り修正し、受け取ったidに対応するファイルの内容を返す関数getPostData()を追加する。
matter関数によりMarkdown内のfrontmatterを解析し、titleやdateを取り出してmatterResult.dataに入れている。

// lib/posts.js


     }
   };
 }
+
+export function getPostData(id) {
+  const fullPath = path.join(postsDirectory, `${id}.md`);
+  const fileContents = fs.readFileSync(fullPath, 'utf8');
+  const matterResult = matter(fileContents);
+  return {
+    id,
+    ...matterResult.data
+  };
+}

ブログポスト画面に対し、取得した記事の内容をプリレンダリングする処理を追加する。
「pages/posts/[id].js」を開き、以下のように修正する。

// pages/posts/id.js

@@ -1,8 +1,16 @@
 import Layout from "../../components/layout";
-import { getAllPostIds } from "../../lib/posts";
+import { getAllPostIds, getPostData } from "../../lib/posts";

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

 export async function getStaticPaths() {
@@ -11,4 +19,13 @@ export async function getStaticPaths() {
     paths,
     fallback: false
   };
+}
+
+export async function getStaticProps({ params }) {
+  const postData = getPostData(params.id);
+  return {
+    props: {
+      postData
+    }
+  };
 }

ここまでで「ssg-ssr.md」は「/posts/ssg-ssr」に、「pre-rendering.md」は「/posts/pre-rendering」に対応するよう動的ルーティングの実装をした。
以下のURLにアスセスすることで確認できる。

Render Markdown

Markdownの構造をHTMLに変換するため「remark」と「remark-html」をインストールする。

yarn add remark remark-html

「lib/posts.js」を以下の通り修正する。

// lib/posts.js

 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(), 'posts');

@@ -32,12 +34,20 @@ export function getAllPostIds() {
   });
 }

-export function getPostData(id) {
+export async function getPostData(id) {
   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();
+
   return {
     id,
+    contentHtml,
     ...matterResult.data
   };
}

getPostData()をasyncに変更したので、「[id].js」からの呼び出し部分でawaitするよう修正する。
また、Markdownから変換したHTMLを表示するよう修正する。

       {postData.id}
       <br />
       {postData.date}
+      <br />
+      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
     </Layout>
   );
 }
@@ -22,7 +24,7 @@ export async function getStaticPaths() {
 }

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

記事画面を表示すると、Markdownの構造がHTMLに変換されて表示される。

スクリーンショット 2021-03-10 13.23.37.png

Polishing the Post Page

<title>タグを記事画面に追加する。
「pages/posts/[id].js」を以下のように修正する。

import Layout from "../../components/layout";
 import { getAllPostIds, getPostData } from "../../lib/posts";
+import Head from 'next/head';

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

を開くと、タイトルが「When to Use Static Generation v.s. Server-side Rendering」になっていることが確認できる。

次に、日付を表示するため、Dateコンポーネントを作成する。
「components/date.js」を作成し、以下のように修正する。

components/date.js

+import { parseISO, format } from 'date-fns';
+
+export default function Date({ dateString }) {
+  const date = parseISO(dateString);
+  return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
+}

記事画面に対し、Dateコンポーネントを利用して日付を表示するのに加えて、CSSを追加する。
以下の通り、「pages/posts/[id].js」を修正する。

 import Layout from "../../components/layout";
 import { getAllPostIds, getPostData } from "../../lib/posts";
 import Head from 'next/head';
+import Date from '../../components/date';
+import utilStyles from '../../styles/utils.module.css';

 export default function Post({ postData }) {
   return (
@@ -8,14 +10,14 @@ export default function Post({ postData }) {
       <Head>
         <title>{postData.title}</title>
       </Head>
-      {postData.title}
-      <br />
-      {postData.id}
-      <br />
-      {postData.date}
-      <br />
-      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
-    </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>
   );
 }

を確認すると、以下のように「January 1, 2020」と日付が表示される。

スクリーンショット 2021-03-10 17.54.01.png

Polishing the Index Page

インデックス画面にLinkコンポーネントを利用して各記事へのリンクを作成する。

「pages/index.js」を以下の通り修正する。


 import Layout, { siteTitle } from "../components/layout";
 import utilStyles from '../styles/utils.module.css';
 import { getSortedPostsData } from "../lib/posts";
+import Link from 'next/link';
+import Date from '../components/date';

 export default function Home({ allPostsData }) {
   return (
@@ -20,13 +22,15 @@ export default function Home({ allPostsData }) {
         <ul className={utilStyles.list}>
           {allPostsData.map(({id, date, title}) => (
             <li className={utilStyles.listItem} key={id}>
-              {title}
+              <Link href={`/posts/${id}`}>
+                <a>{title}</a>
+              </Link>
               <br />
-              {id}
-              <br />
-              {date}
+              <small className={utilStyles.lightText}>
+                <Date dateString={date} />
+              </small>
             </li>
-          ))}
+         ))}
         </ul>
       </section>
     </Layout>

下の画像のように、各記事へのリンクが表示され、クリックすることで記事画面に遷移することが確認できる。

スクリーンショット 2021-03-11 9.56.17.png

Dynamic Routes Details

動的ルーティングについての詳細情報

  • Fetch External API or Query Database

getStaticPropsと同様に、getStaticPathsも任意のデータソースからデータを取得することが可能。

例えば、現在の実装ではgetAllPostIdsによってファイルシステムから記事データを取得しているが、外部のAPIから取得することもできる。

  • Development v.s. Production

開発サーバーではgetStaticPathsはリクエスト毎に実行される。
本番ビルドした場合、getStaticPathsはビルド時のみ実行される。

  • Fallback

現在、以下のような実装になっているが、fallback: falseについて触れる。
fallback: falseを返すと、returnされていないパスは全て404になる。

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

fallback: trueの場合、返していないパスは404にならず、代わりに「fallback」バージョンを提供する。
これを利用すると、事前にビルドされていないURLについては以下のようなフローを実装できる。

  1. fallbackバージョンの画面(ローディング画面)を表示
  2. サーバー側でgetStaticPsoptsを実行して画面を生成
  3. 生成されたらそちらの画面に切り替える。
  4. それ以降のリクエストに対しては生成したファイルを返す。

というようなことが可能。
詳しくは、以下を参照。 

  • Catch-all Routes

ページコンポーネントのファイル名を「[...id].js」のように「...」が含まれるようにすることで、「/posts/a/b」のような階層のパスにもマッチするようになる。
その場合、getStaticPathsは以下のようにidの配列を返さなくてはならない。

return [
  {
    params: {
      // Statically Generates /posts/a/b/c
      id: ['a', 'b', 'c']
    }
  }
  //...
]

詳細は以下を参照

  • Router

useRouterフックをnext/routerからインポートすることで、Next.jsのルーターにアクセスできる。

  • 404ページ

404ページを作るには、「pages/404.js」を作成すればよい。

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

6. API Routes

Next.jsはAPI Routesをサポートしている。
API RoutesはNode.jsのサーバーレスファンクションとしてAPIのエンドポイントを作成するための仕組み。
詳しくは以下の通り。

この章では以下を学ぶ。

  • API Routeの作り方
  • API RoutesについてのTIPS

Creating API Routes

API RoutesによってAPIエンドポイントを作成するには、ディレクトリ「pages/api」以下にエンドポイントの定義を実装していく。

試しに、「pages/api/hello.js」を作成し、以下の通り実装する。

// pages/api/hello.js

+export default function handler(req, res) {
+  res.status(200).json({text: 'Hello'});
+}

この状態で

にアクセスすると、返したjsonの内容が画面に表示される。

引数としてhandler関数に渡される「req」は、http.incommingMessageのインスタンス。「res」はhttp.ServerResponseのインスタンスとなっている。

API Routes Details

API Routesについての詳細情報。

  • Do Not Fetch an API Route from getStaticProps or getStaticPaths

getStaticProps, getStaticPathsはサーバーサイドでのみ実行され、クライアントサイドで実行されることは無く、JSバンドルにも含まれない。
そのため、getStaticProps, getStaticPathsからAPI Routeをフェッチするのではなく、これらの関数の中に直接サーバーサイドのコードを書くようにする。

  • A Good Use Case: Handling Form Input

API Routesの良い使い方の例は、フォーム入力の処理。
例えば、POSTリクエストの処理などを書くことができる。
JSにバンドルされないのでクライアントから見えないようになっているため、handler関数の中には、データベースにデータを保存する処理などを書くことができる。

  • Preview Mode

Static GenerationはヘッドレスCMSからデータを取得する場合に便利だけど、プレビューしながら執筆したい時は理想的ではない。執筆中にいちいちビルドするのではなく、リクエスト時にレンダリングされてほしいはず。このようなときはStatic Generationがバイパスされてほしい。

この問題を解決するため、Next.jsにはプレビューモードが存在している。
プレビューモードはAPI Routesによって提供される。
詳しくは以下参照。

  • Dynamic API Routes

API Routesは動的なルーティング「Dynamic Routing」も可能。
例:
pages/post/[id]/index.js
e.g. matches /post/my-example (/post/:id)

pages/post/[id]/[comment].js
e.g. matches /post/my-example/a-comment (/post/:id/:comment)

ドキュメントはこちら

7. Deploying Your Next.js App

最後の章では本番環境にデプロイする。
Next.jsの開発者が開発したJamstackのデプロイメントプラットフォーム「Vercel」にデプロイする方法を学ぶ。

この章では以下の内容を学ぶ。

  • Next.jsアプリケーションをVercelにデプロイする方法
  • DSPワークフローについて。D(Develop), P(Preview), S(Ship)
  • Next.jsアプリケーションを自分のホスティングプロバイダにデプロイする方法

Push to GitHub

デプロイする前にGithubにデプロイしておく。
これでデプロイが楽になる。

(Githubへのデプロイ方法はこの記事では割愛)

Deploy to Vercel

Next.jsのデプロイで最も簡単なのはVercelを使うこと。

VercelはグローバルなCDNやサーバーレスファンクションを備えたオールインワンプラットフォームで、無料で使い始められる。

以下からアカウントを作成する。

アカウントを作成したら、pushしたリポジトリをインポートする。
「import Git Repository」からリポジトリへのアクセス権を付与する。
リポジトリを選択すると、プロジェクト名などの入力フォームが表示されるが、初期値でよい。「Deploy」ボタンを押す。

「Visit」ボタンを押すと、デプロイされたことが確認できる。

Next.js and Vercel

VercelはNext.jsの開発者により作られていて、Next.jsに対してファーストクラスのサポートがされている。
Next.jsアプリケーションをVercelにデプロイすると、以下のことが自動で実行される。

  • JS, CSS, 画像, フォントなどの静的アセットがVercelの高速なエッジネットワークから配信される。
  • サーバーサイドレンダリング、APIルーティングを使用するページはそれぞれ独立したサーバーレスファンクションで実行される。これにより、APIリクエストが無限にスケールされる。
  • Vercel上で環境変数を設定することにより、カスタムドメインを割り当てることが可能。
  • 自動でHTTPS対応され、SSL証明書は自動更新される。

Preview Deployment for Every Push

別のブランチを作成する。

git checkout -b develop

一部修正してコミットする。
例 (pages/index.js)

// pages/index.js

         <title>{siteTitle}</title>
       </Head>
       <section className={utilStyles.headingMd}>
-        <p>Nullam hendrerit faucibus arcu nec viverra. Mauris scelerisque arcu eget neque tincidunt, id vestibulum odio convallis. </p>
+        <p>Development branch test</p>
         <p>

修正をpushする。

git push origin develop

github上でプルリクエストを作成すると、
vercel botがプルリクエストにコメントを書き込む。

スクリーンショット 2021-03-11 23.35.24.png

その中に修正版へのプレビューのURLがあるので、クリックするとプレビュー版を確認することができる。

スクリーンショット 2021-03-11 23.38.06.png

プレビューを確認した上で問題なければメインブランチにマージする。
マージされると、Vercelは自動的に本番デプロイされた環境を作成する。

これが、DPS(Develop, Preview, Ship)と呼ばれるワークフローとなっている。

  • D(Develop): Next.jsの開発サーバーでホットリロードを利用しながらコードを書く。
  • P(Preview): Githubにpushし、プルリクエストを作成すると、プレビュー用の環境が作成されて公開される。URLを共有することで複数人で確認できる。
  • S(Ship): プルリクエストをマージすると、本番用の環境が作成される。

このワークフローにより、素早く反復作業を行うことが可能。

Other Hosting Options

もし、Vercelではなく個人のホスティングプロバイダを利用する場合、自分で本番用にビルドする必要がある。

yarn build

ビルドが完了すると、「.next」ディレクトリ以下に結果が生成される。
ビルド完了後、「yarn start」により、Node.jsのサーバーでホスティングを開始する。

yarn start

Finally

ここまでの成果をTwitterでシェアするとよい。
TypeScriptを利用する場合のチュートリアルは以下。

以上。

45
32
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?