July 2026 Release Notes (2026年7月 リリースノート - Ryota-Blog)

りょた
目次
こんにちはです!☺️
7月のリリースノートをお届けします!
7月は、このブログが始まって以来もっとも「土台」に手を入れた月になりました。記事コンテンツの追加も多い月でしたが、今回はあえて内部の仕組みのアップデートにフォーカスします。
今月のハイライトはこちら。
- microCMSからVelite + MDXへのコンテンツ基盤移行(脱CMS)
- パフォーマンス改善の総仕上げ(フォントのサブセット自己ホスト・キャッシュ層の強化)
- Next.js 16の非推奨警告に従ったら本番デプロイが止まった話
- AI運用体制(サブエージェント/スキル)の整備
コンテンツ基盤の刷新: microCMS → Velite + MDX 📦
#7月最大の変更は、記事コンテンツの管理をmicroCMSからリポジトリ内のMDXファイル(Velite)へ移行したことです。日英30記事×2言語=60ファイルを、URL完全維持・変換ロスゼロで引っ越しました。
「なぜ移行したのか」「どうやって1日で終わらせたのか」という移行の全体像は専用記事に書いたので、そちらをご覧ください。
Next.js個人ブログを脱CMSした話(microCMS → Velite + MDXへ1日で移行)
リリースノートでは、専用記事に書いていない「移行を支えた検証の仕組み」と「移行後に起きたこと」を書きます。
移行を支えたパリティ検証 🔍
#今回の移行では「読者から見て何も変わらないこと」をパリティ原則として最初にADR(Architecture Decision Record)へ明文化し、それを機械的に検証するスイートを最終ゲートにしました。検証は7段階です。
- 全記事URLの網羅チェック(旧URLがすべて生きているか)
- 旧URLからのリダイレクト検証
- sitemap・RSS・llms.txtの出力を移行前スナップショットとdiff比較
- メタデータ(title/description/OGP)の一致検証
- 記事構造(見出し・埋め込み要素)の一致検証
- ビジュアル回帰(pixelmatchによるスクリーンショット比較)
- Lighthouseスコアの劣化チェック
エンドポイントのスナップショットは移行作業の前に取得してリポジトリにコミットしておき、移行後のビルド出力と突き合わせる方式にしました。「変わっていないこと」の証明は、変わる前の記録がないと始まらないためです。
それでもすり抜けたバグはあります。移行直後の本番で「存在しないslugへのアクセスが404ではなく500になる」問題が見つかりました。原因はNext.jsの評価順序で、generateMetadataはページ本体より先に実行されるため、ここで投げた例外はページ側の404処理に到達する前に未捕捉エラーになります。
export async function generateMetadata({ params }: Props) {
try {
const blog = await getBlogBySlugByLocaleCached(blogId, locale);
// ...メタデータを構築
} catch {
// generateMetadataはページ本体より先に評価されるため、
// ここでnotFound()しないと未知slugが500になる
notFound();
}
}
7段階の検証を通しても、本番投入後にしか顕在化しないエッジケースは残る、という教訓でした。
見出しIDの自動生成を2日で2回作り直した話 🔁
#移行した記事は、microCMSが自動付与していた見出しID(hba7e17d1c0のような形式)をfrontmatterのheadingIds配列で保持し、rehypeプラグインが出現順に復元する設計にしました。共有済みのアンカーリンクを壊さないためです。
ところが移行後に新規記事を書いてみると、目次が空になるバグが発覚。headingIdsを持たない新規記事のIDを自動生成する経路が漏れていました。
初版の修正ではgithub-sluggerで見出しテキストからスラッグを生成したのですが、日本語見出しはURLフラグメントでパーセントエンコードされて肥大化することが分かり、翌日に方式を変更。最終的にmicroCMS互換の「h + sha256ハッシュ先頭10桁」形式に落ち着きました。
// 見出しid1件分のハッシュを計算する(microCMS互換の h+16進10桁 形式)
const hashHeadingId = (input: string): string =>
`h${createHash("sha256").update(input, "utf8").digest("hex").slice(0, 10)}`;
ポイントは次の3つです。
- ランダム値ではなくコンテンツ由来のハッシュなので、ビルドは決定的なまま(同じ本文からは常に同じIDが生成される回帰テストも追加)
- 同名見出しは出現回数をハッシュ入力に混ぜて区別し、それでも衝突したらsaltを足して再計算する多重防御
- 目次の生成とHTMLレンダリングの両方が同一関数・同一ソース(生MDX本文)からIDを導出することで、目次のリンク先と実際の
id属性が構造的にズレない
文字化けとの戦いと、検出の仕組み化 🕵️
#移行から約2週間後、記事の一部に文字化けが見つかりました。それも�のような分かりやすい化け方ではなく、似た形の漢字への置換という厄介なパターンです。
大卒→大匲男性→男有全員→全和遭遇→遇遇
HTML→MDXの一括変換時に混入したものでした。移行前のアーカイブと1文字ずつ突き合わせる比較を行って全64ファイルを横断チェックし、6記事の化けを修正。英語版32ファイルは検出0件で、化けは日本語の処理経路に固有の問題でした。
修正だけで終わらせず、再発を機械検出するチェッカー(scripts/check-garbled-text.mjs)を作って品質ゲートnpm run lint:contentに組み込みました。検出は3層構えです。
- 異常コードポイント検出: 単独の結合濁点、置換文字U+FFFD、NFC非正規化テキストなど
- 既知の化けパターン再発検知: 過去事案(
大匲・男有・遇遇など)のリストとの照合 - kuromoji形態素解析による未知語検出: 辞書に無い日本語トークンを疑わしい箇所として報告
コードブロックやURLの内部は、行番号を保ったまま空白に置換してから検査することで誤検知を防いでいます。
面白い(悔しい)のは検出の限界で、実測ではこの種の化けの約44%しか機械検出できません。全員→全和のような実在語の組み合わせへの化けは、形態素解析では原理的に検出不能です。そこはLLMによるレビュー層の担当、と役割分担をコード内に明記しました。機械にできることと、できないことの線引きをはっきりさせるのが運用のコツだと思っています。
パフォーマンス改善: LCP 17.4秒事件のその後 ⚡
#モバイルLCPが最悪17.4秒だった問題を、render-blocking CSSやクライアントバンドルへの記事全文混入を潰して本番3.7〜4.3秒まで改善した話は、専用記事に詳しく書きました。
Next.js個人ブログのモバイルLCPを17.4秒から改善した話(犯人は画像じゃなかった)
ここではその記事に書いていない「続き」の改善を紹介します。
フォントを「実際に使っている1,760文字」に切り詰める ✂️
#本文フォントのKosugi MaruはGoogle Fonts CDNから配信していましたが、Google Fontsのunicode-range 121分割CSSは「日本語Web全体の平均頻度」向けの汎用最適化です。実測すると、記事ページでは漢字の種類数に応じて分割ファイル34〜49個・280〜631KBの転送が発生していました。
ここで効いてくるのが脱CMSです。コンテンツがすべてリポジトリ内にあり、ビルド時に全文が確定するので、「サイトで実際に使っている文字」だけを抽出したサブセットフォントを作れます。
ビルド時に走るscripts/generate-font-subset.mjsの仕組みはこうです。
content/(記事MDX)・src/(UI文言)・locales/(翻訳)を再帰走査して実使用文字を収集- 安全マージンとしてASCII・ひらがな・カタカナ・記号類の基本セットを常時含める
subset-font(HarfBuzz WASM)でTTFからwoff2サブセットを生成- ファイル名にはwoff2とCSSテンプレートのsha256ハッシュを付与し、immutableキャッシュと両立
結果、1,760文字種・約283KBの全ページ共通1ファイルになりました。
地味に大事なのが「欠字」への防御です。
- draft記事の文字も収集対象に含める(公開に切り替えた瞬間の欠字を防ぐ)
- 収集ファイル数が異常に少ない場合はビルド自体を失敗させる(走査対象の設定ミスでサブセットが静かに縮むのを防ぐ)
- 未収録の漢字が使われてもフォールバックフォントで表示されるだけで、次のビルドで自動的に取り込まれる
新しい記事を書くたびにフォントが再生成されるので、運用の手間はゼロです。
もしもアフィリエイトウィジェットの遅延初期化 🐢
#実は7月上旬に、もしもアフィリエイトのウィジェットには別の問題がありました。ウィジェットを描画するローダー(bundle.js)はDOMContentLoaded/loadイベントでのみ描画処理を発火する仕様のため、Next.jsのソフトナビゲーション(クライアント遷移)で記事に入るとイベントが二度と発火せず、ウィジェットが永遠に表示されないのです。これはscript要素をマウント時に作り直し、合成loadイベントを1バッチにつき1回だけ発火させる方式で解決しました。
その上で今回の改善です。ウィジェットを9個含む記事では、ページロード直後にbundle.js(50KB)+商品画像9枚(約218KB)が一斉取得され、LCP画像と帯域を奪い合っていました。
そこでIntersectionObserverを使い、ウィジェットがビューポートの800px手前に近づくまでscript要素の生成自体を遅延するようにしました。ファーストビュー内のウィジェットは従来どおり即初期化されます。
遅延化で顕在化したレース条件もありました。ページ遷移時に前ページの未処理キューを破棄する仕組みが、遅延初期化下では「直前に発火した別ウィジェットのトリガー待ちキュー」まで誤って破棄し、そのウィジェットが永久に描画されない可能性がある。トリガー待ち中は破棄をスキップするよう修正し、「初期リクエストゼロ」「スクロールで描画」「ソフトナビ遷移後も描画」をE2Eテスト(3ブラウザ)で固定しました。
初期ロードの総転送量は1.18MB → 961KBになりました。
OpenNextのキャッシュにもう一段(regional cache)🏎️
#このブログはCloudflare Workers上でOpenNext(@opennextjs/cloudflare)を使って動いていて、ISR/SSGのキャッシュはR2に保存されます。ところが素のR2直結だと、キャッシュヒットでも毎回R2への往復が発生し、実測TTFBは520〜850msありました。
そこでキャッシュ層を2段構成にしました。
export default defineCloudflareConfig({
...(isProduction && {
// Cache API(データセンターローカル)でR2の手前をラップし、
// ISR/SSGエントリをrevalidate値までリージョン内で再利用する
incrementalCache: withRegionalCache(r2IncrementalCache, { mode: "long-lived" }),
queue: doQueue,
tagCache: d1NextTagCache,
}),
// ISR/SSGキャッシュヒット時にNextServerの起動を丸ごとスキップする
enableCacheInterception: true,
});
withRegionalCacheはデータセンターローカルのCache APIをR2の手前に挟み、リージョン内の2回目以降のアクセスをR2往復なしで返します。enableCacheInterceptionはさらに踏み込んで、キャッシュヒット時にNext.jsサーバーの起動そのものをスキップします。検証はレスポンスヘッダーのx-opennext-cacheで行いました。
ページ遷移のちらつき退治 ✨
#7月はView Transitions APIを使った「一覧のサムネイルが記事のアイキャッチへ滑らかに変形しながら移動する」遷移を入れたのですが、導入直後は遷移のたびに画面全体が明滅する問題に悩まされました。原因は2つありました。
- View Transitionsのデフォルト挙動であるページ全体の重ね合わせフェード(旧画面と新画面を重ねながら切り替える
::view-transition-old/new(root)のアニメーション)がヘッダーごと画面を明滅させていた。rootのアニメーションをanimation: noneで無効化し、サムネイルの変形アニメーションだけを残す構成に変更 - マウントのたびに走るfadeInアニメーションが、View Transitionのスナップショットを
opacity: 0の状態で撮ってしまっていた。フェード自体を撤去し、Tailwindの該当keyframe定義には「再導入禁止」の警告コメントを設置
画像側は、Veliteがビルド時に自動生成するblurDataURLをプレースホルダーに採用しました。スケルトンとblurを重ねると逆に明滅して見えるため、blurがある画像にはスケルトンを重ねない、という使い分けもしています。
Next.js 16の非推奨警告に従ったら本番が止まった話 🚨
#今月いちばんの「事件」です。
Next.js 16.2.10はビルド時に「middleware file conventionは非推奨。proxyへ移行せよ」という警告を出します。素直にこれへ対応し、src/middleware.tsをsrc/proxy.tsへリネームしました。事前にNext.js本体のビルド出力を確認し、proxy.tsがビルド後に従来と同じ構造で出力されることも見ていました。
3日後、本番デプロイが停止しました。
opennextjs-cloudflare buildが「Node.js middleware is not currently supported」というエラーで落ちるようになったのです。調査の結果、真因はNext.js本体のソースにありました。
- Next.js 16は
proxy.ts規約のファイルを必ずNode.jsランタイムのミドルウェアとして出力する(next/dist/build/entries.jsのrunDependingOnPageTypeに、proxyファイルをEdgeにする分岐が存在しない) - 一方、旧来の
middleware.ts規約は既定でEdgeランタイムになる - そして
@opennextjs/cloudflare(当時最新の1.20.1)はNode.jsミドルウェア未対応
つまり「非推奨警告に従う」と「Cloudflareにデプロイできる」が両立しない状態でした。runtime指定でも解決できない(そもそもEdgeへの分岐が無い)ため、リネームをrevertしてmiddleware.ts規約へ戻す判断をしました。警告 < 本番停止です。
再発防止として、src/middleware.tsの冒頭に経緯を丸ごと警告コメントで残しています。
// ⚠️ このファイルは意図的に `middleware.ts` 規約のまま維持している(proxy.ts へ改名しないこと)。
// 理由: Next.js 16 の新 `proxy.ts` 規約は、ビルド時に必ず Node.js ランタイムのミドルウェアとして
// 出力される(next/dist/build/entries.js の runDependingOnPageType: isProxyFile → onServer() 固定で
// Edge にする分岐が無い)。一方 `middleware.ts` は既定で Edge ランタイムになる。
// @opennextjs/cloudflare(現行 1.20.1)は Node.js ミドルウェアを未サポートで、Node 判定だと
// `opennextjs-cloudflare build` が "Node.js middleware is not currently supported" で exit 1 になる。
// Next.js 16 は `middleware.ts` に対して非推奨"警告"を出すが、警告 < 本番停止 であり、
// opennextjs-cloudflare が Node プロキシに対応する(upstream)まではこの規約を維持する。
フレームワークの推奨(deprecation)とデプロイアダプタの対応状況は、別々のタイムラインで動きます。「警告に従う前に、自分のデプロイ経路が追いついているか確かめる」は今月いちばんの教訓でした。
htmlのlang属性をSSRで正しく出す 🌐
#アクセシビリティ監査で見つかった構造的な課題も直しました。
このブログは/ja/...と/en/...の2ロケール構成ですが、従来のroot layoutはロケールを確定できない位置にあったため、lang属性はbeforeInteractiveスクリプトでdocument.documentElement.langを後付けする方式でした。この方式の問題は、SSRされた初期HTMLにlangが無いこと。JavaScriptを実行しないクローラーや支援技術からは、言語未設定のページに見えてしまいます。
App Routerには「html要素を描画できるのはroot layoutだけ」「root layoutは[locale]セグメントのparamsを受け取れない」という制約があります。そこで発想を変えて、src/app/[locale]/layout.tsx自体をroot layout化しました。これで<html lang={locale}>をSSR出力できます。
代償として、[locale]の外側で発生するエラー・404の境界設計をやり直す必要がありました。ロケールに依存しないnot-found.tsx/error.tsxは自前でhtml/bodyを持つ独立ファイルにし、ロケール未確定のケースはlang="ja"を既定値としています。1つの属性のためにlayout構成を作り直した形ですが、「初期HTMLの時点で正しい」ことにはそれだけの価値があると思っています。
AI運用体制の整備 🤖
#サブエージェントとスキルのハーネス化
#7月から、開発におけるAI(Claude Code)の運用を「その場のプロンプト頼み」から「リポジトリにコミットされた体制」へ移行しました。.claude/配下にプロジェクト専用の定義を置いています。
- repo-explorer(調査担当): コードベースの横断調査・影響範囲特定。読み取り専用
- web-researcher(Web調査担当): 技術情報の調査。プロジェクトの依存バージョンと照合し、古い記事を鵜呑みにしない指示付き
- test-runner(検証担当): lint・型チェック・テスト・ビルドの実行と失敗の一次切り分け。修正はしない
- code-reviewer(レビュー担当): このリポジトリ専用のシニアレビュアー
設計の軸はモデルルーティングです。調査・検索・定型実行は軽量モデルのサブエージェントに並列で任せ、結果の統合・設計判断・レビューは上位モデルが担う。特にcode-reviewerには、過去にこのリポジトリで実際に起きた事故(クライアントバンドルへの全記事JSON混入、OpenNextのリダイレクト設定バグなど)をチェックリスト化して持たせています。一度踏んだ地雷を、レビュー観点として資産化するイメージです。
あわせて、定型ワークフローをスキルとして3つ定義しました。
quality-check: lint → 型チェック → 単体テスト → ビルドの品質ゲート一括実行cloudflare-preview: workerd(Cloudflareの実行環境)での実機検証。next startでは再現しないOpenNext固有の挙動差を検出するlighthouse-audit: mobile基準のLighthouse計測。計測の罠(リダイレクトによるスコアペナルティなど)の知見も同梱
ちなみに、このリリースノート自体もこの体制で書いています。7月の全マージコミットの調査は3体のサブエージェントが並列で行いました。
AIボットのアクセス解析 📊
#AI時代の観測装置として、AIクローラー/AIエージェントによる記事アクセスの解析基盤も作りました。GPTBot・ClaudeBot・PerplexityBotなど14種のボットをUser-Agentで判定し、用途別(学習クロール/検索インデックス/ユーザー代理取得)に分類して記録します。
実装のポイントは、計測がページ表示の速度に一切影響しないことです。
- middlewareで記事URLへのAIボットアクセスを検出
event.waitUntil()でCloudflare D1への書き込みを非同期化(レスポンスは待たない)- テーブルは日付×記事×ボットのユニーク制約に対するUPSERTで、行数が爆発しない日次集計型
管理画面では記事別・ベンダー別・日別トレンドを可視化しています。どの記事がどのAIによく読まれているのか、眺めているだけでも面白いデータです。
小さいけど効いた修正 🔧
#Google検索のfaviconが地球儀になった
#パフォーマンス改善の一環でfaviconを1024×1024(1.4MB!)から96×96(2.4KB)へ縮小したところ、数日後にGoogle検索結果のfaviconが地球儀アイコンに変わってしまいました。
根本原因はサイズ変更そのものではなく、/favicon.icoが404だったこと。ブラウザや検索エンジンがデフォルトで参照する安定URLが存在せず、ハッシュ付きURLの変化をきっかけにGoogleの再評価で拾われなくなった形です。src/app/favicon.icoに16/32/48pxのマルチサイズICOを配置し、キャッシュヘッダーを整えて解決しました。
カテゴリID「test」の名前衝突
#「テスト」カテゴリのIDはtestだったのですが、記事雛形ジェネレーターが生成する仮値もtestでした。つまりfrontmatterにcategories: [test]とあったとき、本物のカテゴリなのか差し替え忘れなのか、機械的にも人間にも判別できない。実際にドキュメントが実在記事を「差し替え忘れ」と誤認する実害も起きました。
対策は衝突の両側を変えることです。カテゴリIDはtestingへ改名し、雛形の仮値は__REPLACE_ME__(実在し得ないID)に変更。旧URL/blogs/test/...はロケール有無×ページ種別の6パターンを恒久リダイレクトで網羅しました。命名の衝突は、起きてから気づくと本当に紛らわしいです。
まとめ
#7月の内部改善をまとめます。
- 脱microCMS: Velite + MDXへ移行、7段階のパリティ検証で「読者から見て何も変わらない」を担保
- 品質ゲート強化: 文字化けチェッカー(形態素解析つき)・内部リンクチェックをCIに組み込み
- フォント配信最適化: 実使用1,760文字のサブセット自己ホスト(全ページ共通283KB)
- キャッシュ強化: OpenNextにregional cache + cache interceptionを導入
- ランタイム騒動の収束: middleware.ts規約を維持する判断と、その理由のコード内ドキュメント化
- AI運用体制: サブエージェント/スキルをリポジトリにコミットされた資産へ
脱CMSでコンテンツがリポジトリに入ったことで、フォントのサブセット化や文字化けチェックのような「ビルド時に全コンテンツへ触れられる」前提の改善が次々に可能になりました。基盤の移行が、それ単体で終わらず後続の最適化を連鎖的に解放していく感覚があった1ヶ月です。
最後まで読んでいただき、ありがとうございました!🙏
ご質問やフィードバックがございましたら、お気軽にX(@Ryo54388667)までお声がけください。
もしこの記事が役に立ったら、欲しいものリストから投げ銭(ギフト券)していただけると泣いて喜びます🥺
