Next.js 14 + microCMSでJamstackなオリジナルブログ作ってみた
1ヶ月ほど前から学習を始めたReact, Next.jsのアウトプットとして、オリジナルブログを作成したので紹介したいと思います。
- サイトURL:
 
- リポジトリ:
 
実装した機能
- 自分のZennの最新記事10件表示
 - microCMSを使ったオリジナルブログ
- 記事に付与されたカテゴリーによる検索機能あり
 
 - 記事に付与されたカテゴリーによる検索機能あり
 - ページネーション
 - 動的OG画像
 - 全ページレスポンシブ対応
 - ライト・ダークモード
 - ページ遷移時のアニメーション
 
バックエンドにmicroCMSを使うことでお手軽にオリジナルブログを構築しました(Jamstack構成)。Next.js + microCMSの記事は他にもいくつかあったのですが、Pages Routerで書かれたものが多かったので少し苦労しました。
技術スタック
利用した技術とその簡単な理由は以下のとおりです。
Next.js 14.1.4 (App Router)
制作時の最新バージョン
TailwindCSS
過去に利用経験があり慣れていたため採用。クラス名をいちいち考えたり別途でCSSファイルを作らなくてよくて便利。
TypeScript
今までバニラのJavaScriptを使うことが多かったのですが、練習のために採用。まだあまり使いこなせている感じはしないものの、特にAPIからのデータの取得の時に恩恵を感じました。
microCMS
国産CMSで安心感があったため採用。ヘッドレスCMSの採用のおかげでバックエンドの開発の負担がかなり減りました。おまけに無料。
Vercel
無料で使える・Next.jsと相性が良いらしいため採用
ESLint, Prettier, Git, GitHub
コードの整形や管理の定番
以上のような構成で作成しました。割とベーシックな感じだと思います。また、おまけ程度ですがUIライブラリとしてNextUIを使いました。実際にNextUIを使ったのはページネーションのボタンの部分だけですが、、笑
開発方針
今まで作ってきたサイトがどうも素人くさく、テンションが上がらない感じだったので、今回は少し”おしゃれ”な感じで、かつUXも良いサイトを目指しました。デザインについてはあまり知識もセンスも無いため、個人的に良いなと思ったブログを参考にさせていただきました。
参考にしたセンスが良すぎる方達↓
また、microCMSとNext.jsの連携に関しては以下の方のブログ記事がとても参考になりました。
ダークモードのカスタマイズやちょっとしたアニメーションなどを実装するとなんだかこなれた感じになってよかったです。また、モバイルファーストということで全てのページでレスポンシブ対応なのもこだわりポイントです。
苦労したこと
以下、苦労したことです。参考になった記事や動画も貼っておくので、同じような問題に悩まされている方は参考になるかもしれません。
TailwindCSSでのダークモードのカスタマイズ方法
TailwindCSSでダークモードのカスタマイズの経験が無く、苦労しました。globals.cssでCSS変数を定義してtailwind.config.tsでCSS変数とカスタムカラーを結びつけるという流れは勉強になりました。
動的OG画像の生成・設定
動的はおろか静的なOG画像の生成すら経験がなかったのでかなり時間がかかりました。src/app/api/ogにroute.tsxというファイルを作り、OG画像の生成APIとしました。route.tsxではURLのクエリパラメータで与えられたブログ記事のIDから該当する記事の情報取得とGoogleフォントからフォントデータの取得を行い、OG画像を生成して返すという仕組みになっています。
本稿冒頭で画像を貼りましたが、生成されたOG画像には記事のタイトルと記事に割り当てられたカテゴリーが動的に描画されるようにしています。
import { ImageResponse } from '@vercel/og'
import { getArticlesDetail } from '@/app/_lib/microcms'
import { loadGoogleFont } from '@/app/_lib/font'
export const runtime = 'edge'
export async function GET(request: Request) {
  try {
    const { searchParams } = new URL(request.url)
    const id = searchParams.get('id')
    if (!id) {
      return new Response('id is required', { status: 400 })
    }
    // 初めはフォントデータをNext.jsのアプリ内にアップロードしておく方針でしたが、
    // Vercelの無料枠(1MB)を超えてしまうため、Googleフォントから取得する形式になりました
    // const fontData = await fetch(
    //   new URL("./compressedMplus.ttf", import.meta.url),
    // ).then((res) => res.arrayBuffer());
    const fontData = await loadGoogleFont({
      family: 'Noto Sans JP',
      weight: 700,
    })
    const article = await getArticlesDetail(id)
    return new ImageResponse(
      (
        <div
          style={{
            fontFamily: 'Noto',
            backgroundImage: 'linear-gradient(to bottom, #dbf4ff, #ffeeff)',
            backgroundSize: '100% 100%',
            height: '100%',
            width: '100%',
            display: 'flex',
            textAlign: 'left',
            alignItems: 'flex-start',
            paddingTop: '70px',
            flexDirection: 'column',
            flexWrap: 'nowrap',
            position: 'relative',
          }}
        >
          <div
            style={{
              width: '100%',
              fontSize: 70,
              color: '#000',
              padding: '0 120px',
              marginBottom: '10px',
              wordWrap: 'break-word',
            }}
          >
            {article.title}
          </div>
          <div
            style={{
              width: '100%',
              fontSize: 25,
              fontStyle: 'normal',
              color: '#000',
              padding: '0 120px',
              display: 'flex',
            }}
          >
            {article.categories.map((category, index) => (
              <div
                key={index}
                style={{
                  backgroundColor: '#6666FF',
                  padding: '5px 10px',
                  marginRight: '10px',
                  borderRadius: '30px',
                  color: '#fff',
                }}
              >
                {category.name}
              </div>
            ))}
          </div>
          <div
            style={{
              position: 'absolute',
              bottom: '70px',
              marginLeft: '140px',
              fontSize: 60,
            }}
          >
            Somahc 😼
          </div>
        </div>
      ),
      {
        width: 1200,
        height: 630,
        fonts: [
          {
            name: 'Noto',
            data: fontData,
            style: 'normal',
          },
        ],
      },
    )
  } catch (e: any) {
    return new Response('OG画像の生成に失敗しました', { status: 500 })
  }
}
以下の動画や記事が大変参考になりました。
環境変数で割り当てられるドメインが保護されている問題
上で作成した動的OG画像生成APIを利用するため、https://<アプリのドメイン名>/api/og?id=XXXXXのようなURLにアクセスする必要があります。ところが、環境変数NEXT_PUBLIC_VERCEL_URLで自身のドメイン名を指定したURLを使ってもOG画像が取得できない症状に悩まされました。メタデータの指定の仕方がおかしいのかと色々考えましたが、VercelのダッシュボードからOG画像をテストしてみると問題なく設定されていることがわかりました。

どうやらNEXT_PUBLIC_VERCEL_URLで割り当てられるドメインは認証が必要なもので、それが理由でOG画像の取得が妨げられているとわかりました。認証が必要無い方のドメイン名をベタ書きすることで解決しましたが、正しい方法かわからないので、もし良い方法があれば教えてください。
ドメインの保護については以下で公式が説明しています。
アニメーションがうまく再生されない問題
ページ遷移時のアニメーション(要素がフワッと表示される)を実装するため調べたところ、Framer Motionというライブラリが広く使われているようでした。見よう見まねで自分のアプリに組み込んでみたところ、遷移先の要素が一瞬表示された後、消えてから改めてフワッと表示されるという謎挙動が発生してしまいまいた。結局原因はわからなかったのですが、template.tsxという特殊なファイルをルートのlauout.tsxやpage.tsxが置かれている場所に配置して設定することで正常に動作するようになりました。以下の動画が参考になりました。
公式ドキュメントでもtemplate.tsxについて説明があります。ちゃんと理解せずに使ってしまったので今度また勉強したいと思います。
MantineとTailwindCSSが競合してしまう問題
ページネーションボタンを作るため、当初はUIライブラリであるMantineを使ってみようと思ったのですが、TailwindCSSのスタイルと競合してしまうようで、デザインがおかしくなってしまいました。TailwindCSSとMantineを共存させる方法もあるみたいですが、ネットにあるやり方は自分の環境ではうまくいかず、それを自分のアプリに合わせて修正する技術もなかったため、NextUIという別のUIライブラリに切り替えました。
NextUIはTailwindCSSとの相性がよく、Tailwindを使って好きなようにカスタマイズできてとても良かったです。今回はページネーションボタンでしか利用しませんでしたが、今後はもっと積極的に活用していきたいです。
(MantineとTailwindCSSを共存させるためにはglobals.cssを以下のように変えるとうまくいくみたいです。自分の場合はダークモードの実装の兼ね合いからこの方法ではうまくいきませんでした。)
まとめ
おそらくNext.jsとmicroCMSを使ったブログサイト構築自体はそこまで難易度が高いわけでは無いと思うのですが、実力が伴っていないくせに色々な機能を盛り込もうとしてなかなか時間がかかってしまいました笑。
ただ、無理した分、色々なことが知れたし勉強にもなったのでよかったです。まだまだ独自ドメインの設定やリファクタリングなどやることはたくさんあるので、最強のブログ()を目指して頑張りたいと思います。
Discussion