# 2.Next.jsを使ってJamstackなアプリを作る

# 概要

  • この章ではビルド時にAPIからデータを取得してページを生成する、というJamstackの特徴を持ったアプリを作成していきます
    • Next.jsというWebページを作成するためのフレームワークを使います
    • この章ではAPIは自前で作成せずにQiitaのAPIを使ってデータを取得し、Qiitaの最新記事一覧を表示してみます
    • Next.jsを使うとビルド時にAPIからデータを取得してHTMLを生成することができるため、Jamstackの構成を実現することができます

# ゴール

  • Next.jsを使ってWebアプリを作成できていること
  • Next.jsの機能を使ってビルド時にAPIからデータを取得できていること
  • 作成したWebアプリをnowにデプロイし公開できていること

完成形

# 2-1.Next.jsの雛形を作成

  • Next.jsを使ってアプリを作成していきます
  • Next.jsの雛形生成ライブラリであるcreate-next-appと、コマンドラインツールであるyarnをインストールします
npm i -g create-next-app yarn
1
  • create-next-appを使ってアプリを作成します
    • 途中で選択肢が出たらDefault starter appを選択します
create-next-app jamstack-sample
1
  • jamstack-sampleというフォルダが作成されてその中にアプリの雛形が生成されました

スターター

  • ログに表示される案内に従って起動できることを確認しましょう

create-next-app log

cd jamstack-sample
yarn dev
1
2

TIP

止めたいときは Ctl + c で停止できます

create-next-app

TIP

今後出てくるコマンドの実行は特別な案内がない限りjamstack-sampleディレクトリ内(=yarn devを実行した場所)で実行してください

  • 動きが確認できたらVSCodeでjamstack-sampleプロジェクトを開いておきましょう

# 2-2.記事一覧ページの作成

  • Qiitaの新着記事一覧を表示するページを作成していきます

# 埋め込みデータで記事一覧ページの作成

  • まずは新しいページを作成してHelloだけ表示させてみましょう
  • pages/items/index.jsというファイルを作成して以下の内容を記述してください

TIP

pages配下のディレクトリ構成がそのままURL構造に適用されます。pages/items/index.jshttp://localhost:3000/items/indexにマッピングされます。一般的に/indexは省略するのでhttp://localhost:3000/itemsにマッピングされることになります。

function Items() {
  return (
    <div>
      <h1>Hello</h1>
    </div>
  );
}

export default Items;
1
2
3
4
5
6
7
8
9

hello

  • 表示が確認できたらダミーの記事一覧を表示するように修正してみます
 
 
 
 
 
 





 
 
 
 
 






// ダミーの記事一覧を格納した配列を定義
const items = [
  { id: 1, title: '記事のタイトル1' },
  { id: 2, title: '記事のタイトル2' },
  { id: 3, title: '記事のタイトル3' },
];

function Items() {
  return (
    <div>
      <h1>Hello</h1>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default Items;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

TIP

.mapを使うと配列の要素を順番にループ処理できます。上のコードでは記事の数だけliタグを生成しています

list

# QiitaのAPIから取得したデータを表示する

  • QiitaのAPIをたたくので、通信処理を実行するためのライブラリをインストールします
yarn add node-fetch
1
  • pages/items/index.jsを修正してQiitaのAPIからデータを取得するようにします
 
 

 
 












 
 
 
 
 
 
 
 
 
 
 
 
 
 
 



// 通信ライブラリであるnode-fetchをimport
import fetch from 'node-fetch';

// getStaticPropsから渡されるitemsという変数を受け取る
function Items({ items }) {
  return (
    <div>
      <h1>Hello</h1>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

// getStaticPropsという名前の関数はビルド時にフレームワークが実行してくれる
export async function getStaticProps() {
  // QiitaのAPIをコール
  const res = await fetch('https://qiita.com/api/v2/items', {
    // アクセストークンをセット
    headers: {
      Authorization: 'Bearer a8f7b4026700cd36eb8e3a75525d767d0115aabe',
    },
  });
  const data = await res.json();
  // APIから取得したデータを必要な項目(idとtitle)だけに絞り込む
  const items = data.map(item => ({ id: item.id, title: item.title }));
  // 取得したデータをpropsとしてreturnするとItems関数の引数に渡すことができる
  return { props: { items } };
}

export default Items;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

TIP

QiitaのAPIはアクセス回数に制限があるためトークンを発行してセットしておいてください。Qiitaのアカウントがなかったりトークンの発行手順がわからない場合は上記サンプルに埋め込まれているトークンを使ってください。マイページの「アプリケーション」からトークンを発行できます。

TIP

Next.jsの機能としてgetStaticPropsという名前で関数を定義するとビルド時に処理が実行され結果をコンポーネントに渡すことができます。ここでAPIをたたく処理を行うことでビルド時にデータを取得しコンポーネントに渡しています。(コンポーネントとはHTMLをreturnしているfunctionのこと)

qiita items

# 2-3.記事詳細ページの作成

  • 一覧画面で記事を選択すると詳細ページが表示されるようにしてみましょう
  • 詳細ページのURLは/items/記事のIDとなるようにします
    • ex. http://localhost:3000/items/4075d03278d1fb51cc37

# 記事一覧に記事詳細へのリンクを追加する

  • 記事詳細ページはこの後作りますが、先に一覧から詳細へ遷移できるように修正しておきます
  • pages/items/index.jsLinkを追加します
 
 









 
 
 
 





















// ページ遷移をするためのLinkコンポーネントをimport
import Link from 'next/link';
import fetch from 'node-fetch';

function Items({ items }) {
  return (
    <div>
      <h1>Hello</h1>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {/* Linkを追加 */}
            <Link href="/items/[id]" as={`/items/${item.id}`}>
              <a>{item.title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

export async function getStaticProps() {
  const res = await fetch('https://qiita.com/api/v2/items', {
    // アクセストークンをセット
    headers: {
      Authorization: 'Bearer a8f7b4026700cd36eb8e3a75525d767d0115aabe',
    },
  });
  const data = await res.json();
  const items = data.map(item => ({ id: item.id, title: item.title }));
  return { props: { items } };
}

export default Items;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  • 遷移先はエラーになりますが一覧がリンク化されました

qiita items

# 詳細ページを作成する

  • 一覧画面は/itemsにマッピングさせたいのでpages/items/index.jsというファイルを作りましたが、今回作りたいページは/items/記事のIDなのでURLが動的に変動します
  • そういう場合は[id].jsといったファイル名で作成することで対応できます
  • pages/items/[id].jsを作成し以下の内容を記述してください
import fetch from 'node-fetch';

// getStaticPropsからitemを受け取る
function Item({ item }) {
  return (
    <div>
      <h1>{item.title}</h1>
      <hr />
      <div dangerouslySetInnerHTML={{ __html: item.body }}></div>
    </div>
  );
}

// ビルド時に実行される関数で、returnした値をコンポーネントに渡すことができる
export async function getStaticProps({ params }) {
  // QiitaのAPIから記事の詳細情報を取得
  const res = await fetch(`https://qiita.com/api/v2/items/${params.id}`, {
    // アクセストークンをセット
    headers: {
      Authorization: 'Bearer a8f7b4026700cd36eb8e3a75525d767d0115aabe',
    },
  });
  const data = await res.json();
  // レスポンスから必要な項目だけを抽出
  const item = { id: data.id, title: data.title, body: data.rendered_body };
  // 抽出した値をreturn(コンポーネントに引数として渡される)
  return { props: { item } };
}

// ビルド時に実行される関数で、[id].jsのidに具体的にどんな値が入るのかをリストでreturnする
export async function getStaticPaths() {
  // QiitaのAPIから記事一覧の情報を取得
  const res = await fetch('https://qiita.com/api/v2/items', {
    // アクセストークンをセット
    headers: {
      Authorization: 'Bearer a8f7b4026700cd36eb8e3a75525d767d0115aabe',
    },
  });
  const data = await res.json();
  // レスポンスを元に詳細ページのURLのリストを作成
  const paths = data.map(item => `/items/${item.id}`);
  return { paths, fallback: false };
}

export default Item;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

TIP

Jamstackはビルド時に各ページのHTMLを生成するため、今回の記事詳細ページのような場合でも全てのページのHTMLを作成しておく必要があります。どのようなURLのページがあるかはAPIを叩いてみないとわからないので動的に指定する必要がありますが、getStaticPathsを使うとそれを実現することができます。

  • ここまでできたら一覧画面から詳細画面へ遷移してみましょう
  • 以下のように記事の本文が表示されているはずです

qiita item

# ビルドしてみる

  • これまではyarn devコマンドで開発モードで起動していました
  • 本番用にビルドして成果物を確認してみます
yarn build
1
  • このようなログがでます

build

  • build後の成果物は.nextディレクトリに作成されます
  • .nextの内部はいろいろなファイルがあって複雑ですが、一覧画面と各詳細画面のhtmlファイルが生成されていることを確認できます

tree

  • 以下のコマンドでビルドしたアプリを起動することができます
yarn start -p 3001
1

TIP

デフォルトではyarn devと同じ3000番ポートで起動するのでオプションをつけて3001番で起動しています。http://localhost:3001/itemsにアクセスしてみてください。

  • yarn devのときは都度ビルドが走るため最新記事が都度反映されていました
  • yarn startの場合はビルド済みのアプリを起動するだけなので、再ビルドするまで記事一覧の内容は更新されません
    • APIをたたくのがビルド時のみで、実行時はAPIをたたかないのがJamstackの特徴でしたね

# リファクタリング

  • 通信処理が何度も出てくるので専用のファイルに切り出しておきましょう
  • api/qiitaApi.jsを作成して以下の内容を記述してください
    • pagesディレクトリと横並びになる位置にapiディレクトリを作成してください

apiフォルダ

import fetch from 'node-fetch';

const baseUrl = 'https://qiita.com/api/v2';

const headers = {
  Authorization: 'Bearer a8f7b4026700cd36eb8e3a75525d767d0115aabe',
};

// 記事一覧を取得する関数
export async function getItems() {
  const res = await fetch(`${baseUrl}/items`, { headers });
  return res.json();
}

// 記事詳細を取得する関数
export async function getItem({ id }) {
  const res = await fetch(`${baseUrl}/items/${id}`, { headers });
  return res.json();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 作成したqiitaApiの内容を適用していきます
  • pages/items/index.jsを修正します

 
 



















 






import Link from 'next/link';
// qiitaApiから一覧を取得するgetItemsをimport
import { getItems } from '../../api/qiitaApi';

function Items({ items }) {
  return (
    <div>
      <h1>Hello</h1>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            <Link href="/items/[id]" as={`/items/${item.id}`}>
              <a>{item.title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

export async function getStaticProps() {
  const data = await getItems();
  const items = data.map(item => ({ id: item.id, title: item.title }));
  return { props: { items } };
}

export default Items;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • pages/items/[id].jsを修正します
 
 












 





 






// qiitaApiから一覧を取得するgetItemsと詳細を取得するgetItemをimport
import { getItems, getItem } from '../../api/qiitaApi';

function Item({ item }) {
  return (
    <div>
      <h1>{item.title}</h1>
      <hr />
      <div dangerouslySetInnerHTML={{ __html: item.body }}></div>
    </div>
  );
}

export async function getStaticProps({ params }) {
  const data = await getItem({ id: params.id });
  const item = { id: data.id, title: data.title, body: data.rendered_body };
  return { props: { item } };
}

export async function getStaticPaths() {
  const data = await getItems();
  const paths = data.map(item => `/items/${item.id}`);
  return { paths, fallback: false };
}

export default Item;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  • yarn devで起動して変わらずに動くことを確認しておいてください

# 2-4.アプリをnowにデプロイする

  • これまでは端末のローカル環境で起動していたのでその端末からしかアクセスできませんでした
  • ホスティングサービスにアプリをアップロードしてどこからでもアクセスできるように公開してみましょう
  • 今回はnowというホスティングサービスを使います

# アカウントの作成

  • まずはGitHubのアカウントを作成します
  • 以下のページからアカウントを作成してください
  • 次にnowのアカウントを作成します
  • GitHubアカウントを使ってnowのアカウントを作成できます

# コマンドラインツールのセットアップ

  • nowのコマンドラインツールをインストールします
npm i -g now@latest
1
  • インストールできたらコマンドラインでnowにログインします
    • メールアドレスの入力を求められるのでGitHub登録時のメールアドレスを入力してください
    • メールが飛ぶのでVerifyを押すとログインできます
now login
1
  • 以下のようなメールが届くのでVerifyを押します

verify

# デプロイする

  • 以下のコマンドでデプロイします
    • いろいろ聞かれるので全てデフォルトのままエンターでOK
    • デプロイが実行されるので少し時間がかかります
now
1
  • デプロイが完了するとURLが表示されます

now deploy

  • アクセスするとローカルで動かしていたのと同じアプリが表示されます!

now

# 2-5.レイアウトを整える

  • Jamstackとは直接関係ありませんがせっかくなのでレイアウトを整えておきます
  • 今回はReactBootstrapというライブラリを使います

# ReactBootstrapのセットアップ

  • 必要なライブラリをインストールします
yarn add react-bootstrap bootstrap
1
  • 設定ファイルを作成します
  • pages/_app.jsを作成して以下の内容を記述してください
import 'bootstrap/dist/css/bootstrap.css'
import App from 'next/app'

export default App
1
2
3
4

# ReactBootstrapのコンポーネントを適用する

  • まずは一覧画面から適用していきます
  • pages/items/index.jsを修正してください

 
 




 

 

 
 
 

 
 











import Link from 'next/link';
// react-bootstrapからコンポーネントをimport
import { Container, ListGroup } from 'react-bootstrap';
import { getItems } from '../../api/qiitaApi';

function Items({ items }) {
  return (
    <Container>
      <h1>Hello</h1>
      <ListGroup>
        {items.map(item => (
          <Link key={item.id} href="/items/[id]" as={`/items/${item.id}`}>
            <ListGroup.Item action>{item.title}</ListGroup.Item>
          </Link>
        ))}
      </ListGroup>
    </Container>
  );
}

export async function getStaticProps() {
  const data = await getItems();
  const items = data.map(item => ({ id: item.id, title: item.title }));
  return { props: { items } };
}

export default Items;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • 見た目の雰囲気が変わりました

items bootstrap

  • 記事詳細画面も修正します
  • pages/items/[id].jsを修正してください
 
 




 



 

















// react-bootstrapからコンポーネントをimport
import { Container } from 'react-bootstrap';
import { getItem, getItems } from '../../api/qiitaApi';

function Item({ item }) {
  return (
    <Container>
      <h1>{item.title}</h1>
      <hr />
      <div dangerouslySetInnerHTML={{ __html: item.body }}></div>
    </Container>
  );
}

export async function getStaticProps({ params }) {
  const data = await getItem({ id: params.id });
  const item = { id: data.id, title: data.title, body: data.rendered_body };
  return { props: { item } };
}

export async function getStaticPaths() {
  const data = await getItems();
  const paths = data.map(item => `/items/${item.id}`);
  return { paths, fallback: false };
}

export default Item;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • 詳細画面も見た目の雰囲気が変わりました

item bootstrap

  • 修正版をデプロイします
    • 二度目以降のデプロイはデフォルトだとdev環境へのデプロイになるので--prodをつけます
now --prod
1
  • デプロイが完了すると修正版が公開されているはずです

# まとめ

  • create-next-appによるNext.jsを使ったアプリの作成のしかたを学びました
  • getStaticPropsなどNext.jsの機能を使うことでビルド時にAPIからデータを取得しJamstackなアプリにすることができました
  • nowにデプロイすることで作成したアプリを世の中に公開することができました
Last Updated: 2020/04/29