[{"data":1,"prerenderedAt":66},["ShallowReactive",2],{"article-kkrzgveya":3,"prev-article-kkrzgveya":33,"next-article-kkrzgveya":54},{"contents":4,"totalCount":31,"offset":32,"limit":31},[5],{"id":6,"createdAt":7,"updatedAt":8,"publishedAt":8,"revisedAt":8,"title":9,"content":10,"tags":11,"is_no_index":30},"kkrzgveya","2025-09-02T11:43:40.111Z","2025-09-02T11:46:23.890Z","Nuxt4がいつの間にか出ていたのでサイトを更新した","\u003Cp>どうも、わたしです。\u003C/p>\u003Cp>先日、Nuxtのバージョン4がリリースされたことに、数ヶ月経ってから気づきました。\u003Cbr>告知はあったようですが、知らないところでいつの間にかひっそりと公開されていて、少しばかり驚いています。\u003Cbr>Nuxt 3のリリースから約2年半ぶりのメジャーバージョンアップだそうです。\u003C/p>\u003Cp>メジャーバージョンアップと聞くと、多くの破壊的変更が伴うのが常で、正直なところ「面倒だなーーー」という感情が先に立ちました。\u003Cbr>しかし、どうせいつかは対応しないといけないので、少しばかり重い腰を上げて、移行作業に取り掛かることにしました。\u003C/p>\u003Cp>\u003Ca href=\"https://nuxt.com/blog/roadmap-v4\" target=\"_blank\" rel=\"noopener noreferrer\">公式のロードマップ\u003C/a>に目を通すと、近々Nuxt5のリリースも控えてそうな雰囲気で、今回のv4は、v5へ向けた地ならし、つまり一度破壊的変更のステップを踏んでおくためのリリースみたいです。\u003C/p>\u003Ch1 id=\"ha8a4a373ac\">Nuxt 3から4への移行、所感とか\u003C/h1>\u003Cp>結論から言うと、めちゃくちゃスムーズにアップグレードできました。\u003C/p>\u003Cp>以前のNuxt 2から3への移行を経験された方なら同意していただけると思うのですが、あれは本当に悪夢でした。\u003Cbr>エコシステムの対応は追いついていないし、Composition APIへの移行は思考の根本的な書き換えを要求されるしで、多くの方が移行作業で心を消耗させたはずです。\u003Cbr>当時のわたしもその一人でした。\u003C/p>\u003Cp>その反動なのか、今回のNuxt 3から4への移行は、驚くほどストレスなく、シンプルに完了しました。\u003Cbr>対応が必須となる箇所が少なく、後方互換性もかなり考慮されている印象です。\u003Cbr>あの地獄のようなマイグレーション作業を思うと、拍子抜けするほどでした。\u003C/p>\u003Ch1 id=\"h2220f0ef7d\">やったこと\u003C/h1>\u003Cp>詳細な手順については、ほぼ\u003Ca href=\"https://nuxt.com/docs/4.x/getting-started/upgrade\" target=\"_blank\" rel=\"noopener noreferrer\">公式のアップグレードガイド\u003C/a>の通りに進めて問題ありませんでした。\u003C/p>\u003Cp>既にNuxt 3の環境がある場合、\u003Ccode>nuxt.config.ts\u003C/code>に\u003Ccode>future.compatibilityVersion: 4\u003C/code>を一行書き足すのが最も手軽な方法として紹介されています。\u003Cbr>しかし、古いパッケージを使いながら設定だけで新しいバージョンに擬態させるのは、どうにも気持ち悪く、私はこの方法を選びませんでした。\u003C/p>\u003Cp>というわけで、パッケージごと更新する、よりクリーンなアプローチを選択しました。\u003Cbr>公式で紹介されていたマイグレーション用のコマンド\u003Ccode>npx codemod@latest nuxt/4/migration-recipe\u003C/code>は、残念ながら私の環境ではうまく動作しませんでした。\u003C/p>\u003Cp>やったことは、本当にこれだけです。\u003C/p>\u003Col>\u003Cli>\u003Ccode>pnpm i nuxt@^4.0.0\u003C/code> を実行\u003C/li>\u003Cli>その他、関連ライブラリを最新版に更新\u003C/li>\u003Cli>ディレクトリ構成を変更\u003C/li>\u003C/ol>\u003Cp>\u003Ccode>pnpm dev\u003C/code>を実行して、ローカルサーバーが何事もなかったかのように起動したときは、あっさりすぎて逆にびっくり。\u003Cbr>Nuxt 2から3への移行で経験した延々と続くエラーとの格闘は、もう過去のものみたいです。\u003C/p>\u003Ch1 id=\"h1afe451c43\">さいごに\u003C/h1>\u003Cp>今回のNuxt 4への移行は、過去の経験が嘘のようにスムーズなものでした。\u003Cbr>あの時の苦労を思えば、今回の変更は部屋の模様替え程度の軽微な作業だったと言えます。\u003C/p>\u003Cp>しかし、前述の通り、これは数ヶ月後にリリースが予定されているNuxt 5への布石に過ぎないようです。\u003Cbr>この束の間の平穏がいつまで続くのかは分かりませんが、とりあえずは最新の環境で運用できることに満足しています。\u003C/p>\u003Cp>みさなんも、メジャーアップデートと聞いて身構えず、早めに対応しておくのがよさそうです。\u003Cbr>それでは。\u003C/p>",[12,18,24],{"id":13,"createdAt":14,"updatedAt":15,"publishedAt":14,"revisedAt":15,"slug":16,"name":17},"4oy2jc8mdg","2025-04-28T07:14:01.179Z","2025-12-03T16:08:24.495Z","diary","日記",{"id":19,"createdAt":20,"updatedAt":21,"publishedAt":20,"revisedAt":21,"slug":22,"name":23},"qfey3yw0z1","2025-04-29T08:44:41.687Z","2025-12-03T16:07:14.622Z","technology","テクノロジー",{"id":25,"createdAt":26,"updatedAt":27,"publishedAt":26,"revisedAt":27,"slug":28,"name":29},"8q8qyy8sz3","2025-04-29T08:44:23.406Z","2025-12-03T16:07:24.495Z","programming","プログラミング",false,1,0,{"contents":34,"totalCount":53,"offset":32,"limit":31},[35],{"id":36,"createdAt":37,"updatedAt":38,"publishedAt":39,"revisedAt":38,"title":40,"content":41,"tags":42,"is_no_index":30,"summary":52},"px6vdcxupeg","2025-08-24T09:38:57.191Z","2025-08-24T13:07:52.435Z","2025-08-24T12:52:35.857Z","【GCP破産RTA】SSGでのLLM要約機能実装の顛末","\u003Cp>どうも、わたしです。\u003C/p>\u003Cp>先日、このブログにLLMによる記事の要約機能を実装しました。\u003Cbr>記事一覧のDescriptionが、単に本文の冒頭を切り取ったものではなく、Gemini 2.5 Flashが生成した要約文になる、というものです。\u003C/p>\u003Cfigure>\u003Cimg src=\"https://images.microcms-assets.io/assets/3aba23b5bd6f4b79800a0305d0e4f8aa/d5eca816bbbf4ccc812a30c867c1b9c8/image.png\" alt=\"\" width=\"3600\" height=\"2010\">\u003C/figure>\u003Cp>技術的な課題としては、microCMSの無料プランではwriteAPIが利用できず、生成した要約をCMS側に保存できないという制約をどう乗り越えるか、という点にありました。\u003C/p>\u003Ch1 id=\"h6a33b9ef65\">アーキテクチャ選定と過信\u003C/h1>\u003Cp>最も安直なSSRでは、リクエスト毎にAPIを叩くことになり、コストとUXの観点から論外です。\u003Cbr>そこで、ビルド時に全記事の要約を済ませておくSSGのアプローチを選択しました。\u003Cbr>具体的には、Nuxtのビルドフックを利用して、ビルドプロセスの中で全記事のデータをmicroCMSから取得、記事IDごとに要約生成用のAPIエンドポイント (\u003Ccode>/api/summarize-article/:id\u003C/code>) を\u003Ccode>prerender\u003C/code>の対象に加える、という設計です。\u003C/p>\u003Cp>これなら、LLMのAPIコールはビルド時に限定され、ユーザーへのレスポンスは静的ファイルなので高速。\u003Cbr>コストとUXの問題を両立できる、かなりいい感じでイケイケな設計だと、この時点では本気で考えていました。\u003Cbr>夜のテンションも相まって、自分の実装に若干の陶酔すら覚えていたかもしれません。\u003C/p>\u003Cp>\u003Ca href=\"https://github.com/chan-mai/mq1-web/commit/c21b057b369abada5554390d6e825160fcb4e38f\" target=\"_blank\" rel=\"noopener noreferrer\">https://github.com/chan-mai/mq1-web/commit/c21b057b369abada5554390d6e825160fcb4e38f\u003C/a>\u003C/p>\u003Ch1 id=\"ha0bd70f85e\">破滅\u003C/h1>\u003Cp>しかし、実装から24時間も経たないうちに、GCPから予算アラートの通知が届きました。\u003C/p>\u003Cp>一瞬、何かの間違いかと思いました。\u003Cbr>APIキーの漏洩も考えましたが、クライアントサイドからの呼び出しは実装しておらず、キーの管理も適切に行っているはず。\u003Cbr>GCPの請求ダッシュボードを確認すると、グラフが異常な角度で跳ね上がっており、明らかに何かがおかしい。\u003C/p>\u003Cfigure>\u003Cimg src=\"https://images.microcms-assets.io/assets/3aba23b5bd6f4b79800a0305d0e4f8aa/508f2e9ff39940d5a578e5b44cd16bb7/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202025-08-24%2017.51.40.png\" alt=\"\" width=\"3600\" height=\"2004\">\u003C/figure>\u003Cp>メトリクスを確認すると、APIの呼び出し時刻はビルド実行時のみ。\u003Cbr>ここでようやく、問題が自分の実装そのものにある可能性に行き着きました。\u003C/p>\u003Cp>原因は、わたしの設計にあった致命的な考慮漏れでした。\u003Cbr>以下が問題のコードです。\u003C/p>\u003Cdiv data-filename=\"/nuxt.config.ts\">\u003Cpre>\u003Ccode class=\"language-typescript\">  nitro: {\n    prerender: {\n      autoSubfolderIndex: true,\n      crawlLinks: false,\n      routes: [],\n      failOnError: false,\n    }\n  },\n  hooks: {\n    async &quot;nitro:config&quot;(nitroConfig) {\n      if (nitroConfig.dev)  return;\n      if (nitroConfig.prerender?.routes === undefined) return;\n      \n      const client = createClient({\n        serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN!,\n        apiKey: process.env.MICROCMS_API_KEY!,\n      });\n\n      const [articles, tags] = await Promise.all([\n        client.get({\n          endpoint: &apos;articles&apos;,\n          queries: {\n            limit: 100, // &lt;-- 問題の根源その1\n            orders: &quot;-publishedAt&quot;,\n          },\n        }),\n        client.get({\n          endpoint: &apos;tags&apos;,\n          queries: {\n            limit: 100,\n            orders: &quot;-publishedAt&quot;,\n          },\n        }),\n      ]);\n\n      // 記事\n      const articleRoutes = articles.contents.flatMap((mount: any) =&gt; [\n        `/entry/${mount.id}`,\n        `/api/summarize-article/${mount.id}`, // &lt;-- 問題の根源その2\n      ]);\n      // タグ\n      const tagRoutes = tags.contents.map((mount: any) =&gt; `/tag/${mount.id}`);\n\n      nitroConfig.prerender.routes = [\n        ...nitroConfig.prerender.routes,\n        ...articleRoutes,\n        ...tagRoutes,\n      ];\n    },\n  },\u003C/code>\u003C/pre>\u003C/div>\u003Cp>勘のいいガキは嫌いだよ…なんてセリフを言うまでもなく、聡明な読者の方ならこの実装の問題に気づいたかもしれません。\u003C/p>\u003Cp>わたしは、Nuxtのビルドプロセスにフックを仕掛けて、microCMSから記事を100件まとめて取得しています。\u003Cbr>そして、その取得した100件の記事すべてに対して、\u003Ccode>prerender.routes\u003C/code>に要約生成APIのエンドポイント (\u003Ccode>/api/summarize-article/${mount.id}\u003C/code>) を追加しているのです。\u003C/p>\u003Cp>これが何を意味するか、わかりますか？\u003C/p>\u003Cp>ビルドが走るたびに、記事の内容が1文字も変更されていなくても常に100件の記事すべてに対する要約生成リクエストがGeminiに飛ばされる。\u003Cbr>そう、毎回、です。\u003Cbr>ちょっとCSSを1行直しただけのコミットでも、\u003Ccode>package.json\u003C/code>のライブラリを更新しただけのコミットでも、CI/CDが健気にビルドを走らせるたびに、100回、GeminiのAPIが叩かれる。\u003C/p>\u003Cp>しかも、このブログの記事、一つ一つが無駄に長い。\u003Cbr>短いものでも3,000文字、長いものだと3万文字を超えたりします。\u003Cbr>LLMの料金は概ね入力と出力のトークン数で決まります。\u003Cbr>大量の文字は、大量のトークンを消費する。\u003C/p>\u003Cp>つまり、私は「ビルドのたびに、長文記事100個分の要約リクエストを強制的に実行する」という、GCP破産直行便みたいな処理を良かれと思って実装してしまっていたのです。\u003Cbr>本当に、ぐえーーって感じ。\u003Cbr>昨日の私を本気で殴りに行きたい。\u003Cbr>なんで気づかなかったんだろう。\u003Cbr>キャッシュ戦略がどうとか、差分だけ更新するとか、そういう当たり前の地味で大事なことをすっかり忘れていました。\u003Cbr>成長が、ない。\u003C/p>\u003Cp>恐ろしい。本当に恐ろしいです。\u003C/p>\u003Ch1 id=\"h55b1e78e3d\">対処と結果\u003C/h1>\u003Cp>原因が特定できた以上、選択肢は一つしかありません。\u003Cbr>このまま放置すれば、わたしのけして分厚くはないお財布は、Google神への強制的なお布施によって、数日のうちに塵と化すでしょう。\u003Cbr>リアルに生活が終わってしまう。\u003C/p>\u003Cp>LLM関連のコミットをすべてRevertし、機能を丸ごとロールバック。\u003C/p>\u003Cp>副次的な効果として、ビルド時間が劇的に改善されました。\u003Cbr>以前は律儀に100件近い記事の要約APIの応答を待っていたため、8分ほどかかっていたビルドが、機能削除後はわずか50秒で完了するようになってしまいました。\u003C/p>\u003Cfigure>\u003Cimg src=\"https://images.microcms-assets.io/assets/3aba23b5bd6f4b79800a0305d0e4f8aa/790de29cb7eb49e7924e6ccbe02a3c11/image.png\" alt=\"\" width=\"3600\" height=\"2008\">\u003C/figure>\u003Cp>あまりの速さにもはや笑うしかありませんでした。\u003Cbr>物理的に重たい処理がなくなったのですから当然ですが、これだけの時間をAPIとの通信に費やしていたという事実を改めて突きつけられました。\u003C/p>\u003Ch1 id=\"h1afe451c43\">さいごに\u003C/h1>\u003Cp>今回のトラブルから得られた教訓は、極めてシンプルです。\u003C/p>\u003Cul>\u003Cli>ビルドプロセスにおける外部有料APIのコールは慎重に検討する\u003Cp>ビルドは、コンテンツの更新以外にも、依存関係の更新や設定変更など、様々なトリガーで実行されます。\u003Cbr>そのたびに意図しないコストが発生する可能性があることを常に念頭に置くべきでした。\u003C/p>\u003C/li>\u003Cli>冪等性を意識し、キャッシュ戦略を導入する\u003Cp>今回のようなケースでは、一度生成した要約はCloudflare R2やVercel KVのような外部ストレージにキャッシュとして保存し、記事が更新された場合のみ再生成する、といった差分更新の仕組みを導入すべきでした。\u003Cbr>全件処理は、やはり悪手です。\u003C/p>\u003C/li>\u003C/ul>\u003Cp>割と高い勉強代にはなりましたが、インフラコストに対する意識が甘かったことを痛感させられる良い経験だったと、今は思うことにしています。\u003Cbr>皆さんも、ビルド時のAPIコールにはくれぐれもご注意ください。\u003C/p>\u003Cp>それでは。\u003C/p>",[43,44,50,51],{"id":13,"createdAt":14,"updatedAt":15,"publishedAt":14,"revisedAt":15,"slug":16,"name":17},{"id":45,"createdAt":46,"updatedAt":47,"publishedAt":46,"revisedAt":47,"slug":48,"name":49},"wia4slp55q","2025-04-28T07:14:14.541Z","2025-12-03T16:08:13.798Z","thoughts","独り言",{"id":19,"createdAt":20,"updatedAt":21,"publishedAt":20,"revisedAt":21,"slug":22,"name":23},{"id":25,"createdAt":26,"updatedAt":27,"publishedAt":26,"revisedAt":27,"slug":28,"name":29},"どうも、わたしです。先日、このブログにLLMによる記事の要約機能を実装しました。記事一覧のDescriptionが、単に本文の冒頭を切り取ったものではなく、Gemini 2.5 Flashが生成した要約文になる、というものです。技術的な課題としては、microCMSの無料プランではwriteAPIが",32,{"contents":55,"totalCount":65,"offset":32,"limit":31},[56],{"id":57,"createdAt":58,"updatedAt":59,"publishedAt":59,"revisedAt":59,"title":60,"content":61,"tags":62,"is_no_index":30,"summary":64},"5uoungfiv","2025-09-19T18:00:20.644Z","2025-09-19T18:08:37.327Z","正しさの主張","\u003Cp>お久しぶりです。\u003C/p>\u003Cp>最近、目を背けたい事実に直面することがありました。\u003Cbr>きっかけは、過去に自分が書いた、あるコミュニティについて論じた文章を偶然読み返したことです。\u003Cbr>そこには、特定の人物像に対し、その行動原理を半ば断罪するかのように書き連ねる、傲慢な自分がいました。\u003Cbr>そして、最も衝撃的だったのは、その批判の矛先が、現在の自分自身に正確に突き刺さることでした。\u003C/p>\u003Cp>過去の文章で私が問題視\u003Cs>(あるいは敵視)\u003C/s>していたのは、他責的で、他者への要求がましく、常に自分は被害者であるという立場から世界を語る人々でした。\u003C/p>\u003Cp>しかし、今の私はどうでしょう。\u003Cbr>日々の生活の中で、自分の思い通りにならないことがあると、すぐに他者や環境のせいにしてはいないか。\u003Cbr>自分の苦境を理解し、手を差し伸べるのが当然であるかのように、周囲に期待してはいないか。\u003Cbr>心当たりのある節は、枚挙にいとまがありません。\u003Cbr>自分がいかに他人に対して要求がましく、不平不満を募らせているかに気づいてしまいました。\u003Cbr>誰かが自分の期待通りに動いてくれないと苛立ちを覚えたり、自分の苦境を理解してくれない社会や他者に対して、心の中で攻撃的な言葉を並べたりする。\u003Cbr>その姿は、私がかつて記事の中で批判的に描写した人物像と、驚くほど酷似していました。\u003C/p>\u003Cp>私がコミュニティの中に見ていた問題点は、全くの他人事などではなく、何を隠そう、私自身の内面にある課題を投影したものであったのだと。\u003Cbr>これはまさしく同族嫌悪です。\u003Cbr>自分の中にある、認めたくはない醜い部分。\u003Cbr>それと同じものを他者の中に見つけ出し、批判することで、あたかも自分だけは違うのだと安心しようとする、卑劣な防衛機制でした。\u003C/p>\u003Cp>以前の記事でも言及した気がしますが、「誰も私を助けてくれない」という嘆きは、いつしか「だからあなたは私を助けるべきだ」という、他者への一方的な要求に変わってしまう。\u003Cbr>というか変わってしまった。\u003Cbr>他者の時間や感情を際限なく奪うための、万能の呪文を私は覚えてしまった。\u003Cbr>私も立派な化け物だったみたいです。笑ってください。\u003C/p>\u003Cp>完全な被害者であるという認識は、一種の安全圏として機能します。\u003Cbr>自分は傷つけられた側であり、悪くない。\u003Cbr>故に、他者からの配慮や支援を要求する権利がある。\u003Cbr>この論理は、一見すると正当なものに思えますが、容易に肥大化し、「自分は常に正しく、周囲が間違っている」という歪んだ特権意識へと変わります。\u003Cbr>この立場に安住する限り、自分自身の問題と向き合う必要がありません。\u003Cbr>全ての原因を外部に押し付け、自分は変わらずに済むからです。\u003Cbr>傲慢ですね。\u003C/p>\u003Cp>「誰も助けてくれない」という嘆きは、この被害者意識と結びつくことで、容易に他者への攻撃性へと変質します。\u003C/p>\u003Cp>自分の価値を他者からの承認に依存していると、期待通りの反応が返ってこないだけで、自分の存在が根底から否定されたかのような感覚に陥ってしまいます。(しまいました)\u003Cbr>すると、助けてくれなかった相手は、単なる「期待に応えなかった人」から、「自分を否定し、傷つけた加害者」へと認識がすり替わります。\u003Cbr>こうして、本来であれば内省に向けられるべき体力が、他者への攻撃という最も安直な形で発散されてしまうのです。\u003Cbr>私自身、ほんとに心当たりしかなくてびっくりします。\u003C/p>\u003Cp>私がかつて批判した彼ら彼女らの傲慢で横暴な姿は、すでになっていた自分自身の姿だったみたいです。\u003C/p>",[63],{"id":45,"createdAt":46,"updatedAt":47,"publishedAt":46,"revisedAt":47,"slug":48,"name":49},"お久しぶりです。最近、目を背けたい事実に直面することがありました。きっかけは、過去に自分が書いた、あるコミュニティについて論じた文章を偶然読み返したことです。そこには、特定の人物像に対し、その行動原理を半ば断罪するかのように書き連ねる、傲慢な自分がいました。そして、最も衝撃的だったのは、その批判の矛",36,1783975277940]