上げる先が、ひとつしかなかった
脆弱性の警告が28件。そのうち23件が next に集約されることまでは分かっていました。あとは上げるだけ——のはずが、まず「どこまで上げるか」で手が止まりました。
更新先のバージョンをどう選んだか、自動化ツールがやってくれたこととやってくれなかったこと、そして「入れる必要のない設定」を消した判断。メジャーバージョンを2つ跨ぐ更新を、確認しながら進めた記録です。
まず、選択肢を数える
手元は 14.2.35 でした。自動で提案されていたのは 16.3.1。2世代ぶんの差があります。
最初に考えたのは「まず14系の最新まで上げて、それで消える分を消そう」でした。メジャーを跨がなければ壊れにくいからです。これは実行しませんでした。調べたら、前提が違っていたからです。
Next.js 14 サポート終了(2025-10-26)
最終パッチ 14.2.35 ← 手元にあるのがこれ
Next.js 15 Maintenance LTS 2026年10月まで
Next.js 16 Active LTS 2027年10月まで
14系の最新は、すでに手元にありました。「上げる」という操作に余地が無く、18件の警告は14に留まる限り1件も消えません。
調べる前は「14系の最新に上げましょう」と判断していました。実行すれば何かが変わるはず、という思い込みです。手を動かす前にサポート期限を1つ調べただけで、この無駄が消えました。作業の前に前提を確認するのは、遠回りに見えて速いです。
15を選ばなかった理由
残る選択肢は15と16です。15の方が変更が小さいので、一見こちらが安全に見えます。
しかし15のサポートは2か月後に切れます。そこで上げても、すぐ同じ作業をやり直すことになります。「変更が小さい」ことの価値は、その変更が長持ちするかどうかで決まります。2か月で無効になるなら、小さいことに意味がありません。
実質的に、上げる先は16しかありませんでした。
何が壊れるかを、先に調べる
移行ガイドを読んで、このサイトに該当するものだけを抜き出しました。全部を読むのではなく、自分のコードに関係するものを探すという読み方です。
| 変更点 | このサイトへの影響 |
|---|---|
params が非同期になる |
該当。記事ページで同期的に使っている |
| React 19.2 が必要 | 該当。18.3.1 から上げる必要がある |
next lint の廃止 |
該当。package.json に書いてある |
| 画像設定の既定値変更 | 明示指定していないので影響なし |
| Turbopackが既定に | webpack設定を持っていないので影響なし |
middleware の改名 |
使っていない |
該当したのは3つだけでした。2世代ぶんの変更に見えて、実際に手を入れる場所は限られていると分かった時点で、作業の見通しが立ちます。
自動化ツールは、半分やってくれる
Next.jsには移行用のコードモッド(コードを機械的に書き換えるツール)が用意されています。2つ実行しました。
# パッケージ更新・設定ファイルの書き換え・lintの移行
npx @next/codemod@canary upgrade latest
# params / searchParams の非同期化
npx @next/codemod@canary next-async-request-api .
変換結果は正確でした。たとえば記事ページはこうなります。
- type Props = { params: { slug: string } };
+ type Props = { params: Promise<{ slug: string }> };
- export default async function BlogPostPage({ params }: Props) {
+ export default async function BlogPostPage(props: Props) {
+ const params = await props.params;
49ファイルを走査して2ファイルを変更、エラー0。手で直していたら見落としが出る種類の作業です。
「0 errors」は「全部やった」ではありません。ツールが認識できなかった書き方は、静かに素通りします。変更されたファイル数と、自分が想定していた箇所の数が合っているかは、自分で確かめる必要があります。今回は2ファイルで一致していました。
入れる必要のない設定を、消した
ここが今回いちばん判断が要った箇所です。
コードモッドは、7つのファイルにこれを追加していました。
// TODO: Cache Components adoption. Refactor this route so this
// opt-out can be removed.
export const instant = false;
調べると、これは16.3の新機能に対するオプトアウトで、別の設定を有効にしているときにだけ意味を持ちます。このサイトはその設定を使っていないので、7ファイルすべてで何もしていません。
将来のために先回りして入れてくれたものです。しかし残すとどうなるかを考えました。
| 残す場合 | 消す場合 |
|---|---|
| 将来その機能を有効にしたとき、警告が出ない | そのとき改めて入れる必要がある |
| 「あとで対応する」というTODOが7か所に残る | コードに無効な設定が残らない |
消しました。誰も対応しないTODOは、そのうち景色になります。読むたびに「これは何だっけ」と確認するコストが7か所ぶん発生し、いずれ誰も読まなくなります。必要になったときに、必要な場所へ入れるほうが健全だと判断しました。
コードモッドが入れたものは正しい提案です。しかし「このプロジェクトにとって必要か」までは判断していません。ツールは一般的に安全な側へ倒します。プロジェクトの事情を知っているのは書き手のほうです。
本番に入れる前に、本番と同じもので確かめる
今回はmainに直接入れず、ブランチを切りました。
git checkout -b feat/next-16
(コードモッド実行・手直し)
npm run build ← 手元で通ることを確認
git push -u origin feat/next-16
ブランチをpushすると、デプロイ先がプレビュー用のURLを作ってくれます。本番と同じビルド手順で、本番と同じ環境に載った状態を、公開せずに触れます。
そこで確認したのは6点です。
1. 記事一覧が20本、サムネイルが出る
2. 記事ページの本文・目次・読了バー
3. sitemap.xml が25件(数を数える)
4. robots.txt が4行
5. 構造化データの日時に Z が付いている
6. ビルド時のエラー・警告
sitemapは、失敗しても固定ページだけを返して200で応答する作りにしてあります。壊れていても画面は正常に見えます。「表示された」ではなく「25件あった」まで確認しないと、記事がsitemapから消えていても気づけません。
全部通ったのを見てから、mainへマージしました。プレビューで通ったものが本番でも通る——確認の順番として、これが一番安く済みます。
結果
next 18件 → 0
postcss 4件 → 0
nanoid 1件 → 0
glob 3件 → 0
lodash 2件 → 2 (開発用のみ)
合計 28件 → 2件
glob が消えたのは想定外でした。ESLintも一緒に上がり、その依存として新しい glob が入ったためです。本番で動くもののうち、既知の脆弱性は無くなりました。
残った2件は開発用の依存で、配信される成果物には入りません。
今回の学び
まず、「上げる」の前に、上げ先を数えること。14系の最新はすでに手元にあり、実行しても何も起きないコマンドを打ちかけていました。サポート期限を1つ調べるだけで防げます。
次に、変更の小ささより、その変更が持つ期間で選ぶこと。15は変更が小さいですが2か月で切れます。安全に見える選択肢が、実は先送りなだけということがあります。
そして、自動化ツールの出力も、読んでから受け取ること。コードモッドは正確でしたが、このプロジェクトに不要なものも入れていました。ツールは一般的に安全な側へ倒し、個別の事情までは知りません。
最後に、本番と同じもので確かめてから本番に入れること。プレビュー環境を1回挟むだけで、「マージしてから気づく」が「マージ前に気づく」に変わります。確認の順番を変えただけで、リスクの大きさが変わります。
意味を確認したい言葉があれば、用語集にまとめています。
セマンティックバージョニング / EOL / 型定義の自動生成 / コードモッド / ブランチ / プルリクエスト / ステージング環境 / 脆弱性 / デプロイ / ビルド / sitemap.xml / robots.txt / 構造化データ / 固定ページ
関連記事

依存の中の依存
脆弱性の警告が29件。数字だけ見ると手のつけようがないが、分類すると実質3つの問題だった。自動で出てくる更新PRの危険度の見分け方と、直接の依存を上げたのに警告が消えなかった理由。
追従する目次と、画面の取り合い
PCで右側に追従させていた目次を、スマホでは上部に貼り付けた。ところが貼り付くものは画面を占め続けるため、本文を読む場所が足りなくなる。情報を消さずに縦を横へ並べ替えた話と、「飛んだ先がバーの裏に隠れる」という副作用への対処。

wwwのあるなしで、別のサイトになる
kisaku.site と www.kisaku.site はどちらを開いても同じ画面が出る。しかし機械から見ると別のサイトになる。apexドメインにCNAMEを置けない理由と、Search Console・GA4・OGPの3か所すべてで同じホスト名に揃えなければならなかった話。