LoginSignup
29
25

Next.jsに入門した

Last updated at Posted at 2022-11-16

以下のチュートリアルをやっていきます
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の数だけやって、というのをやらずにタグをそのまま書くことが出来ます

チュートリアルページに分かりやすい図画ありましたので貼っておきます

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

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ファイルをブラウザで開くと以下のように表示される筈です

2-2. 関数で書く

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

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 💙"を引数として渡しています

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>

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

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

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

テンプレートリテラル
function Header({ title }) {
    return <h1>{`Cool ${title}`}</h1>;
}
関数を呼ぶ
function createTitle(title) {
    if (title) {
        return title;
    } else {
        return 'Default title';
    }
}
  
function Header({ title }) {
    return <h1>{createTitle(title)}</h1>;
}

条件演算子も使えます

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

2-4. 繰り返し処理

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

見出しの列挙
<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を追加できます

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してくれるようです

2-6. useStateを使う

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

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

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
> 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
npm run dev

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

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

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

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

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タグについては何も書いてないんですが適宜やってくれるようです

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

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

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

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

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

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

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のところにトップページへのリンクが付いています

3-5. 画像を表示する

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

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を書いています

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に書くことも出来ますので遅延読み込みへの対応もやりやすいようです

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の適用もスマートにやってくれます

components/layout.module.css
.container {
    max-width: 36rem;
    padding: 0 1rem;
    margin: 3rem auto 6rem;
}
components/layout.js
import styles from './layout.module.css';

export default function Layout({ children }) {
    return <div className={styles.container}>{children}</div>;
}
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を読みこめば、すべてのページに共通して適用できます

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

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}
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;
}

リンクに色が付いてマウスホバーで下線が出るようになりました

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/ に以下が表示される筈です

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

terminal
my-app> npm install gray-matter

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

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.
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の操作をやってくれるものです

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()から呼んでないのにこれで読み込みできるのが何故なのかよく分からなかったので、後日分かったら追記したいです

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/ に以下が表示される筈です

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

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

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

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

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

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

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の場合は以下のようになります

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フックを作成していて使用を推奨しているようです

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()で書き出すページの内容を渡すと当該ページを生成してくれるようです

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
    }
  }
}
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 を作れば良いようです

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

7. APIルート

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

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

http://localhost:3000/api/hello

8. デプロイ

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

8-1-1. githubにpushする

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

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したリポジトリを選択と進みます

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

上手くいったようです

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

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

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

terminal
npm run build

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

terminal
npm run start

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

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

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

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

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

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

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

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

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

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

9-4. APIをつくる

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

できました

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

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

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

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

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

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

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

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

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

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プレビューから確認できます

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

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

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

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

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>
    );
};

できました

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

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

terminal
npm install 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;
  }
}
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>
    );
};

9-8. ビルドする

terminal
npm run build

9-9. githubにpushする

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

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

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

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

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

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

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

左側にトップページが表示されていますが、ここをクリックすると現時点でデプロイされている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

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

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

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

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

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

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

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

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

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

29
25
0

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
29
25