AetherEchoesEngineering
Vol.042026年5月9日
Engineering#00543 min1,0646 view

Next.js 16 + Tailwind v4 を本番で動かして気付いた N 個のこと

Pages Router + @theme + next/font 7 種で個人雑誌を組んだ実装メモ。CSS 変数の名前空間、フォントの preload 戦略、ISR の落とし穴。

SoSoraEndo2026年5月9日3 min1,064

なぜ Pages Router を選んだか

App Router を採用しなかった。理由は単純で、個人サイトに RSC(React Server Components)の認知コストを払う価値がまだ薄い から。

私のサイトは:

  • 記事は SSG + ISR でほぼ全部静的化できる
  • ログインユーザは Admin 画面の自分だけ
  • レイアウトはフラット(ネスト 2 階層)

これに対して App Router は:

  • Server / Client の境界に常に意識を向ける必要がある
  • metadata API への移行学習コスト
  • ライブラリ互換性のばらつき(特に Next 16 直後)

費用対効果が見合わない。Pages Router は getStaticProps + getServerSideProps のシンプルな概念で十分。Next.js 16 でも継続サポートされている。

App Router を選ぶべきは、認証ありのプロダクトで多段階のサーバ取得 + ストリーミングが価値を生む場合。個人ブログは違う。

Tailwind v4 の @theme で semantic トークン体系

v4 で tailwind.config.js がほぼ廃止され、CSS の @theme ブロック側にトークンを書くスタイルになった。これがよくできていて、JS 設定への往復が減る のは想像以上に体験が変わる。

私は次の名前空間でトークンを定義した:

@theme {
  /* 背景階層 */
  --color-paper:        #FAFAF7;
  --color-paper-alt:    #F1F1EC;
  --color-paper-sunken: #E8E8E2;

  /* 文字階層 */
  --color-ink:       #0E0E0E;
  --color-ink-2:     #3A3A38;
  --color-ink-muted: #6E6E6A;

  /* 罫線 */
  --color-rule:      #1F1F1D;
  --color-rule-soft: #D9D9D2;

  /* 5 部門カラー */
  --color-cat-engineering: #2D5BFF;
  --color-cat-design:      #FFB020;
  --color-cat-ai:          #00A86B;
  --color-cat-process:     #A66B3A;
  --color-cat-essay:       #E63946;
}

これで bg-paper text-ink-muted border-rule-soft bg-cat-engineering などが自動生成される。Reader / Admin 両方で同じトークンを共有 できるので、命名空間を分けなくていい。

落とし穴を 1 つ。max-w-prose だけは Tailwind の built-in(65ch)が勝つ--container-prose: 42.5rem@theme に書いても、なぜか max-w-prose は上書きされない。素直に max-w-[42.5rem] のような arbitrary value で書く方が確実だった。

next/font の preload は CJK で外す

7 種類のフォントを next/font/google でセルフホストしている:

  • Bricolage Grotesque(display, variable wdth/opsz)
  • Instrument Sans(body, variable)
  • Fraunces(serif italic, SOFT/WONK 軸付き)
  • Zen Kaku Gothic New(CJK display)
  • Noto Sans JP(CJK body)
  • Noto Serif JP(CJK serif)
  • JetBrains Mono(mono)

Latin 系は preload あり、CJK 系は preload なし が正解。日本語フォントは subset にしても 5MB 以上あるので、どのページにも全 weight が並ぶと初回 LCP が壊れる。

const fontNotoJp = Noto_Sans_JP({
  weight: ['400', '500', '700'],
  variable: '--ff-noto-jp',
  display: 'swap',
  preload: false,   // ← ここが重要
});

display: swap と組み合わせると、英文が即座に出て日本語フォントは読み込み完了後に置き換わる 動きになる。FOIT より体感は良い。

ISR + revalidate は最初から組む

記事公開時に Next.js の ISR を即座に再生成する仕組みを最初から組んだ。これをあとから入れるのは大変なので、最初の 1 日でやる方がよい。

仕組みは単純:

# Rails 側、Post 保存後の after_commit
class Post < ApplicationRecord
  after_commit :enqueue_revalidate, if: :saved_change_to_status?

  def enqueue_revalidate
    RevalidatePostsWorker.perform_async(slug)
  end
end
# RevalidatePostsWorker
class RevalidatePostsWorker
  include Sidekiq::Worker

  def perform(slug)
    HTTP.headers('Authorization' => "Bearer #{ENV['NEXT_REVALIDATE_SECRET']}")
        .post("#{ENV['NEXT_PUBLIC_URL']}/api/revalidate", json: { paths: ["/posts/#{slug}", "/", "/posts"] })
  end
end
// frontend/pages/api/revalidate.ts
export default async function handler(req, res) {
  if (req.headers.authorization !== `Bearer ${process.env.NEXT_REVALIDATE_SECRET}`) {
    return res.status(401).end();
  }
  const { paths } = req.body;
  await Promise.all(paths.map((p) => res.revalidate(p)));
  res.json({ revalidated: true });
}

これで「Admin で公開ボタンを押す → 数秒以内にトップページと記事ページが更新される」流れができる。

SSR API URL と CSR API URL の使い分け

これは Next.js + Rails を Docker で動かしている人が必ず詰まるところ。

  • ブラウザから叩く URL: http://localhost:2001/api/v1
  • SSR / getStaticProps から叩く URL: http://backend:3000/api/v1

Docker Compose の networking では、コンテナ内の別サービスは host=service_name port=internal_port でアクセスできる。localhost:2001 を Next.js コンテナの内側で使うと届かない

私は Axios クライアントで分岐させた:

const baseURL =
  typeof window === 'undefined'
    ? process.env.NEXT_PUBLIC_SSR_API_URL || 'http://backend:3000/api/v1'
    : process.env.NEXT_PUBLIC_API_URL || 'http://localhost:2001/api/v1';

typeof window === 'undefined' が SSR 判定のお決まり。これを忘れると、dev では動くが prod で 500 が返る 罠にハマる。

まとめ

実運用で初めて気づいたことを、5 つに圧縮するなら:

  1. Pages Router は今でも妥当な選択肢。App Router が万能とは限らない。
  2. @theme ベースの semantic トークン は Reader / Admin 両方で再利用が効く。
  3. CJK フォントは preload を切る。サイズが桁違い。
  4. ISR + revalidate は最初の日に組む。後付けは難しい。
  5. API URL の SSR / CSR 分岐は必須。typeof window で判定。

この 5 つを最初に押さえると、Next.js + Rails の個人サイトは半日で安定運用に入れる。

ところで、これらは全部 Claude Code とのセッションで気づいた。AI に書かせた素のコードからではなく、AI と話しながら本番に出す過程で出てきた。

実装の落とし穴は、結局のところ「動かしてみないと見えない」場所にある。

Tags

Reaction

Share

X (Twitter)