2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiitanがほしい人の一人アドカレAdvent Calendar 2024

Day 9

Next.jsとmicroCMSでStaticなブログを作ろうとしたけど考え直した話

Last updated at Posted at 2024-12-09

読み飛ばしてください

ひとりアドカレ9日目の今回はNext.jsmicroCMSでモダンなブログを作ろうとして既に半年以上が経過している話を綴っていこうかと思います。

Next.jsでStaticなブログを作ろうとした話

まず使用しているものは以下のような感じです。

  • フロントエンド
    • Next.js
  • CSSフレームワーク
    • Tailwind CSS
    • NextUI
  • コンテンツ管理
    • MicroCMS
    • Cloudflare R2
    • Imgix
  • デプロイ
    • Cloudflare Pages
  • 開発環境
    • DevContainer

かなりモダンな技術で揃えているとは思います。
これらの技術を利用しようとしたのは金銭的にクソ雑魚ナメクジといったところです。

ほとんどゼロ円です。タダより高いものはないといいますが、出ないものを出すよりタダで済ませる派です。

今回はこの中でNext.jsが中心の話です。

Staticなサイトとは?

Next.jsでSSG(Static Site Generation)を行うということです。
詳しくは公式で説明してありますが、予めHTML形式のファイルを生成して返却するのが一番早いよねってことです。

また、SSGだけで考えるのであればAstroも高速で良いとの話です。
新しくSSGなサイトを作るのであればこちらも検討すると良いと思います。

他にもNext.jsではいくつかのレンダリング方法をサポートしていますが、それらに関しては以下の記事がわかりやすいかなと思います。

これを使うことで、CloudflarePages側では単純にHTML形式のファイルを返却するだけとなり、爆速になります。

SSGを行う場合はnext.config.jsにSSGの設定を書くだけなので、設定自体は簡単ですね。

next.config.js
/** @type {import("next").NextConfig} */
const nextConfig = {
    output: "export", // これがSSGの設定
    images: {
        unoptimized: true,
    },
}

module.exports = nextConfig

Next.jsの仕様と求めてるものの差異

SSGを行うために、microCMSに対して何度もクエリを送るのはなんとなーく申し訳ないし、そもそも1度で全部取って来てしまえばあとはデータ加工するだけで全てのブログ記事を描画しきれるではないか?と思いました。

ですが、Next.jsでコレをやるのはオススメできません

なぜなら、Next.jsのSSGを行う際に各ページを生成する際にそれぞれが独立した非同期プロセスのような状態で実行されるためです。

そのため、グローバルなスコープで予めデータを保持するような関数を定義したとしても、非同期プロセスによってそれらを再度呼び出す事になります。

ある程度はキャッシュでどうにかしてくれる

まず、前提としてSGGをする際に呼び出したfetch関数はそれぞれ引数の引数に対してのキャッシュを取ってくれるそうです。

そのため、以下のような単純なコードであればキャッシュを取得してくれるので、あまり気を使いすぎる必要はありません。

export const fetchTest = async () => {
    // ここのキャッシュは勝手にとってくれる
    await fetch('https://example.com/todos/1')
}

これはmicrocms-js-sdkを利用しても同様で、内部で利用されているfetch関数のキャッシュを取ってくれるので、何度も全件取得するような事態は以下のようなコードでもある程度避けられます。

import { createClient } from "microcms-js-sdk"
import { Blog } from "@/types/microcms"

// 設定を食わせてよしなにする
export const cmsClient = createClient({
    serviceDomain: process.env.NEXT_PUBLIC_MICRO_CMS_SERVICE_DOMAIN || "",
    apiKey: process.env.NEXT_PUBLIC_MICRO_CMS_API_KEY || ""
})

// すべてのブログ記事を取得する
export const fetchAllPosts = async () => {
    try {
        const response: Post[] = await cmsClient.getAllContents({
            endpoint: "post",
        })
        return response
    } catch (error) {
        return []
    }
}

ブログ記事のページを生成する

Node.jsではAppDirectoryというapp配下のディレクトリをそのままサイトのディレクトリ構造のように振る舞ってくれるという仕様があります。

また、[]で囲まれたディレクトリは特別な扱いで、generateStaticParams()関数を定義してあげることで、列挙されたページ情報を渡すことでmicroCMS上にあるデータから動的にページをレンダリングすることが出来ます。

app/[post]/page.tsx
export async function generateStaticParams() {
    // 全ブログを取得
    const posts = await fetchAllPosts()

    // ID一覧に変換
    return posts
        .map(post => {id: post.id})
}

先ほどもいった通りfetchAllPostsを呼んで全データ取得したとしても、fetchは一度しか呼ばれないはずです。
そのため、ここで全件呼び出しをしてもそこまで大きなオーバーヘッドはないと思われます。

app/[post]/page.tsx
export default async function Page({
    params,
}: {
    params: { id: string } // さっきの blog.id を受け取る
}) {
    const { id } = params
    // 記事IDから記事を取得する
    const post = await fetchPostById(id)
    if (!post) {
        return <div>Not Found</div>
    }

    return (
        <div>
            <h1>{post.Title}</h1>
            <div dangerouslySetInnerHTML={post.Content} ></div>
        </div>
    )
}

ここで、fetchPostByIdの実装です。

import { unstable_cache } from "next/cache"

export const fetchPostById = unstable_cache(async (id: string) => {
    const posts = await fetchAllPosts()
    return posts.filter(post => post.id === id)[0]
}

うん、良くないね!

unstable_cacheはキャッシュを保持するラッパーです。
つまりpostsはキャッシュされた値を返すし、fetchPostByIdもキャッシュされた値を返します。
そのため、ある程度はとんでもなく酷いパフォーマンスになるわけではないのですが、このunstable_cacheでキャッシュされるまでに並走しているタスクで同時にデータを取得していることも多々あります。

そのため、タイミングによってはキャッシュが生かされない事もしばしばあります。

postsをMAPとして保持してそれをKeyで取得するという方法もありますが、そもそもキャッシュは文字列としてファイルが生成されるようなので、読み書きのオーバーヘッドを考えるとあまり良い方法ではないでしょう。

そもそもgenerateStaticParamsはオブジェクトごと渡せないの?

私が調べた感じだと、それに該当するものはなかったです。

なので、全件取得というアプローチ自体がやっぱり対応してなさそう。といったところです。
もし、オブジェクトとして渡せるのであれば、いい感じにページを生成できて良さそうではあります。

export async function generateStaticParams() {
    // 全ブログを取得
    const posts = await fetchAllPosts()

    // こんなふうにオブジェクトごと渡せれば楽
    return posts
        .map(post => {id: post.id, post: post})
}

export default async function Page({
    params,
}: {
    params: { id: string, post: post }
}) {
    // 直接受け取ったオブジェクトからよしなにページ組み立て
    const { id, post } = params
    
    if (!post) {
        return <div>Not Found</div>
    }

    return (
        <div>
            <h1>{post.Title}</h1>
            <div dangerouslySetInnerHTML={post.Content} ></div>
        </div>
    )
}

この辺の仕様が私的には「ぐぬぬ」となってモチベーションが下がって放置してしまいました。

そもそもSSGする必要あるのか?

ブログの記事を列挙させるだけであれば良いのですが、ブログ記事一覧のページや、タグごとに表示するページなども一緒にSSGするとなると、組み合わせ量が結構な量になります。
その記事自体に関連記事や、次の記事、前の記事をリンクを貼るなどのレイアウトもあったほうが嬉しいです。

これをmicroCMSに記事が投稿されるたびにCloudflarePagesでビルドし直すと考えると、そもそもSSG自体が向いていないように思えます。

また、SSGはアクセス数が多いサイトでは効果的に働くと思いますが、個人ブログのアクセス数であれば必要ないと思われます。

まとめ

新しい技術を触るのは楽しいですが、現実的にはほどほどな感じがちょうど良さそうですね。

このブログはいずれSSRで作り直そうと思っていますが、それはいつになるやら。

それでは。

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?