VitePressで作ったブログをNext.jsにマイグレーションする

経緯
もうちょい前の話になるけど、仕事でも愛用しているGitHub Copilotが誰でも無料で使えるようになったこともあり、なんかコーディングをしてみたくなった。
さて何を作ろうか?というときに、自分の技術ブログがVueのVitePressのJAMstackで作られているのを思い出して、前から使ってみたかったReactで作り直してみようかなぁと。
仕様を考える
まぁブログをやるだけならいくらでも無料サービスはあるんだけど、自分で思いのままにシステムをいじれて、かつ無料で運用できるってことで今回もGitHub PagesにSSGでビルドした静的なサイトをホスティングする形にしてみることに。
Reactで色々調べてみると、結局「Next.js」っていうフレームワークがSSGに向いてて良さそうとのこと。
今までは記事に集中できるように1カラムデザインとしていたが、今回は2カラムデザインとしてみる。
こうして出来上がったブログが以下。

Next.jsの導入
Node.js をインストール
MacかつHomebrew導入済みであれば以下コマンドでOK。
brew install node
Next.jsプロジェクトを作成
ボイラープレートを構築するには、以下コマンドを打つだけ。
npx create-next-app@latest
対話的に設定が可能。デフォルトだと以下が導入される。
- TypeScript
- Tailwind CSS
- ESLint
- App Router
- Turbopack
ローカルで動作確認
ターミナルで以下を打つ。
cd my-app
npm run dev
ブラウザでhttp://localhost:3000にアクセスして動作確認可能。
ブラウザにReact Developer Toolsも入れる。
Node.jsについて
JavaScriptのブラウザ以外での動作環境として、Node.jsというランタイムが存在する。
Reactによるアプリ開発や、ECMAScriptのトランスパイル、バンドルといった操作はNode.jsによって行う。
Reactなどの各種ライブラリは、Node.jsのパッケージマネージャであるnpmによってインストール可能。
Reactについて
すでに入門済みのVue.jsとの違いについて。
コンポーネントが基本的にHTML、CSS、JavaScriptを.vueファイルにまとめて記載するVue.jsに対して、.jsファイルにJavaScriptとして記載してHTMLとCSSはJavaScriptで返すようにするのがReact。
宣言的にDOM操作を行うために開発されたのがReact。JSXというJavaScriptの拡張構文を用いて宣言的にDOMを操作可能。また、状態管理も可能。
Reactでは、画面要素をコンポーネントという単位に分割して再利用可能な形で扱うことが可能。
Reactで実装したWebアプリケーションは、SPA(シングルページアプリケーション)になる。
Next.jsについて
SPAの課題として、ページの初期表示が遅くなる点と、検索エンジンのクローラーがHTMLメタ情報を読み取るのが難しいという点が挙げられる。
これらの課題は、Next.jsによるサーバーサイドレンダリング(SSR)という手法によって、Webサーバー上であらかじめJavaScript(React)からHTMLを生成することで解決できる。
他にも、APIサーバーとしての役割を持たせることや、静的サイト生成(SSG)の機能もある。ファイルベースルーティングも可能。
デプロイ先はVercelがおすすめ。
TypeScriptについて
静的型付け機能を追加したJavaScriptのスーパーセットのこと。Microsoftが開発。
トランスパイルすることでJavaScriptに変換される。
JavaScriptの動的型付けを起因とする実行時エラーから解放されるのが大きなメリット。
tsconfig.jsonでトランスパイルの詳細を決定する。
ChatGPTがまとめてくれたReact/Next.js実戦メモ(数日間の学び)
全体を通した重要な学び
- remark / rehype は混ぜると死ぬ
- TOC・検索・SEO は HTML を正とする
- Tailwind は動的 class に弱い
- 日本語全文検索は Fuse.js の限界を理解する
- Next App Router は Promise 前提
Markdown / HTML / パース周り
- Markdown → HTML → 表示、という 流れを常に意識
- remark と rehype は役割が違う(混在させない)
- TOC・検索・SEO は Markdown ではなく 最終HTMLを信頼
- 見出しIDは「自分で生成」より「付いた結果を読む」
- 正規表現ベースのTOCは壊れやすい
TOC(目次)
- TOCは Markdown から作らない、HTMLから作る
hrefとidが1文字でもズレたらリンクは機能しないuser-content-プレフィックスは典型的な地雷- Tailwind の
pl-${}は効かない(静的class必須) - 見出しレベルが深いほど 右に寄せる方が自然
Tailwind / CSS 設計
- Tailwindは 動的classに弱い
- Markdown表示には
proseが必須 - ダーク/ライト切り替えは CSS変数で一元管理
- 各コンポーネントに色を書くと後で破綻する
- スタイルは「ページ」より「レイアウト」に集約
検索(Fuse.js)
- Fuse.js は 形態素解析しない
- 日本語検索は「たまたまヒットする」ケースがあるだけ
plaintextは必須フィールド(optionalにしない)- 長文検索では
ignoreLocation: trueが重要 - 完璧な日本語検索は Fuse.js では難しい
TypeScript / 型設計
string | undefinedは最大の敵- Search用データと表示用データは型を分ける
anyは最後の逃げ道(使ったら必ず戻す)- 型エラーは 設計ミスのシグナル
Next.js App Router
- App Router の
paramsは Promise前提 - Server / Client Component の境界を常に意識
fsは Server Component だけで使う- Layout に集約すると全体が一気に楽になる
- build が通るまで安心しない(型チェックは最後)
開発フロー・トラブル系
- favicon.ico は意外と鬼門
- ビルドは「動いてたものを壊す」
- GitHubに上げる前に
.env/ secrets を再確認 - Linterは早めに入れると後が楽
- デグったら「一段階前に戻す」が最速
設計思想(重要)
- レイアウトは共通化、差分は
children - 記事 / カテゴリ / 検索は「中身だけ違う」
- 表示より データの形を先に整える
- 後から機能を足す前提で作ると壊れにくい
総括
- Next.jsは「分かってる人向けの優しさ」
- ブログは小さな機能の集合体
- 一個ずつ潰せば、ちゃんと積み上がる
- 今回のハマり方は 確実に実戦力が上がるやつ
感じたこと
- GitHub CopilotはいちいちチャットUIにソースをコピペしなくても、ワークスペース全体を把握した上でソース修正なり回答なりをしてくれるのが本当に便利で手放せない存在になってしまった
- でも、たった1日で無料分の月間クォータを食い潰してしまった…。1ヶ月待つのはしんどいからめっちゃ課金したくなってしまった
- 代わりとしては、
Amazon Q Developerが無料だから良いかも - 有料だけど
Claude Codeも気になってきてしまった
- React/Next.jsがほぼ未経験にも関わらず、2日間でマイグレーションがほぼ完成できたのはやっぱりAIすごいなぁと感じた
- いわゆる「バイブコーディング」をやってみたけど、成果物を完成させる速度は確かに早かったけど技術的に成長できた感じはしないなぁ
- 使い捨てツールならいいんだけど、結局ソースコードがツギハギだらけになっててなんだかなぁと。結局大事なのは設計力だなぁ
- Next.jsと相性の良いらしい
Vercelも気になってきた。特にAPI作りたくなった場合は試してみたい Codexも試してみる。いい感じGemini Code Assistも入れたけど無料枠多くて良さそう
リファクタリング
「このプロジェクト全体を通して、リファクタリングできる箇所はある?」
- 各コンポーネントにダークモードの背景色とテキスト色が書かれてたのでhtml要素にまとめる
- Tailwindの書き方がバージョン3と4で混在していたので4に統一
- srcフォルダ作成
- libフォルダをsrc以下に移動
- componentsをsrc以下に移動
- typesフォルダを作成してposts.tsを移動
- src/config/site.tsを新規作成
- Gemini:全体的に、定数はsrc/config/site.tsに外出ししてください
- カテゴリページとタグページも記事一覧ページと同じデザインを適用
- SVGアイコンはLucideに置き換え
- 読了時間表示
- トップへ戻るボタン追加
- 不要なパッケージのアンインストール
npx depcheck
npm uninstall <package>
- コメントつけ直し
- 「プロジェクト全体を対象に、JSDocを付けて」
- サイトマップ追加
- プライバシーポリシー追加
- ブラウザの開発者モードで出てるエラーの解消
- Linterエラーの解消
- 未使用関数、変数の削除
- Markdown処理の重複共通化
- 重複ロジックを整理
- をに置き換え
- ビルド時間とメモリ効率の改善
- getAllPostMeta() の結果を cache() でメモ化して、ビルド時の重複読み込みを減らす
- RSS/Sitemap のXML生成を共通化 + エスケープ対応
- 一覧ページの重複ロジックを統合
- TOC生成の正規表現依存をやめる
- カテゴリURLをslug化して統一
- データアクセス層の責務分離
- 検索結果の条件を設定
- タグ/カテゴリの「存在しない時」の扱いを共通化
- generateMetadata で重い処理を呼んでいる
- import順の整理
- slug生成ロジックの衝突リスク
- タグ集計ロジックの重複
- ファイルI/Oが同期API固定
- Geist をローカルフォント化してネットワーク非依存ビルドにする対応
- 一覧ページ実装の重複