16
13

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 3 years have passed since last update.

Next.jsによるSSG最低限

Posted at

SSG(Server Side Generation)を利用する機会がありそうなのでメモ。

Next.jsには9.3からSSGが導入されました。

やりたいこと

  • TOPページ(index.js)に記事一覧
  • [id].jsに個別ページ
  • index.jsと[id].jsをSSGする
  • 試しにcsr.jsも作成して、CSR(Client Side Rendering)の様子も見る

[id].jsとか[name].jsはNextでの動的ルーティングのページになります。

簡易APIの作成(Blog記事APIを想定)

blog記事をSSGすることを想定し、簡単なMock APIを作成してみます。
ここでは静的なJSONを返していますが、実際はDBやHeadlessCMS等から返すことになります。

作業場の作成と必要ファイルの生成

mkdir mock-blog-api
cd mock-blog-api

npm install express cors
touch index.js

実装

index.jsを実装していきます。
ポイントは、

  • クライアントとオリジンが異なるので簡易CORS対応をしている
  • ポート3001で起動している(Next側が標準で3000なのでぶつからないようにする)

といったところです。

index.js
const express = require("express");
const app = express();

//CORS対応(許可)
const cors = require("cors");
app.use(cors());

const data = {
    status: "OK",
    contents: [
        { id: 1, title: "記事1", body: "記事1の内容。" },
        { id: 2, title: "記事2", body: "記事2の内容。" },
        { id: 3, title: "記事3", body: "記事3の内容。" }
    ]
};

//記事一覧を返すAPI
app.get("/blog", (req, res) => {
    res.json(data);
});

//個別記事を返すAPI
app.get("/blog/:id", (req, res) => {
    const id = req.params.id;
    res.json(data.contents.find(d => d.id == id));
});

//サーバリッスン開始
app.listen(3001, () => {
    console.log("Server start on port 3001.");
});

実行

実行しておきます(Nextの実装後でもいいです)。

node index.js

Next.jsの実装

API側ができたので本丸のNext側の実装です。

プロジェクトの生成

nodeは入ってる前提でnpxでcraete-next-appを呼び出します。

npx create-next-app next-ssg-test
cd next-ssg-tset

必要ファイルの生成

pages以下にblogディレクトリを生成し、その中に[id].jsを生成。

mkdir pages/blog
touch pages/blog/[id].js

いちおうcsrのコードを確認するためcsr.jsも作成し実装してみます。

touch pages/csr.js

index.js(記事一覧)

getStaticPropsがサイト実行前にサーバで実行され内容を反映したページが生成されます。

pages/index.js
import Link from "next/link";

const Home = ({ blog }) => {
  return (
    <div>
      <h1>記事一覧(SSG)</h1>
      <ul>
        {blog.map((blog) => (
          <li key={blog.id}>
            <Link href={`/blog/${blog.id}`}>
              <a>{blog.title}</a>
            </Link>
          </li>
        ))}
      </ul>
      <p><Link href="/csr"><a>CSRページへ</a></Link></p>
    </div>
  );
}

//SSG処理
export const getStaticProps = async () => {
  const res = await fetch("http://localhost:3001/blog");
  const json = await res.json();
  return {
    props: {
      blog: json.contents,
    }
  }
}


export default Home;

[id].js(個別ページ)

getStaticPathsで生成するページ(path)のパターンを定義。
getStaticPropsでそのパターンでの呼び出しを定義します。

pages/blog/[id].js
import Link from "next/link";

const BlogId = ({ blog }) => {
    return (
        <div>
            <h1>{blog.title}</h1>
            <p>{blog.body}</p>
            <br />
            <p><Link href="/"><a>戻る</a></Link></p>
        </div>
    );
}

export default BlogId;

//どんなpathがあるか
export const getStaticPaths = async () => {
    const res = await fetch("http://localhost:3001/blog");
    const json = await res.json();

    const paths = json.contents.map((content) => `/blog/${content.id}`);
    return { paths, fallback: false };
}

//各ページのレンダリング
export const getStaticProps = async (context) => {
    const id = context.params.id;
    const res = await fetch(`http://localhost:3001/blog/${id}`)
    const json = await res.json();
    return {
        props: {
            blog: json
        }
    }
}

csr.js(普通の?React的CSR)

useStateとuseEffectを利用した一般的なAPI連携。

pages/csr.js
import Link from "next/link";
import { useState, useEffect } from "react";

const CsrPage = () => {

    //contentの入れ物
    const [contents, setContents] = useState([]);

    //ページ読み込み時に取得(CSR)
    useEffect(() => {
        const getContents = async () => {
            const res = await fetch("http://localhost:3001/blog");
            const json = await res.json();
            setContents(json.contents);
        }
        getContents();
    }, []);

    return (
        <div>
            <h1>記事一覧(CSR)</h1>
            <ul>
                {contents.map((content) => (
                    <li key={content.id}>
                        <Link href={`/blog/${content.id}`}>
                            <a>{content.title}</a>
                        </Link>
                    </li>
                ))}
            </ul>
            <Link href="/"><a>戻る</a></Link>
        </div>
    );
}

export default CsrPage;

実行

### 開発モード(開発中)

以下のようにします。

npm run dev

本番モード(SSG処理)

run buildでビルドし、生成ぶつを実行するにはrun startとします。

buildの結果物は.next/server/pages以下に生成されるようです。

npm run build
npm run start

その他

これらをgithub軽油でvercelやgithub pagesなどにデプロイします。

おまけ

ついでにuseSWRを利用してデータを取得してみます。
SWRはRenderingの種類ではない。Stale-While-Revalidateというキャッシュ戦略の略。

touch pages/swr.js

実装。シンプルに書け、かついろいろ便利な機能があるが、ここでは以上とします。

pages/swr.js
import useSWR from "swr";
import Link from "next/link";

//fetcherの定義
const url = "http://localhost:3001/blog";
const fetcher = () => fetch(url).then(r => r.json());

const SwrPage = () => {

    //swr
    const { data, error } = useSWR(url, fetcher);

    //エラーやローディング中の処理
    if (error) return <div>fail to load.</div>
    if (!data) return <div>Loading...</div>

    return (
        <div>
            <h1>記事一覧SWR利用</h1>
            <ul>
                {data.contents.map((content) => (
                    <li key={content.id}>
                        <Link href={`/blog/${content.id}`}>
                            <a>{content.title}</a>
                        </Link>
                    </li>
                ))}
            </ul>
        </div>
    );
}

export default SwrPage;

参考

16
13
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
16
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?