[{"data":1,"prerenderedAt":5997},["ShallowReactive",2],{"seo-locale-availability:\u002Fblog\u002Fobservable-ai-systems":3,"post:ja:\u002Fblog\u002Fobservable-ai-systems":7,"surroundings:ja":425},[4,5,6],"zh-CN","en","ja",{"post":8,"contentLocale":6,"isFallback":405},{"id":9,"title":10,"accent":11,"body":12,"description":404,"draft":405,"experience":406,"extension":407,"featured":405,"image":408,"meta":414,"navigation":415,"path":416,"publishedAt":417,"readingTime":183,"seo":418,"stem":419,"tags":420,"updatedAt":406,"video":406,"__hash__":424},"posts_ja\u002Fblog\u002Fobservable-ai-systems.md","AI システムの可観測性は Token の請求書だけではない","violet",{"type":13,"value":14,"toc":391},"minimark",[15,19,25,28,32,35,40,43,47,50,54,57,61,64,67,71,79,219,222,225,244,247,250,253,313,316,319,322,340,343,346,349,352,366,369,373,376,384,387],[16,17,18],"p",{},"従来のサービスで問題が起きたとき、私たちはステータスコード、レイテンシ、エラースタックを確認します。AI アプリケーションでもこれらの指標は重要ですが、最も大切な問いに答えるには不十分なことがよくあります。",[16,20,21],{},[22,23,24],"strong",{},"モデルはなぜこの選択をしたのか。",[16,26,27],{},"答えは一つのログにはありません。ユーザーの目標、コンテキストの組み立て、モデルの判断、ツールの実行、最終結果までのチェーン全体にあります。",[29,30,31],"h2",{"id":31},"まず四種類の失敗を区別する",[16,33,34],{},"すべての失敗を「モデルの性能が悪い」と記録してしまうと、チームは具体的な行動を取れません。少なくとも次の四つを区別する必要があります。",[36,37,39],"h3",{"id":38},"_1-入力の失敗","1. 入力の失敗",[16,41,42],{},"ユーザーの目標が不完全、またはタスクの完了に必要なコンテキストをシステムが取得できていません。この場合、モデルをアップグレードしても通常は解決しません。",[36,44,46],{"id":45},"_2-推論の失敗","2. 推論の失敗",[16,48,49],{},"コンテキストは正しいものの、モデルが誤った経路を選び、制約を見落とし、あるいは早すぎる結論に到達しています。",[36,51,53],{"id":52},"_3-ツールの失敗","3. ツールの失敗",[16,55,56],{},"モデルの選択は正しいものの、権限、ネットワーク、パラメータ、外部状態などの理由で実行に失敗しています。",[36,58,60],{"id":59},"_4-伝達の失敗","4. 伝達の失敗",[16,62,63],{},"内部の作業は完了しているのに、最終回答が結果、制約、次のステップを明確に説明できていません。",[16,65,66],{},"これら四つの問題は、プロダクト、プロンプト設計、プラットフォームエンジニアリング、インタラクション設計という異なる担当者を必要とします。可観測性の最初の価値は、問題を正しい人へ戻すことです。",[29,68,70],{"id":69},"一つの実行には共通の-trace-が必要","一つの実行には共通の Trace が必要",[16,72,73,74,78],{},"各ユーザーリクエストには安定した ",[75,76,77],"code",{},"trace_id"," を割り当て、モデル呼び出しとツール実行をつなぎます。",[80,81,86],"pre",{"className":82,"code":83,"language":84,"meta":85,"style":85},"language-ts shiki shiki-themes github-dark github-dark","type AgentEvent = {\n  traceId: string\n  spanId: string\n  parentSpanId?: string\n  type: 'model' | 'tool' | 'handoff' | 'result'\n  startedAt: string\n  durationMs?: number\n  status: 'running' | 'ok' | 'error'\n}\n","ts","",[75,87,88,108,122,132,143,171,181,192,213],{"__ignoreMap":85},[89,90,93,97,101,104],"span",{"class":91,"line":92},"line",1,[89,94,96],{"class":95},"sOPea","type",[89,98,100],{"class":99},"sFR8T"," AgentEvent",[89,102,103],{"class":95}," =",[89,105,107],{"class":106},"suv1-"," {\n",[89,109,111,115,118],{"class":91,"line":110},2,[89,112,114],{"class":113},"s-3mD","  traceId",[89,116,117],{"class":95},":",[89,119,121],{"class":120},"s8ozJ"," string\n",[89,123,125,128,130],{"class":91,"line":124},3,[89,126,127],{"class":113},"  spanId",[89,129,117],{"class":95},[89,131,121],{"class":120},[89,133,135,138,141],{"class":91,"line":134},4,[89,136,137],{"class":113},"  parentSpanId",[89,139,140],{"class":95},"?:",[89,142,121],{"class":120},[89,144,146,149,151,155,158,161,163,166,168],{"class":91,"line":145},5,[89,147,148],{"class":113},"  type",[89,150,117],{"class":95},[89,152,154],{"class":153},"s4wv1"," 'model'",[89,156,157],{"class":95}," |",[89,159,160],{"class":153}," 'tool'",[89,162,157],{"class":95},[89,164,165],{"class":153}," 'handoff'",[89,167,157],{"class":95},[89,169,170],{"class":153}," 'result'\n",[89,172,174,177,179],{"class":91,"line":173},6,[89,175,176],{"class":113},"  startedAt",[89,178,117],{"class":95},[89,180,121],{"class":120},[89,182,184,187,189],{"class":91,"line":183},7,[89,185,186],{"class":113},"  durationMs",[89,188,140],{"class":95},[89,190,191],{"class":120}," number\n",[89,193,195,198,200,203,205,208,210],{"class":91,"line":194},8,[89,196,197],{"class":113},"  status",[89,199,117],{"class":95},[89,201,202],{"class":153}," 'running'",[89,204,157],{"class":95},[89,206,207],{"class":153}," 'ok'",[89,209,157],{"class":95},[89,211,212],{"class":153}," 'error'\n",[89,214,216],{"class":91,"line":215},9,[89,217,218],{"class":106},"}\n",[16,220,221],{},"ツール呼び出しは、モデル判断の外部表現です。呼び出した理由を残さず戻り値だけを記録すると判断コンテキストを失います。プロンプトだけを保存してツール結果を残さなければ、実行が意図どおりだったか判断できません。",[16,223,224],{},"観測は無制限な収集を意味しません。生のプロンプトには個人情報、認証情報、業務データが含まれる可能性があります。より安全な方法は次のとおりです。",[226,227,228,232,235,238,241],"ul",{},[229,230,231],"li",{},"デフォルトでは構造化メタデータを記録する。",[229,233,234],{},"本文はフィールド単位でマスキングする。",[229,236,237],{},"デバッグ用サンプルには短い保存期間を設定する。",[229,239,240],{},"ユーザーや事業者が学習・分析への不参加を明示的に選べるようにする。",[229,242,243],{},"ログにシークレットや完全な個人識別情報を保存しない。",[29,245,246],{"id":246},"品質指標をタスクに近づける",[16,248,249],{},"Token、レイテンシ、モデルエラー率はプラットフォーム指標ですが、ユーザーが結果を得られたかは示せません。",[16,251,252],{},"コード修正 Agent にとって、より意味のある指標は次のようなものです。",[254,255,256,269],"table",{},[257,258,259],"thead",{},[260,261,262,266],"tr",{},[263,264,265],"th",{},"レイヤー",[263,267,268],{},"指標の例",[270,271,272,281,289,297,305],"tbody",{},[260,273,274,278],{},[275,276,277],"td",{},"結果",[275,279,280],{},"影響を受けるテストが通ったか",[260,282,283,286],{},[275,284,285],{},"プロセス",[275,287,288],{},"関連する制約ファイルを読んだか",[260,290,291,294],{},[275,292,293],{},"安全性",[275,295,296],{},"タスク範囲外のファイルを変更していないか",[260,298,299,302],{},[275,300,301],{},"効率",[275,303,304],{},"有効なツール呼び出し \u002F 全ツール呼び出し",[260,306,307,310],{},[275,308,309],{},"伝達",[275,311,312],{},"検証可能なファイルやコミットを提供したか",[16,314,315],{},"単なる呼び出し回数より「有効なツール呼び出し」が重要です。一度の正確な検索が、十回の推測的な読み取りを置き換えることがあります。",[29,317,318],{"id":318},"評価を日常的なフィードバックへ変える",[16,320,321],{},"オフライン benchmark はモデルのバージョン比較には役立ちますが、実際のワークフローすべてを網羅できません。持続可能な評価セットは、本番環境の価値あるサンプルから作ります。",[323,324,325,328,331,334,337],"ol",{},[229,326,327],{},"明確に成功した実行と明確に失敗した実行を集める。",[229,329,330],{},"判断構造を保ちながら個人情報を削除する。",[229,332,333],{},"総合点だけでなく、失敗の種類をラベル付けする。",[229,335,336],{},"プロンプト、モデル、ツールを変更したときに再実行する。",[229,338,339],{},"結果品質、コスト、レイテンシを同時に比較する。",[16,341,342],{},"評価セットは最初から大規模である必要はありません。境界が明確で安定して再現できる二十件のケースは、出所が曖昧な二千件のサンプルよりも、エンジニアリングを導くことがあります。",[29,344,345],{"id":345},"人のための可観測性",[16,347,348],{},"最後の観測レイヤーはダッシュボードではなく、プロダクトの画面にあります。",[16,350,351],{},"Agent がファイルを読み、承認を待ち、デプロイを実行しているとき、ユーザーはどの段階にいるのかを知る必要があります。良いステータス更新は内部思考を垂れ流すのではなく、次を伝えます。",[226,353,354,357,360,363],{},[229,355,356],{},"現在何を完了しようとしているか。",[229,358,359],{},"すでに何を確認したか。",[229,361,362],{},"ユーザーの行動が必要か。",[229,364,365],{},"どのリスクがまだ残っているか。",[16,367,368],{},"この可視性は、システムの信頼性に対するユーザーの判断を直接変えます。数十秒かかっても過程が明確なタスクは、十秒後に突然不透明な結果を返すタスクより安心できることがあります。",[29,370,372],{"id":371},"説明可能であることはすべてを公開することではない","説明可能であることは、すべてを公開することではない",[16,374,375],{},"AI システムは内部のすべての token をユーザーへ見せる必要はなく、機密コンテキストをそのままログに書くべきでもありません。必要なのは、十分に明確な因果チェーンです。",[80,377,382],{"className":378,"code":380,"language":381,"meta":85},[379],"language-text","ユーザーの目標\n  → どのコンテキストを使ったか\n  → どのツールを選んだか\n  → ツールがどの状態を返したか\n  → 最終的にどの結果を届けたか\n","text",[75,383,380],{"__ignoreMap":85},[16,385,386],{},"このチェーンを検索、比較、再現できるようになって初めて、チームは「時々とても賢い」を「長期的に信頼できる」へ変えられます。",[388,389,390],"style",{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":85,"searchDepth":124,"depth":124,"links":392},[393,399,400,401,402,403],{"id":31,"depth":110,"text":31,"children":394},[395,396,397,398],{"id":38,"depth":124,"text":39},{"id":45,"depth":124,"text":46},{"id":52,"depth":124,"text":53},{"id":59,"depth":124,"text":60},{"id":69,"depth":110,"text":70},{"id":246,"depth":110,"text":246},{"id":318,"depth":110,"text":318},{"id":345,"depth":110,"text":345},{"id":371,"depth":110,"text":372},"プロンプト、ツール呼び出し、ユーザーに見える結果まで、AI アプリケーションには「なぜ」を説明できる観測チェーンが必要です。",false,null,"md",{"src":409,"alt":410,"caption":411,"width":412,"height":413},"\u002Fmedia\u002Fposts\u002Fobservable-ai-systems\u002Fcover-v1.webp","AI リクエストが入力からモデルとツールのノードを通り、完全な観測チェーンを形成している","プロンプトから結果までを説明できる Trace",1600,900,{},true,"\u002Fblog\u002Fobservable-ai-systems","2026-07-09",{"title":10,"description":404},"blog\u002Fobservable-ai-systems",[421,422,423],"AI","可観測性","エンジニアリング","t0LYdTk81d8m4r5OxPnfA9rpTh4e3hfr1sF49X_xm5o",[426,2903,4619,4876,5330,5732],{"id":427,"title":428,"accent":429,"body":430,"description":2880,"draft":405,"experience":406,"extension":407,"featured":415,"image":2881,"meta":2885,"navigation":415,"path":2886,"publishedAt":2887,"readingTime":1060,"seo":2888,"stem":2889,"tags":2890,"updatedAt":406,"video":2895,"__hash__":2902,"contentLocale":6,"isFallback":405},"posts_ja\u002Fblog\u002Fcloudflare-email-service-send-receive-guide.md","Cloudflare Email Service 送受信実践ガイド—Routing、Workers、API、SMTP まで","cyan",{"type":13,"value":431,"toc":2847},[432,439,442,453,473,480,483,489,493,496,547,550,556,559,565,572,578,582,585,589,592,595,615,619,622,625,639,642,646,649,653,656,667,670,678,685,702,705,716,719,725,728,732,735,751,754,761,771,1134,1144,1238,1254,1258,1265,1400,1403,1420,1424,1450,1453,1474,1477,1481,1484,1491,1561,1567,1570,1589,1595,1599,1810,1816,1820,1823,2147,2150,2156,2161,2165,2168,2221,2224,2227,2248,2251,2254,2376,2379,2382,2385,2388,2394,2397,2420,2431,2434,2437,2440,2446,2449,2455,2459,2462,2487,2490,2510,2513,2517,2528,2534,2537,2540,2561,2564,2625,2628,2631,2663,2666,2669,2689,2692,2709,2712,2715,2718,2721,2724,2745,2748,2751,2844],[16,433,434,435,438],{},"長い間、Cloudflare とメールの関係は主に二つでした。DNS を管理することと、Email Routing で ",[75,436,437],{},"support@example.com"," を実際のメールボックスへ転送することです。",[16,440,441],{},"2026 年、この境界は大きく前へ進みました。",[16,443,444,445,448,449,452],{},"Cloudflare は 4 月に ",[22,446,447],{},"Email Sending"," を public beta へ移行し、Workers から ",[75,450,451],{},"env.EMAIL.send()"," を直接呼んでトランザクションメールを送れるようにしました。6 月には認証 SMTP も追加されました。従来の Email Routing と Email Workers を組み合わせると、Cloudflare Email Service は現在、次の処理を行えます。",[226,454,455,458,461,464,467,470],{},[229,456,457],{},"独自ドメイン宛てのメールを受信する。",[229,459,460],{},"検証済みの実メールボックスへ転送する。",[229,462,463],{},"受信メールを Worker に渡し、解析、フィルター、保存、自動化を行う。",[229,465,466],{},"元のメールスレッド内で返信する。",[229,468,469],{},"Worker、REST API、SMTP からトランザクションメールを送る。",[229,471,472],{},"送信ログ、bounce、suppression、配信指標を確認する。",[16,474,475,476,479],{},"この記事は ",[22,477,478],{},"2026 年 7 月 28 日","時点の公式機能を基準に、Cloudflare のメール送受信モデル全体を整理し、Nuxt、Workers、D1 プロジェクトに適した実装方法を示します。",[16,481,482],{},"先に結論を述べます。",[484,485,486],"blockquote",{},[16,487,488],{},"Cloudflare Email Service は従来型の IMAP \u002F POP3 メールボックスでも、Web 画面でメールを読む企業向けスイートでもありません。「メールの入口、送信チャネル、プログラム可能なイベント」の集合として捉えると分かりやすく、認証コード、パスワードリセット、注文通知、システムアラート、サポートチケット、メール駆動の自動化に適しています。",[29,490,492],{"id":491},"最初に受信と送信を分ける","最初に「受信」と「送信」を分ける",[16,494,495],{},"メールの送受信は一つの機能に見えますが、Cloudflare では独立した二つの経路です。",[254,497,498,514],{},[257,499,500],{},[260,501,502,505,508,511],{},[263,503,504],{},"方向",[263,506,507],{},"Cloudflare 製品",[263,509,510],{},"入口",[263,512,513],{},"Worker でできること",[270,515,516,530],{},[260,517,518,521,524,527],{},[275,519,520],{},"受信",[275,522,523],{},"Email Routing \u002F Email Workers",[275,525,526],{},"外部メールサーバーが Cloudflare MX へ接続",[275,528,529],{},"転送、返信、拒否、解析、D1\u002FR2 への保存、Queue への投入",[260,531,532,535,537,540],{},[275,533,534],{},"送信",[275,536,447],{},[275,538,539],{},"Workers binding、REST API、認証 SMTP",[275,541,542,543,546],{},"text\u002FHTML、添付、カスタム header を送り、",[75,544,545],{},"messageId"," を取得",[16,548,549],{},"受信経路はおおよそ次のとおりです。",[80,551,554],{"className":552,"code":553,"language":381,"meta":85},[379],"送信元 SMTP\n  → Cloudflare MX\n  → SPF \u002F DKIM \u002F DMARC と reputation の確認\n  → アドレスルールまたは Catch-all に一致\n  → メールボックスへ転送 \u002F Worker \u002F drop\n",[75,555,553],{"__ignoreMap":85},[16,557,558],{},"送信経路は次のとおりです。",[80,560,563],{"className":561,"code":562,"language":381,"meta":85},[379],"Nuxt API \u002F Worker \u002F 外部アプリケーション\n  → EMAIL binding \u002F REST API \u002F SMTP\n  → 送信ドメインとパラメータの検証\n  → Cloudflare 配信パイプライン\n  → 受信側メールサーバー\n",[75,564,562],{"__ignoreMap":85},[16,566,567],{},[568,569],"img",{"alt":570,"src":571},"メールはエッジ入口で検査され、実メールボックス、Worker 処理ノード、拒否経路へ振り分けられる","\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Finbound-routing-v1.webp",[16,573,574],{},[575,576,577],"em",{},"これは概念図です。実際の判断には SMTP セッション、メール認証、ルールマッチ、Worker 実行制限も含まれます。",[29,579,581],{"id":580},"_2026-年の製品境界と料金","2026 年の製品境界と料金",[16,583,584],{},"Cloudflare は現在、受信と送信を Email Service の下にまとめていますが、利用できるプランは異なります。",[36,586,588],{"id":587},"email-routing受信と転送","Email Routing：受信と転送",[16,590,591],{},"Email Routing は Workers Free と Workers Paid の両方で利用できます。公式料金ページでは受信メール数は無制限とされていますが、Worker が処理するメールは対応する Worker リクエストと計算資源を消費します。",[16,593,594],{},"適している用途は次のとおりです。",[226,596,597,603,606,612],{},[229,598,599,602],{},[75,600,601],{},"support@yourdomain.com"," を既存の個人メールボックスへ転送する。",[229,604,605],{},"サイトごとに独立した連絡先アドレスを作る。",[229,607,608,611],{},[75,609,610],{},"orders+shop-a@yourdomain.com"," のような sub-address で送信元を区別する。",[229,613,614],{},"受信メールを Worker でチケット、Webhook、データベースレコードへ変換する。",[36,616,618],{"id":617},"email-sendingトランザクションメール","Email Sending：トランザクションメール",[16,620,621],{},"Email Sending は現在も public beta です。",[16,623,624],{},"任意の受信者への送信には Workers Paid プランが必要です。2026 年 7 月の公式料金では、",[226,626,627,630,633,636],{},[229,628,629],{},"アカウントごとに月 3,000 通の送信が含まれる。",[229,631,632],{},"超過後は 1,000 通あたり 0.35 ドル。",[229,634,635],{},"アカウント内の「検証済み宛先アドレス」へのメールは常に無料で、含まれる枠を消費しない。",[229,637,638],{},"Email Service に受理されたメールは、後で hard bounce しても通常は枠を消費する。API 境界で直接拒否されたリクエストは消費しない。",[16,640,641],{},"料金と beta 状態は今後も変わる可能性があります。本番導入前に公式料金ページを再確認し、この記事の数値を予算モデルへ固定しないでください。",[29,643,645],{"id":644},"第-1-歩ドメインを正しく接続する","第 1 歩：ドメインを正しく接続する",[16,647,648],{},"Email Service は Cloudflare DNS を利用するドメインを必要とします。送信と受信では用途の異なる二組の DNS record が作られます。",[36,650,652],{"id":651},"受信はルートドメインの-record-を使う","受信はルートドメインの record を使う",[16,654,655],{},"Email Routing を有効にすると、Cloudflare はドメインルートに次を設定します。",[226,657,658,661,664],{},[229,659,660],{},"MX：受信メールを Cloudflare のメール入口へ届ける。",[229,662,663],{},"SPF：Cloudflare による転送を許可する。",[229,665,666],{},"DKIM：転送経路へ署名を付与する。",[16,668,669],{},"これらは「第三者が自分のドメインへメールを送る」経路を担当します。",[36,671,673,674,677],{"id":672},"送信は-cf-bounce-record-を使う","送信は ",[75,675,676],{},"cf-bounce"," record を使う",[16,679,680,681,684],{},"Email Sending を有効にすると、Cloudflare は ",[75,682,683],{},"cf-bounce.yourdomain.com"," に次を設定します。",[226,686,687,690,693,696],{},[229,688,689],{},"MX：bounce を処理する。",[229,691,692],{},"SPF：Cloudflare がドメインを代理して送信することを許可する。",[229,694,695],{},"DKIM：メール内容が転送中に変更されていないことを検証する。",[229,697,698,701],{},[75,699,700],{},"_dmarc.yourdomain.com","：認証失敗時の処理方針を宣言する。",[16,703,704],{},"送信と受信は異なる DKIM selector を使います。DKIM record が二つあっても重複設定ではありません。",[16,706,707,708,711,712,715],{},"Google Workspace、Microsoft 365、その他のメールサービスをすでに使っている場合は、MX と SPF の競合を特に確認してください。一つのドメインに有効な SPF policy は一つだけです。複数サービスを同時に許可する場合は ",[75,709,710],{},"include"," を統合し、二つ目の ",[75,713,714],{},"v=spf1"," を作らないでください。",[16,717,718],{},"reputation の用途ごとに subdomain を分けることを勧めます。",[80,720,723],{"className":721,"code":722,"language":381,"meta":85},[379],"noreply@example.com          アカウント認証とパスワードリセット\nnotify@mail.example.com      プロダクト通知\nsupport@example.com          人によるサポートと受信処理\n",[75,724,722],{"__ignoreMap":85},[16,726,727],{},"トランザクション通知、人とのやり取り、将来の一括メールが互いの reputation を損なわず、障害と移行の境界も明確になります。",[29,729,731],{"id":730},"メール受信転送ルールから-email-worker-まで","メール受信：転送ルールから Email Worker まで",[16,733,734],{},"最も簡単な受信方法にはコードが不要です。",[323,736,737,740,745,748],{},[229,738,739],{},"Email Routing に検証済みの宛先メールボックスを追加する。",[229,741,742,744],{},[75,743,601],{}," を作成する。",[229,746,747],{},"「メールへ送信」を選択する。",[229,749,750],{},"Cloudflare が受信メールを宛先へ転送する。",[16,752,753],{},"件名で分類し、添付を抽出し、チケットを作り、自動返信したい場合は、action を「Worker へ送信」に変えます。",[36,755,757,760],{"id":756},"email-が受信メールの入口",[75,758,759],{},"email()"," が受信メールの入口",[16,762,763,764,767,768,770],{},"Email Worker の入口は ",[75,765,766],{},"fetch()"," ではなく ",[75,769,759],{}," です。",[80,772,774],{"className":82,"code":773,"language":84,"meta":85,"style":85},"interface Env {\n  SUPPORT_FORWARD_TO: string\n}\n\nexport default {\n  async email(message, env): Promise\u003Cvoid> {\n    const recipient = message.to.toLowerCase()\n    const subject = message.headers.get('subject') ?? ''\n\n    if (message.rawSize > 10 * 1024 * 1024) {\n      message.setReject('Message is too large for this mailbox')\n      return\n    }\n\n    if (recipient === 'support@example.com') {\n      console.log('Support email accepted', {\n        fromDomain: message.from.split('@').at(-1),\n        subjectLength: subject.length,\n        rawSize: message.rawSize,\n      })\n\n      await message.forward(env.SUPPORT_FORWARD_TO)\n      return\n    }\n\n    message.setReject('Unknown recipient')\n  },\n} satisfies ExportedHandler\u003CEnv>\n",[75,775,776,786,795,799,804,814,851,870,899,903,931,948,954,960,965,981,998,1029,1041,1047,1053,1058,1078,1083,1088,1093,1108,1114],{"__ignoreMap":85},[89,777,778,781,784],{"class":91,"line":92},[89,779,780],{"class":95},"interface",[89,782,783],{"class":99}," Env",[89,785,107],{"class":106},[89,787,788,791,793],{"class":91,"line":110},[89,789,790],{"class":113},"  SUPPORT_FORWARD_TO",[89,792,117],{"class":95},[89,794,121],{"class":120},[89,796,797],{"class":91,"line":124},[89,798,218],{"class":106},[89,800,801],{"class":91,"line":134},[89,802,803],{"emptyLinePlaceholder":415},"\n",[89,805,806,809,812],{"class":91,"line":145},[89,807,808],{"class":95},"export",[89,810,811],{"class":95}," default",[89,813,107],{"class":106},[89,815,816,819,822,825,828,831,834,837,839,842,845,848],{"class":91,"line":173},[89,817,818],{"class":95},"  async",[89,820,821],{"class":99}," email",[89,823,824],{"class":106},"(",[89,826,827],{"class":113},"message",[89,829,830],{"class":106},", ",[89,832,833],{"class":113},"env",[89,835,836],{"class":106},")",[89,838,117],{"class":95},[89,840,841],{"class":99}," Promise",[89,843,844],{"class":106},"\u003C",[89,846,847],{"class":120},"void",[89,849,850],{"class":106},"> {\n",[89,852,853,856,859,861,864,867],{"class":91,"line":183},[89,854,855],{"class":95},"    const",[89,857,858],{"class":120}," recipient",[89,860,103],{"class":95},[89,862,863],{"class":106}," message.to.",[89,865,866],{"class":99},"toLowerCase",[89,868,869],{"class":106},"()\n",[89,871,872,874,877,879,882,885,887,890,893,896],{"class":91,"line":194},[89,873,855],{"class":95},[89,875,876],{"class":120}," subject",[89,878,103],{"class":95},[89,880,881],{"class":106}," message.headers.",[89,883,884],{"class":99},"get",[89,886,824],{"class":106},[89,888,889],{"class":153},"'subject'",[89,891,892],{"class":106},") ",[89,894,895],{"class":95},"??",[89,897,898],{"class":153}," ''\n",[89,900,901],{"class":91,"line":215},[89,902,803],{"emptyLinePlaceholder":415},[89,904,906,909,912,915,918,921,924,926,928],{"class":91,"line":905},10,[89,907,908],{"class":95},"    if",[89,910,911],{"class":106}," (message.rawSize ",[89,913,914],{"class":95},">",[89,916,917],{"class":120}," 10",[89,919,920],{"class":95}," *",[89,922,923],{"class":120}," 1024",[89,925,920],{"class":95},[89,927,923],{"class":120},[89,929,930],{"class":106},") {\n",[89,932,934,937,940,942,945],{"class":91,"line":933},11,[89,935,936],{"class":106},"      message.",[89,938,939],{"class":99},"setReject",[89,941,824],{"class":106},[89,943,944],{"class":153},"'Message is too large for this mailbox'",[89,946,947],{"class":106},")\n",[89,949,951],{"class":91,"line":950},12,[89,952,953],{"class":95},"      return\n",[89,955,957],{"class":91,"line":956},13,[89,958,959],{"class":106},"    }\n",[89,961,963],{"class":91,"line":962},14,[89,964,803],{"emptyLinePlaceholder":415},[89,966,968,970,973,976,979],{"class":91,"line":967},15,[89,969,908],{"class":95},[89,971,972],{"class":106}," (recipient ",[89,974,975],{"class":95},"===",[89,977,978],{"class":153}," 'support@example.com'",[89,980,930],{"class":106},[89,982,984,987,990,992,995],{"class":91,"line":983},16,[89,985,986],{"class":106},"      console.",[89,988,989],{"class":99},"log",[89,991,824],{"class":106},[89,993,994],{"class":153},"'Support email accepted'",[89,996,997],{"class":106},", {\n",[89,999,1001,1004,1007,1009,1012,1015,1018,1020,1023,1026],{"class":91,"line":1000},17,[89,1002,1003],{"class":106},"        fromDomain: message.from.",[89,1005,1006],{"class":99},"split",[89,1008,824],{"class":106},[89,1010,1011],{"class":153},"'@'",[89,1013,1014],{"class":106},").",[89,1016,1017],{"class":99},"at",[89,1019,824],{"class":106},[89,1021,1022],{"class":95},"-",[89,1024,1025],{"class":120},"1",[89,1027,1028],{"class":106},"),\n",[89,1030,1032,1035,1038],{"class":91,"line":1031},18,[89,1033,1034],{"class":106},"        subjectLength: subject.",[89,1036,1037],{"class":120},"length",[89,1039,1040],{"class":106},",\n",[89,1042,1044],{"class":91,"line":1043},19,[89,1045,1046],{"class":106},"        rawSize: message.rawSize,\n",[89,1048,1050],{"class":91,"line":1049},20,[89,1051,1052],{"class":106},"      })\n",[89,1054,1056],{"class":91,"line":1055},21,[89,1057,803],{"emptyLinePlaceholder":415},[89,1059,1061,1064,1067,1070,1073,1076],{"class":91,"line":1060},22,[89,1062,1063],{"class":95},"      await",[89,1065,1066],{"class":106}," message.",[89,1068,1069],{"class":99},"forward",[89,1071,1072],{"class":106},"(env.",[89,1074,1075],{"class":120},"SUPPORT_FORWARD_TO",[89,1077,947],{"class":106},[89,1079,1081],{"class":91,"line":1080},23,[89,1082,953],{"class":95},[89,1084,1086],{"class":91,"line":1085},24,[89,1087,959],{"class":106},[89,1089,1091],{"class":91,"line":1090},25,[89,1092,803],{"emptyLinePlaceholder":415},[89,1094,1096,1099,1101,1103,1106],{"class":91,"line":1095},26,[89,1097,1098],{"class":106},"    message.",[89,1100,939],{"class":99},[89,1102,824],{"class":106},[89,1104,1105],{"class":153},"'Unknown recipient'",[89,1107,947],{"class":106},[89,1109,1111],{"class":91,"line":1110},27,[89,1112,1113],{"class":106},"  },\n",[89,1115,1117,1120,1123,1126,1128,1131],{"class":91,"line":1116},28,[89,1118,1119],{"class":106},"} ",[89,1121,1122],{"class":95},"satisfies",[89,1124,1125],{"class":99}," ExportedHandler",[89,1127,844],{"class":106},[89,1129,1130],{"class":99},"Env",[89,1132,1133],{"class":106},">\n",[16,1135,1136,1137,1139,1140,1143],{},"ここで ",[75,1138,827],{}," は ",[75,1141,1142],{},"ForwardableEmailMessage"," です。よく使う field と操作は次のとおりです。",[254,1145,1146,1156],{},[257,1147,1148],{},[260,1149,1150,1153],{},[263,1151,1152],{},"機能",[263,1154,1155],{},"用途",[270,1157,1158,1168,1178,1188,1198,1208,1218,1228],{},[260,1159,1160,1165],{},[275,1161,1162],{},[75,1163,1164],{},"message.from",[275,1166,1167],{},"SMTP envelope の送信者",[260,1169,1170,1175],{},[275,1171,1172],{},[75,1173,1174],{},"message.to",[275,1176,1177],{},"SMTP envelope の受信者",[260,1179,1180,1185],{},[275,1181,1182],{},[75,1183,1184],{},"message.headers",[275,1186,1187],{},"Subject、Message-ID などの header を読む",[260,1189,1190,1195],{},[275,1191,1192],{},[75,1193,1194],{},"message.raw",[275,1196,1197],{},"生の MIME stream",[260,1199,1200,1205],{},[275,1201,1202],{},[75,1203,1204],{},"message.rawSize",[275,1206,1207],{},"生メールのサイズ",[260,1209,1210,1215],{},[275,1211,1212],{},[75,1213,1214],{},"message.forward(address)",[275,1216,1217],{},"検証済み宛先へ転送する",[260,1219,1220,1225],{},[275,1221,1222],{},[75,1223,1224],{},"message.reply(email)",[275,1226,1227],{},"制約の範囲内で元の送信者へ返信する",[260,1229,1230,1235],{},[275,1231,1232],{},[75,1233,1234],{},"message.setReject(reason)",[275,1236,1237],{},"永続的な SMTP rejection を返す",[16,1239,1240,1241,1243,1244,1246,1247,1243,1250,1253],{},"見落としやすいセキュリティ上の注意があります。",[75,1242,1164],{}," \u002F ",[75,1245,1174],{}," は SMTP envelope の値で、メール本文 header 内の ",[75,1248,1249],{},"From:",[75,1251,1252],{},"To:"," とは異なる場合があります。認可、routing、audit では、ユーザーに表示する header アドレスを信頼できる identity と誤認しないでください。",[36,1255,1257],{"id":1256},"本文が必要なときだけ-mime-を解析する","本文が必要なときだけ MIME を解析する",[16,1259,1260,1261,1264],{},"転送またはアドレスによる routing だけなら、メール全体を解析する必要はありません。本文と添付を取り出す場合は ",[75,1262,1263],{},"postal-mime"," を使えます。",[80,1266,1268],{"className":82,"code":1267,"language":84,"meta":85,"style":85},"import PostalMime from 'postal-mime'\n\nexport default {\n  async email(message): Promise\u003Cvoid> {\n    const parsed = await PostalMime.parse(message.raw)\n\n    console.log({\n      subject: parsed.subject,\n      textLength: parsed.text?.length ?? 0,\n      attachmentCount: parsed.attachments.length,\n    })\n  },\n} satisfies ExportedHandler\n",[75,1269,1270,1284,1288,1296,1318,1339,1343,1353,1358,1373,1382,1387,1391],{"__ignoreMap":85},[89,1271,1272,1275,1278,1281],{"class":91,"line":92},[89,1273,1274],{"class":95},"import",[89,1276,1277],{"class":106}," PostalMime ",[89,1279,1280],{"class":95},"from",[89,1282,1283],{"class":153}," 'postal-mime'\n",[89,1285,1286],{"class":91,"line":110},[89,1287,803],{"emptyLinePlaceholder":415},[89,1289,1290,1292,1294],{"class":91,"line":124},[89,1291,808],{"class":95},[89,1293,811],{"class":95},[89,1295,107],{"class":106},[89,1297,1298,1300,1302,1304,1306,1308,1310,1312,1314,1316],{"class":91,"line":134},[89,1299,818],{"class":95},[89,1301,821],{"class":99},[89,1303,824],{"class":106},[89,1305,827],{"class":113},[89,1307,836],{"class":106},[89,1309,117],{"class":95},[89,1311,841],{"class":99},[89,1313,844],{"class":106},[89,1315,847],{"class":120},[89,1317,850],{"class":106},[89,1319,1320,1322,1325,1327,1330,1333,1336],{"class":91,"line":145},[89,1321,855],{"class":95},[89,1323,1324],{"class":120}," parsed",[89,1326,103],{"class":95},[89,1328,1329],{"class":95}," await",[89,1331,1332],{"class":106}," PostalMime.",[89,1334,1335],{"class":99},"parse",[89,1337,1338],{"class":106},"(message.raw)\n",[89,1340,1341],{"class":91,"line":173},[89,1342,803],{"emptyLinePlaceholder":415},[89,1344,1345,1348,1350],{"class":91,"line":183},[89,1346,1347],{"class":106},"    console.",[89,1349,989],{"class":99},[89,1351,1352],{"class":106},"({\n",[89,1354,1355],{"class":91,"line":194},[89,1356,1357],{"class":106},"      subject: parsed.subject,\n",[89,1359,1360,1363,1365,1368,1371],{"class":91,"line":215},[89,1361,1362],{"class":106},"      textLength: parsed.text?.",[89,1364,1037],{"class":120},[89,1366,1367],{"class":95}," ??",[89,1369,1370],{"class":120}," 0",[89,1372,1040],{"class":106},[89,1374,1375,1378,1380],{"class":91,"line":905},[89,1376,1377],{"class":106},"      attachmentCount: parsed.attachments.",[89,1379,1037],{"class":120},[89,1381,1040],{"class":106},[89,1383,1384],{"class":91,"line":933},[89,1385,1386],{"class":106},"    })\n",[89,1388,1389],{"class":91,"line":950},[89,1390,1113],{"class":106},[89,1392,1393,1395,1397],{"class":91,"line":956},[89,1394,1119],{"class":106},[89,1396,1122],{"class":95},[89,1398,1399],{"class":99}," ExportedHandler\n",[16,1401,1402],{},"解析後の HTML、ファイル名、Content-Type、添付はすべて外部入力です。安全な方法は次のとおりです。",[226,1404,1405,1408,1411,1414,1417],{},[229,1406,1407],{},"管理画面へ受信 HTML を直接挿入しない。",[229,1409,1410],{},"本文、添付数、展開後サイズを制限する。",[229,1412,1413],{},"R2 へ保存する前に新しい object key を生成し、元のファイル名を path に使わない。",[229,1415,1416],{},"メール本文から SQL、Shell、template、AI ツールパラメータを直接組み立てない。",[229,1418,1419],{},"virus scan、OCR、モデル呼び出しを受信イベントの同期 CPU 時間へ詰め込まず、重い処理は Queue へ渡す。",[36,1421,1423],{"id":1422},"転送返信拒否silent-drop-の違い","転送、返信、拒否、silent drop の違い",[226,1425,1426,1432,1438,1444],{},[229,1427,1428,1431],{},[22,1429,1430],{},"転送","：人のメールボックスで処理を続ける用途。宛先は事前に検証が必要。",[229,1433,1434,1437],{},[22,1435,1436],{},"返信","：自動確認やメール bot に適するが、追加制約がある。",[229,1439,1440,1443],{},[22,1441,1442],{},"拒否","：SMTP 段階で送信元へ受け付けなかったことを伝える。",[229,1445,1446,1449],{},[22,1447,1448],{},"drop","：ルール一致後に処理を止め、送信元へ業務レベルの応答を返さない。",[16,1451,1452],{},"自動返信を任意の送信 interface として使うことはできません。現在の公式要件は次のとおりです。",[226,1454,1455,1458,1461,1464,1467],{},[229,1456,1457],{},"受信メールに有効な DMARC 結果がある。",[229,1459,1460],{},"一つの受信イベントにつき返信は一度だけ。",[229,1462,1463],{},"返信先は元の送信者である。",[229,1465,1466],{},"返信の送信ドメインは受信したドメインと同じである。",[229,1468,1469,1470,1473],{},"メールループ防止のため、",[75,1471,1472],{},"References"," が 100 項目を超えると返信を拒否する。",[16,1475,1476],{},"この制約はむしろ有益です。「このメールへ返信する」と「任意のアドレスへ新規メールを送る」が異なる権限になります。",[29,1478,1480],{"id":1479},"メール送信workers-binding-を優先する","メール送信：Workers binding を優先する",[16,1482,1483],{},"アプリケーションが Cloudflare Workers 上で動いているなら、Workers binding が最も自然です。業務コードに Cloudflare API Token を保存する必要がありません。",[16,1485,1486,1487,1490],{},"まず ",[75,1488,1489],{},"wrangler.jsonc"," で binding を宣言します。",[80,1492,1496],{"className":1493,"code":1494,"language":1495,"meta":85,"style":85},"language-jsonc shiki shiki-themes github-dark github-dark","{\n  \"send_email\": [\n    {\n      \"name\": \"EMAIL\",\n      \"allowed_sender_addresses\": [\n        \"noreply@example.com\"\n      ]\n    }\n  ],\n  \"vars\": {\n    \"EMAIL_FROM\": \"noreply@example.com\"\n  }\n}\n","jsonc",[75,1497,1498,1503,1508,1513,1518,1523,1528,1533,1537,1542,1547,1552,1557],{"__ignoreMap":85},[89,1499,1500],{"class":91,"line":92},[89,1501,1502],{},"{\n",[89,1504,1505],{"class":91,"line":110},[89,1506,1507],{},"  \"send_email\": [\n",[89,1509,1510],{"class":91,"line":124},[89,1511,1512],{},"    {\n",[89,1514,1515],{"class":91,"line":134},[89,1516,1517],{},"      \"name\": \"EMAIL\",\n",[89,1519,1520],{"class":91,"line":145},[89,1521,1522],{},"      \"allowed_sender_addresses\": [\n",[89,1524,1525],{"class":91,"line":173},[89,1526,1527],{},"        \"noreply@example.com\"\n",[89,1529,1530],{"class":91,"line":183},[89,1531,1532],{},"      ]\n",[89,1534,1535],{"class":91,"line":194},[89,1536,959],{},[89,1538,1539],{"class":91,"line":215},[89,1540,1541],{},"  ],\n",[89,1543,1544],{"class":91,"line":905},[89,1545,1546],{},"  \"vars\": {\n",[89,1548,1549],{"class":91,"line":933},[89,1550,1551],{},"    \"EMAIL_FROM\": \"noreply@example.com\"\n",[89,1553,1554],{"class":91,"line":950},[89,1555,1556],{},"  }\n",[89,1558,1559],{"class":91,"line":956},[89,1560,218],{},[16,1562,1563,1566],{},[75,1564,1565],{},"allowed_sender_addresses"," は説明用の設定ではなく、実行時の権限境界です。endpoint が誤用されても、この binding は別の送信者アドレスになりすませません。",[16,1568,1569],{},"次の設定で宛先も制限できます。",[226,1571,1572,1578,1584],{},[229,1573,1574,1577],{},[75,1575,1576],{},"destination_address","：宛先を一つに固定する。",[229,1579,1580,1583],{},[75,1581,1582],{},"allowed_destination_addresses","：宛先を許可リストに限定する。",[229,1585,1586,1588],{},[75,1587,1565],{},"：利用できる送信元アドレスを制限する。",[16,1590,1591,1592,1594],{},"システムアラート用 Worker は ",[75,1593,1576],{}," の固定に適しています。ユーザー認証サービスでは通常、送信者を制限し、アプリ側で宛先と頻度を検証します。",[36,1596,1598],{"id":1597},"worker-内から送信する","Worker 内から送信する",[80,1600,1602],{"className":82,"code":1601,"language":84,"meta":85,"style":85},"interface Env {\n  EMAIL: SendEmail\n}\n\nexport default {\n  async fetch(_request, env): Promise\u003CResponse> {\n    const result = await env.EMAIL.send({\n      from: {\n        email: 'noreply@example.com',\n        name: 'Example App',\n      },\n      to: 'reader@example.net',\n      subject: 'メールアドレスを確認してください',\n      text: '認証コードは 482913 です。5 分間有効です。',\n      html: '\u003Cp>認証コードは \u003Cstrong>482913\u003C\u002Fstrong> です。5 分間有効です。\u003C\u002Fp>',\n    })\n\n    return Response.json({\n      messageId: result.messageId,\n    })\n  },\n} satisfies ExportedHandler\u003CEnv>\n",[75,1603,1604,1612,1622,1626,1630,1638,1667,1692,1697,1707,1717,1722,1732,1742,1752,1762,1766,1770,1783,1788,1792,1796],{"__ignoreMap":85},[89,1605,1606,1608,1610],{"class":91,"line":92},[89,1607,780],{"class":95},[89,1609,783],{"class":99},[89,1611,107],{"class":106},[89,1613,1614,1617,1619],{"class":91,"line":110},[89,1615,1616],{"class":113},"  EMAIL",[89,1618,117],{"class":95},[89,1620,1621],{"class":99}," SendEmail\n",[89,1623,1624],{"class":91,"line":124},[89,1625,218],{"class":106},[89,1627,1628],{"class":91,"line":134},[89,1629,803],{"emptyLinePlaceholder":415},[89,1631,1632,1634,1636],{"class":91,"line":145},[89,1633,808],{"class":95},[89,1635,811],{"class":95},[89,1637,107],{"class":106},[89,1639,1640,1642,1645,1647,1650,1652,1654,1656,1658,1660,1662,1665],{"class":91,"line":173},[89,1641,818],{"class":95},[89,1643,1644],{"class":99}," fetch",[89,1646,824],{"class":106},[89,1648,1649],{"class":113},"_request",[89,1651,830],{"class":106},[89,1653,833],{"class":113},[89,1655,836],{"class":106},[89,1657,117],{"class":95},[89,1659,841],{"class":99},[89,1661,844],{"class":106},[89,1663,1664],{"class":99},"Response",[89,1666,850],{"class":106},[89,1668,1669,1671,1674,1676,1678,1681,1684,1687,1690],{"class":91,"line":183},[89,1670,855],{"class":95},[89,1672,1673],{"class":120}," result",[89,1675,103],{"class":95},[89,1677,1329],{"class":95},[89,1679,1680],{"class":106}," env.",[89,1682,1683],{"class":120},"EMAIL",[89,1685,1686],{"class":106},".",[89,1688,1689],{"class":99},"send",[89,1691,1352],{"class":106},[89,1693,1694],{"class":91,"line":194},[89,1695,1696],{"class":106},"      from: {\n",[89,1698,1699,1702,1705],{"class":91,"line":215},[89,1700,1701],{"class":106},"        email: ",[89,1703,1704],{"class":153},"'noreply@example.com'",[89,1706,1040],{"class":106},[89,1708,1709,1712,1715],{"class":91,"line":905},[89,1710,1711],{"class":106},"        name: ",[89,1713,1714],{"class":153},"'Example App'",[89,1716,1040],{"class":106},[89,1718,1719],{"class":91,"line":933},[89,1720,1721],{"class":106},"      },\n",[89,1723,1724,1727,1730],{"class":91,"line":950},[89,1725,1726],{"class":106},"      to: ",[89,1728,1729],{"class":153},"'reader@example.net'",[89,1731,1040],{"class":106},[89,1733,1734,1737,1740],{"class":91,"line":956},[89,1735,1736],{"class":106},"      subject: ",[89,1738,1739],{"class":153},"'メールアドレスを確認してください'",[89,1741,1040],{"class":106},[89,1743,1744,1747,1750],{"class":91,"line":962},[89,1745,1746],{"class":106},"      text: ",[89,1748,1749],{"class":153},"'認証コードは 482913 です。5 分間有効です。'",[89,1751,1040],{"class":106},[89,1753,1754,1757,1760],{"class":91,"line":967},[89,1755,1756],{"class":106},"      html: ",[89,1758,1759],{"class":153},"'\u003Cp>認証コードは \u003Cstrong>482913\u003C\u002Fstrong> です。5 分間有効です。\u003C\u002Fp>'",[89,1761,1040],{"class":106},[89,1763,1764],{"class":91,"line":983},[89,1765,1386],{"class":106},[89,1767,1768],{"class":91,"line":1000},[89,1769,803],{"emptyLinePlaceholder":415},[89,1771,1772,1775,1778,1781],{"class":91,"line":1031},[89,1773,1774],{"class":95},"    return",[89,1776,1777],{"class":106}," Response.",[89,1779,1780],{"class":99},"json",[89,1782,1352],{"class":106},[89,1784,1785],{"class":91,"line":1043},[89,1786,1787],{"class":106},"      messageId: result.messageId,\n",[89,1789,1790],{"class":91,"line":1049},[89,1791,1386],{"class":106},[89,1793,1794],{"class":91,"line":1055},[89,1795,1113],{"class":106},[89,1797,1798,1800,1802,1804,1806,1808],{"class":91,"line":1060},[89,1799,1119],{"class":106},[89,1801,1122],{"class":95},[89,1803,1125],{"class":99},[89,1805,844],{"class":106},[89,1807,1130],{"class":99},[89,1809,1133],{"class":106},[16,1811,1812,1813,1815],{},"Workers binding が成功すると ",[75,1814,545],{}," を返します。これは Cloudflare が送信リクエストを受理したことを意味し、ユーザーの受信トレイにすでに表示されたことを保証しません。最終配信、遅延、bounce、suppression は Email Service の log と metric で確認します。",[36,1817,1819],{"id":1818},"nuxt-のサーバー-api-から送信する","Nuxt のサーバー API から送信する",[16,1821,1822],{},"Nuxt を Workers へデプロイすると、サーバールートから同じ binding を読めます。",[80,1824,1826],{"className":82,"code":1825,"language":84,"meta":85,"style":85},"import { z } from 'zod'\n\nconst schema = z.object({\n  email: z.string().trim().email().max(254),\n})\n\nexport default defineEventHandler(async (event) => {\n  const { email } = schema.parse(await readBody(event))\n  const env = event.context.cloudflare.env as {\n    EMAIL: SendEmail\n    EMAIL_FROM: string\n  }\n\n  \u002F\u002F 実際のプロジェクトでは same-origin を確認し、\n  \u002F\u002F IP とメールアドレスを rate-limit し、D1 で\n  \u002F\u002F 一度の送信資格を atomically に取得します。\n  const result = await env.EMAIL.send({\n    from: {\n      email: env.EMAIL_FROM,\n      name: 'Example App',\n    },\n    to: email,\n    subject: 'メールアドレスの確認',\n    text: '認証コードは 5 分後に失効します。',\n    html: '\u003Cp>認証コードは \u003Cstrong>5 分後\u003C\u002Fstrong>に失効します。\u003C\u002Fp>',\n  })\n\n  return {\n    ok: true,\n    messageId: result.messageId,\n  }\n})\n",[75,1827,1828,1840,1844,1862,1893,1898,1902,1929,1961,1978,1987,1996,2000,2004,2010,2015,2020,2040,2045,2055,2064,2069,2074,2084,2094,2104,2109,2113,2120,2131,2137,2142],{"__ignoreMap":85},[89,1829,1830,1832,1835,1837],{"class":91,"line":92},[89,1831,1274],{"class":95},[89,1833,1834],{"class":106}," { z } ",[89,1836,1280],{"class":95},[89,1838,1839],{"class":153}," 'zod'\n",[89,1841,1842],{"class":91,"line":110},[89,1843,803],{"emptyLinePlaceholder":415},[89,1845,1846,1849,1852,1854,1857,1860],{"class":91,"line":124},[89,1847,1848],{"class":95},"const",[89,1850,1851],{"class":120}," schema",[89,1853,103],{"class":95},[89,1855,1856],{"class":106}," z.",[89,1858,1859],{"class":99},"object",[89,1861,1352],{"class":106},[89,1863,1864,1867,1870,1873,1876,1878,1881,1883,1886,1888,1891],{"class":91,"line":134},[89,1865,1866],{"class":106},"  email: z.",[89,1868,1869],{"class":99},"string",[89,1871,1872],{"class":106},"().",[89,1874,1875],{"class":99},"trim",[89,1877,1872],{"class":106},[89,1879,1880],{"class":99},"email",[89,1882,1872],{"class":106},[89,1884,1885],{"class":99},"max",[89,1887,824],{"class":106},[89,1889,1890],{"class":120},"254",[89,1892,1028],{"class":106},[89,1894,1895],{"class":91,"line":145},[89,1896,1897],{"class":106},"})\n",[89,1899,1900],{"class":91,"line":173},[89,1901,803],{"emptyLinePlaceholder":415},[89,1903,1904,1906,1908,1911,1913,1916,1919,1922,1924,1927],{"class":91,"line":183},[89,1905,808],{"class":95},[89,1907,811],{"class":95},[89,1909,1910],{"class":99}," defineEventHandler",[89,1912,824],{"class":106},[89,1914,1915],{"class":95},"async",[89,1917,1918],{"class":106}," (",[89,1920,1921],{"class":113},"event",[89,1923,892],{"class":106},[89,1925,1926],{"class":95},"=>",[89,1928,107],{"class":106},[89,1930,1931,1934,1937,1939,1942,1945,1948,1950,1952,1955,1958],{"class":91,"line":194},[89,1932,1933],{"class":95},"  const",[89,1935,1936],{"class":106}," { ",[89,1938,1880],{"class":120},[89,1940,1941],{"class":106}," } ",[89,1943,1944],{"class":95},"=",[89,1946,1947],{"class":106}," schema.",[89,1949,1335],{"class":99},[89,1951,824],{"class":106},[89,1953,1954],{"class":95},"await",[89,1956,1957],{"class":99}," readBody",[89,1959,1960],{"class":106},"(event))\n",[89,1962,1963,1965,1968,1970,1973,1976],{"class":91,"line":215},[89,1964,1933],{"class":95},[89,1966,1967],{"class":120}," env",[89,1969,103],{"class":95},[89,1971,1972],{"class":106}," event.context.cloudflare.env ",[89,1974,1975],{"class":95},"as",[89,1977,107],{"class":106},[89,1979,1980,1983,1985],{"class":91,"line":905},[89,1981,1982],{"class":113},"    EMAIL",[89,1984,117],{"class":95},[89,1986,1621],{"class":99},[89,1988,1989,1992,1994],{"class":91,"line":933},[89,1990,1991],{"class":113},"    EMAIL_FROM",[89,1993,117],{"class":95},[89,1995,121],{"class":120},[89,1997,1998],{"class":91,"line":950},[89,1999,1556],{"class":106},[89,2001,2002],{"class":91,"line":956},[89,2003,803],{"emptyLinePlaceholder":415},[89,2005,2006],{"class":91,"line":962},[89,2007,2009],{"class":2008},"sJ8bj","  \u002F\u002F 実際のプロジェクトでは same-origin を確認し、\n",[89,2011,2012],{"class":91,"line":967},[89,2013,2014],{"class":2008},"  \u002F\u002F IP とメールアドレスを rate-limit し、D1 で\n",[89,2016,2017],{"class":91,"line":983},[89,2018,2019],{"class":2008},"  \u002F\u002F 一度の送信資格を atomically に取得します。\n",[89,2021,2022,2024,2026,2028,2030,2032,2034,2036,2038],{"class":91,"line":1000},[89,2023,1933],{"class":95},[89,2025,1673],{"class":120},[89,2027,103],{"class":95},[89,2029,1329],{"class":95},[89,2031,1680],{"class":106},[89,2033,1683],{"class":120},[89,2035,1686],{"class":106},[89,2037,1689],{"class":99},[89,2039,1352],{"class":106},[89,2041,2042],{"class":91,"line":1031},[89,2043,2044],{"class":106},"    from: {\n",[89,2046,2047,2050,2053],{"class":91,"line":1043},[89,2048,2049],{"class":106},"      email: env.",[89,2051,2052],{"class":120},"EMAIL_FROM",[89,2054,1040],{"class":106},[89,2056,2057,2060,2062],{"class":91,"line":1049},[89,2058,2059],{"class":106},"      name: ",[89,2061,1714],{"class":153},[89,2063,1040],{"class":106},[89,2065,2066],{"class":91,"line":1055},[89,2067,2068],{"class":106},"    },\n",[89,2070,2071],{"class":91,"line":1060},[89,2072,2073],{"class":106},"    to: email,\n",[89,2075,2076,2079,2082],{"class":91,"line":1080},[89,2077,2078],{"class":106},"    subject: ",[89,2080,2081],{"class":153},"'メールアドレスの確認'",[89,2083,1040],{"class":106},[89,2085,2086,2089,2092],{"class":91,"line":1085},[89,2087,2088],{"class":106},"    text: ",[89,2090,2091],{"class":153},"'認証コードは 5 分後に失効します。'",[89,2093,1040],{"class":106},[89,2095,2096,2099,2102],{"class":91,"line":1090},[89,2097,2098],{"class":106},"    html: ",[89,2100,2101],{"class":153},"'\u003Cp>認証コードは \u003Cstrong>5 分後\u003C\u002Fstrong>に失効します。\u003C\u002Fp>'",[89,2103,1040],{"class":106},[89,2105,2106],{"class":91,"line":1095},[89,2107,2108],{"class":106},"  })\n",[89,2110,2111],{"class":91,"line":1110},[89,2112,803],{"emptyLinePlaceholder":415},[89,2114,2115,2118],{"class":91,"line":1116},[89,2116,2117],{"class":95},"  return",[89,2119,107],{"class":106},[89,2121,2123,2126,2129],{"class":91,"line":2122},29,[89,2124,2125],{"class":106},"    ok: ",[89,2127,2128],{"class":120},"true",[89,2130,1040],{"class":106},[89,2132,2134],{"class":91,"line":2133},30,[89,2135,2136],{"class":106},"    messageId: result.messageId,\n",[89,2138,2140],{"class":91,"line":2139},31,[89,2141,1556],{"class":106},[89,2143,2145],{"class":91,"line":2144},32,[89,2146,1897],{"class":106},[16,2148,2149],{},"この例を、匿名で無制限に呼べ、件名と本文を自由に指定できる公開 API にしないでください。自分のドメインが spam relay になります。",[16,2151,2152],{},[568,2153],{"alt":2154,"src":2155},"Worker が生成したトランザクションメールが複数の認証レイヤーを通り、異なる受信システムへ配信される","\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Foutbound-delivery-v1.webp",[16,2157,2158],{},[575,2159,2160],{},"SPF、DKIM、DMARC は「誰が送信できるか、内容が変更されていないか、失敗時にどう処理するか」を扱います。すべてのメールがメイン受信トレイへ入ることは保証しません。",[29,2162,2164],{"id":2163},"rest-api-と認証-smtp-が適する場面","REST API と認証 SMTP が適する場面",[16,2166,2167],{},"Cloudflare は現在、三つの送信入口を提供しています。",[254,2169,2170,2183],{},[257,2171,2172],{},[260,2173,2174,2177,2180],{},[263,2175,2176],{},"方法",[263,2178,2179],{},"最適な用途",[263,2181,2182],{},"認証情報と境界",[270,2184,2185,2196,2207],{},[260,2186,2187,2190,2193],{},[275,2188,2189],{},"Workers binding",[275,2191,2192],{},"すでに Workers 上で動く Nuxt、Hono、Agent",[275,2194,2195],{},"コードに API Token を保存せず、binding で送信者と宛先を制限できる",[260,2197,2198,2201,2204],{},[275,2199,2200],{},"REST API",[275,2202,2203],{},"別の cloud、CI、backend platform で動くプログラム",[275,2205,2206],{},"最小権限 API Token を使い、response は受信者ごとに delivered、queued、permanent bounce を区別する",[260,2208,2209,2212,2215],{},[275,2210,2211],{},"認証 SMTP",[275,2213,2214],{},"Nodemailer、PHPMailer、JavaMail などの SMTP client を使う既存システム",[275,2216,2217,2220],{},[75,2218,2219],{},"smtp.mx.cloudflare.net:465","、implicit TLS のみ。password は Email Sending 権限を持つ API Token",[16,2222,2223],{},"認証 SMTP は 2026 年 6 月に追加された beta 機能です。現在は 465 port の SMTPS だけをサポートし、587 port の STARTTLS や匿名利用できる 25 port relay はありません。",[16,2225,2226],{},"SMTP Token が漏れると、所有者はアカウントへ接続された送信ドメインを使ってメールを送れます。そのため、",[226,2228,2229,2232,2239,2242,2245],{},[229,2230,2231],{},"account-owned token を優先する。",[229,2233,2234,2235,2238],{},"権限は ",[75,2236,2237],{},"Email Sending: Edit"," だけにする。",[229,2240,2241],{},"repository、image、frontend environment variable に書かない。",[229,2243,2244],{},"システムごとに別 Token を使い、個別に revoke できるようにする。",[229,2246,2247],{},"Worker binding を使えるなら、「SMTP に慣れている」という理由だけで長期 credential を追加しない。",[29,2249,2250],{"id":2250},"必ず把握すべき制限",[16,2252,2253],{},"2026 年 7 月時点で、アプリ側のコードで事前に防御すべき主な platform limit は次のとおりです。",[254,2255,2256,2270],{},[257,2257,2258],{},[260,2259,2260,2263,2267],{},[263,2261,2262],{},"制限",[263,2264,2266],{"align":2265},"right","現在値",[263,2268,2269],{},"実装への影響",[270,2271,2272,2283,2293,2304,2315,2326,2336,2347,2358],{},[260,2273,2274,2277,2280],{},[275,2275,2276],{},"ドメインごとの Email Routing ルール",[275,2278,2279],{"align":2265},"200",[275,2281,2282],{},"multi-tenant システムでユーザーごとの静的ルールを作らない",[260,2284,2285,2288,2290],{},[275,2286,2287],{},"アカウントごとの検証済み宛先",[275,2289,2279],{"align":2265},[275,2291,2292],{},"多数のメールボックスへ転送する前に account-level の宛先を設計",[260,2294,2295,2298,2301],{},[275,2296,2297],{},"受信メールサイズ",[275,2299,2300],{"align":2265},"25 MiB",[275,2302,2303],{},"それより大きいメールは SMTP 段階で拒否",[260,2305,2306,2309,2312],{},[275,2307,2308],{},"送信メール 1 通あたりの受信者",[275,2310,2311],{"align":2265},"50",[275,2313,2314],{},"To、Cc、Bcc の合計",[260,2316,2317,2320,2323],{},[275,2318,2319],{},"通常の送信メール総サイズ",[275,2321,2322],{"align":2265},"5 MiB",[275,2324,2325],{},"添付と MIME encoding overhead を含む",[260,2327,2328,2331,2333],{},[275,2329,2330],{},"検証済み宛先への総サイズ",[275,2332,2300],{"align":2265},[275,2334,2335],{},"検証済み宛先アドレスにのみ適用",[260,2337,2338,2341,2344],{},[275,2339,2340],{},"カスタム header の総サイズ",[275,2342,2343],{"align":2265},"16 KB",[275,2345,2346],{},"業務データを Header へ詰め込まない",[260,2348,2349,2352,2355],{},[275,2350,2351],{},"1 Zone の送信\u002Frouting ドメイン数",[275,2353,2354],{"align":2265},"30",[275,2356,2357],{},"root domain と subdomain の両方を数える",[260,2359,2360,2366,2369],{},[275,2361,2362,2363,2365],{},"返信の ",[75,2364,1472],{}," 項目数",[275,2367,2368],{"align":2265},"100",[275,2370,2371,2372,2375],{},"超えると ",[75,2373,2374],{},"reply()"," が失敗",[16,2377,2378],{},"添付は Base64 と MIME で包装されます。3 MiB の生ファイルでも最終メールが 3 MiB のままとは限りません。encoding、本文、header の余裕を残してください。",[16,2380,2381],{},"platform は account reputation、配信成績、利用履歴に応じて一日の送信枠を調整することもあります。「月間枠が残っている」ことを、ある一秒間に無制限の並行送信ができるという意味にしないでください。",[29,2383,2384],{"id":2384},"認証コードとパスワードリセットの設計",[16,2386,2387],{},"このブログの登録・パスワードリセットメールは、次の経路を使います。",[80,2389,2392],{"className":2390,"code":2391,"language":381,"meta":85},[379],"ユーザーがメールアドレスを送信\n  → same-origin check\n  → IP とメールアドレスの rate limit\n  → 6 桁コードを生成\n  → D1 は salted hash、用途、有効期限、試行回数だけを保存\n  → EMAIL binding で送信\n  → 認証成功時に atomically に使用済みへ更新\n",[75,2393,2391],{"__ignoreMap":85},[16,2395,2396],{},"重要な規則は次のとおりです。",[226,2398,2399,2402,2405,2408,2411,2414,2417],{},[229,2400,2401],{},"同じメールアドレスには 5 分に 1 通だけ送る。",[229,2403,2404],{},"認証コードは 5 分後に失効する。",[229,2406,2407],{},"コードは一度だけ使用できる。",[229,2409,2410],{},"D1 に plaintext のコードを保存しない。",[229,2412,2413],{},"連続して間違え、しきい値へ達したら認証を止める。",[229,2415,2416],{},"送信失敗時は取得した枠を解放し、届かなかったコードでユーザーを lock しない。",[229,2418,2419],{},"パスワードリセット時は password hash だけを置き換えず、既存のログインセッションを revoke する。",[16,2421,2422,2423,2426,2427,2430],{},"rate limit はデータベース内で送信資格を ",[22,2424,2425],{},"atomically に取得","する必要があります。前回送信時刻を検索してから row を insert する実装では、二つの並行リクエストが両方とも確認を通過します。unique key と ",[75,2428,2429],{},"INSERT ... ON CONFLICT ... WHERE"," を組み合わせ、どのリクエストが送信枠を得るかをデータベースに決めさせます。",[16,2432,2433],{},"メール API の response は「このアドレスが登録済みか」を公開してはいけません。攻撃者による user enumeration を可能にします。",[29,2435,2436],{"id":2436},"より完全な本番アーキテクチャ",[16,2438,2439],{},"メール処理が Demo から本番へ進んだら、責務を分けます。",[80,2441,2444],{"className":2442,"code":2443,"language":381,"meta":85},[379],"HTTP リクエスト \u002F 業務イベント\n  → 認証、origin check、入力検証、rate limit、idempotency\n  → Queue\n  → メール template の描画\n  → EMAIL binding\n  → messageId \u002F error code を D1 へ保存\n  → Email Service logs と alert\n",[75,2445,2443],{"__ignoreMap":85},[16,2447,2448],{},"受信経路は次のようにできます。",[80,2450,2453],{"className":2451,"code":2452,"language":381,"meta":85},[379],"support@yourdomain.com\n  → Email Routing\n  → Email Worker\n  → 認証とサイズ確認\n  → MIME 解析\n  → 添付を R2 へ保存 \u002F D1 にチケットを作成\n  → 重い処理を Queue へ\n  → 人のメールボックスへ転送、または制約付き返信\n",[75,2454,2452],{"__ignoreMap":85},[36,2456,2458],{"id":2457},"d1-に記録するもの","D1 に記録するもの",[16,2460,2461],{},"記録に適する項目は次のとおりです。",[226,2463,2464,2467,2470,2475,2478,2481,2484],{},[229,2465,2466],{},"業務イベント ID と idempotency key。",[229,2468,2469],{},"template ID と template version。",[229,2471,2472,2474],{},[75,2473,545],{},"。",[229,2476,2477],{},"受信者数。必ずしも完全なアドレスを保存する必要はない。",[229,2479,2480],{},"accepted \u002F failed などアプリ側の状態。",[229,2482,2483],{},"Cloudflare error code。",[229,2485,2486],{},"作成、送信、最終更新時刻。",[16,2488,2489],{},"既定では記録しないもの：",[226,2491,2492,2495,2498,2501,2504,2507],{},[229,2493,2494],{},"plaintext の認証コード。",[229,2496,2497],{},"パスワードリセット Token。",[229,2499,2500],{},"完全なメール本文。",[229,2502,2503],{},"添付内容。",[229,2505,2506],{},"SMTP\u002FAPI Token。",[229,2508,2509],{},"デバッグのため無期限に保存する生の受信メール。",[16,2511,2512],{},"log 自体が新たな個人データ源になる可能性があります。",[36,2514,2516],{"id":2515},"email-routing-のdroppedを送信失敗と判断しない","Email Routing の「Dropped」を送信失敗と判断しない",[16,2518,2519,2520,2523,2524,2527],{},"公式ドキュメントは、",[75,2521,2522],{},"send_email"," binding から送ったメールが実際には正常に配信されていても、Email Routing の集計ページで ",[22,2525,2526],{},"dropped"," と表示される場合があると明記しています。",[16,2529,2530,2531,2533],{},"送信結果は Email Sending の Metrics と Logs で確認し、",[75,2532,545],{}," を使ってアプリログと関連付けます。Routing ページが示すのは受信 routing の処理であり、完全な送信配信 state machine ではありません。",[29,2535,2536],{"id":2536},"ローカルでのテスト",[16,2538,2539],{},"Wrangler は Email Worker のローカルイベントをサポートします。次を実行します。",[80,2541,2545],{"className":2542,"code":2543,"language":2544,"meta":85,"style":85},"language-bash shiki shiki-themes github-dark github-dark","pnpm exec wrangler dev\n","bash",[75,2546,2547],{"__ignoreMap":85},[89,2548,2549,2552,2555,2558],{"class":91,"line":92},[89,2550,2551],{"class":99},"pnpm",[89,2553,2554],{"class":153}," exec",[89,2556,2557],{"class":153}," wrangler",[89,2559,2560],{"class":153}," dev\n",[16,2562,2563],{},"ローカル handler へ生メールを送ります。",[80,2565,2567],{"className":2542,"code":2566,"language":2544,"meta":85,"style":85},"curl -X POST \\\n  'http:\u002F\u002Flocalhost:8787\u002Fcdn-cgi\u002Fhandler\u002Femail?from=sender@example.net&to=support@example.com' \\\n  -H 'Content-Type: text\u002Fplain' \\\n  --data-binary $'From: sender@example.net\\nTo: support@example.com\\nSubject: Local test\\n\\nHello from local development.'\n",[75,2568,2569,2583,2590,2600],{"__ignoreMap":85},[89,2570,2571,2574,2577,2580],{"class":91,"line":92},[89,2572,2573],{"class":99},"curl",[89,2575,2576],{"class":120}," -X",[89,2578,2579],{"class":153}," POST",[89,2581,2582],{"class":120}," \\\n",[89,2584,2585,2588],{"class":91,"line":110},[89,2586,2587],{"class":153},"  'http:\u002F\u002Flocalhost:8787\u002Fcdn-cgi\u002Fhandler\u002Femail?from=sender@example.net&to=support@example.com'",[89,2589,2582],{"class":120},[89,2591,2592,2595,2598],{"class":91,"line":124},[89,2593,2594],{"class":120},"  -H",[89,2596,2597],{"class":153}," 'Content-Type: text\u002Fplain'",[89,2599,2582],{"class":120},[89,2601,2602,2605,2608,2611,2614,2616,2619,2622],{"class":91,"line":134},[89,2603,2604],{"class":120},"  --data-binary",[89,2606,2607],{"class":153}," $'From: sender@example.net",[89,2609,2610],{"class":120},"\\n",[89,2612,2613],{"class":153},"To: support@example.com",[89,2615,2610],{"class":120},[89,2617,2618],{"class":153},"Subject: Local test",[89,2620,2621],{"class":120},"\\n\\n",[89,2623,2624],{"class":153},"Hello from local development.'\n",[16,2626,2627],{},"ローカルの送信 binding は text と HTML を確認できるファイルとして保存するため、template 検証に適しています。binary 添付のローカル simulation には個別の制限があるため、添付フローは remote test も追加してください。",[16,2629,2630],{},"少なくとも次をテストします。",[226,2632,2633,2636,2639,2642,2645,2648,2651,2654,2657,2660],{},[229,2634,2635],{},"通常の text と HTML。",[229,2637,2638],{},"非 ASCII の件名と送信者表示名。",[229,2640,2641],{},"重複リクエストと idempotency。",[229,2643,2644],{},"cooldown 完了前の認証コード再送。",[229,2646,2647],{},"大きな本文と添付の境界。",[229,2649,2650],{},"Header newline injection。",[229,2652,2653],{},"未検証の送信ドメイン、または制限された受信者。",[229,2655,2656],{},"bounce、suppression、枠、頻度の error。",[229,2658,2659],{},"一致しない受信アドレス、Catch-all、拒否経路。",[229,2661,2662],{},"HTML メール内の script、remote resource、危険な添付ファイル名。",[29,2664,2665],{"id":2665},"適する用途と適さない用途",[16,2667,2668],{},"適する用途：",[226,2670,2671,2674,2677,2680,2683,2686],{},[229,2672,2673],{},"登録認証、magic link、パスワードリセット。",[229,2675,2676],{},"注文、請求、デプロイ、監視通知。",[229,2678,2679],{},"個人ドメインのメール転送。",[229,2681,2682],{},"contact form とサポートチケット。",[229,2684,2685],{},"メールを trigger にする Agent や自動化。",[229,2687,2688],{},"SMTP サーバーの保守を減らしたい Cloudflare アプリケーション。",[16,2690,2691],{},"直接使うべきでない用途：",[226,2693,2694,2697,2700,2703,2706],{},[229,2695,2696],{},"同意のない marketing 一括送信。",[229,2698,2699],{},"unsubscribe、complaint、suppression 管理のない Newsletter。",[229,2701,2702],{},"匿名で公開する「代わりにメールを送る」endpoint。",[229,2704,2705],{},"IMAP folder、calendar、contact、複数人の shared mailbox が必要な企業メール。",[229,2707,2708],{},"任意の受信本文を、高権限ツールを持つ Agent へ直接渡すこと。",[16,2710,2711],{},"Cloudflare はメール入口と配信インフラを保守しますが、送信者 reputation、ユーザー同意、コンテンツ安全性、頻度制御、データ最小化はアプリケーション自身の責任です。",[29,2713,2714],{"id":2714},"最終的な判断",[16,2716,2717],{},"Cloudflare Email Service の最も価値ある点は「新しい SMTP provider」になったことではなく、メールを Workers、D1、R2、Queue、Agents と同じランタイム境界へ入れたことです。",[16,2719,2720],{},"受信メールは人のメールボックスへ転送するだけではなく、制約されたイベント入口になります。送信メールも、認証コードのためだけに外部 SDK と別の credential system を導入する必要がなくなります。",[16,2722,2723],{},"プロジェクトがすでに Workers 上にあるなら、次の最小経路から始めます。",[323,2725,2726,2733,2736,2739,2742],{},[229,2727,2728,2729,2732],{},"Email Routing で ",[75,2730,2731],{},"support@"," を受信する。",[229,2734,2735],{},"Worker binding で認証コードとパスワードリセットを送信する。",[229,2737,2738],{},"D1 で cooldown、一度限りの認証、最小限の log を実装する。",[229,2740,2741],{},"sender allowlist で binding 権限を制限する。",[229,2743,2744],{},"API の成功 response だけでなく、Email Sending log で配信を確認する。",[16,2746,2747],{},"外部 platform から本当に送信する必要が出たときに REST API を追加します。既存システムが SMTP client へ深く依存している場合だけ認証 SMTP を使います。",[29,2749,2750],{"id":2750},"参考資料",[226,2752,2753,2762,2769,2776,2783,2790,2797,2804,2810,2816,2823,2830,2837],{},[229,2754,2755],{},[2756,2757,2761],"a",{"href":2758,"rel":2759},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002F",[2760],"nofollow","Cloudflare Email Service 概要",[229,2763,2764],{},[2756,2765,2768],{"href":2766,"rel":2767},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fget-started\u002Fsend-emails\u002F",[2760],"Email Sending の設定",[229,2770,2771],{},[2756,2772,2775],{"href":2773,"rel":2774},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fget-started\u002Froute-emails\u002F",[2760],"Email Routing の設定",[229,2777,2778],{},[2756,2779,2782],{"href":2780,"rel":2781},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconfiguration\u002Fdomains\u002F",[2760],"ドメインと DNS 設定",[229,2784,2785],{},[2756,2786,2789],{"href":2787,"rel":2788},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconfiguration\u002Fsend-bindings\u002F",[2760],"送信 binding の権限設定",[229,2791,2792],{},[2756,2793,2796],{"href":2794,"rel":2795},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Fworkers-api\u002F",[2760],"Workers 送信 API",[229,2798,2799],{},[2756,2800,2803],{"href":2801,"rel":2802},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Froute-emails\u002Femail-handler\u002F",[2760],"Email Worker 受信 API",[229,2805,2806],{},[2756,2807,2200],{"href":2808,"rel":2809},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Frest-api\u002F",[2760],[229,2811,2812],{},[2756,2813,2211],{"href":2814,"rel":2815},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Fsmtp\u002F",[2760],[229,2817,2818],{},[2756,2819,2822],{"href":2820,"rel":2821},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fplatform\u002Flimits\u002F",[2760],"プラットフォーム制限",[229,2824,2825],{},[2756,2826,2829],{"href":2827,"rel":2828},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fplatform\u002Fpricing\u002F",[2760],"料金",[229,2831,2832],{},[2756,2833,2836],{"href":2834,"rel":2835},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconcepts\u002Femail-authentication\u002F",[2760],"メール認証と配信",[229,2838,2839],{},[2756,2840,2843],{"href":2841,"rel":2842},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Flocal-development\u002Frouting\u002F",[2760],"Email Worker のローカルテスト",[388,2845,2846],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":85,"searchDepth":124,"depth":124,"links":2848},[2849,2850,2854,2859,2865,2869,2870,2871,2872,2876,2877,2878,2879],{"id":491,"depth":110,"text":492},{"id":580,"depth":110,"text":581,"children":2851},[2852,2853],{"id":587,"depth":124,"text":588},{"id":617,"depth":124,"text":618},{"id":644,"depth":110,"text":645,"children":2855},[2856,2857],{"id":651,"depth":124,"text":652},{"id":672,"depth":124,"text":2858},"送信は cf-bounce record を使う",{"id":730,"depth":110,"text":731,"children":2860},[2861,2863,2864],{"id":756,"depth":124,"text":2862},"email() が受信メールの入口",{"id":1256,"depth":124,"text":1257},{"id":1422,"depth":124,"text":1423},{"id":1479,"depth":110,"text":1480,"children":2866},[2867,2868],{"id":1597,"depth":124,"text":1598},{"id":1818,"depth":124,"text":1819},{"id":2163,"depth":110,"text":2164},{"id":2250,"depth":110,"text":2250},{"id":2384,"depth":110,"text":2384},{"id":2436,"depth":110,"text":2436,"children":2873},[2874,2875],{"id":2457,"depth":124,"text":2458},{"id":2515,"depth":124,"text":2516},{"id":2536,"depth":110,"text":2536},{"id":2665,"depth":110,"text":2665},{"id":2714,"depth":110,"text":2714},{"id":2750,"depth":110,"text":2750},"2026 年 7 月時点の Cloudflare Email Service を使った受信、転送、解析、返信、トランザクションメール送信を、Nuxt、Workers、D1、セキュリティ、レート制限とともに解説します。",{"src":2882,"alt":2883,"caption":2884,"width":412,"height":413},"\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Fcover-v1.webp","世界中のエッジネットワークにある Worker ノードがメールを受信、認証、送信する","Cloudflare Email Service は、メールを保守が必要なサーバーからコードで処理できるイベントへ変えます",{},"\u002Fblog\u002Fcloudflare-email-service-send-receive-guide","2026-07-28",{"title":428,"description":2880},"blog\u002Fcloudflare-email-service-send-receive-guide",[2891,2892,2893,2894],"Cloudflare","Email","Workers","Serverless",{"src":2896,"poster":2882,"title":2897,"description":2898,"duration":2899,"width":2900,"height":2901},"\u002Fmedia\u002Fvideos\u002Fcloudflare-email-service-explained\u002Fcloudflare-email-service-explained-v1.mp4","Cloudflare Email Service 実践—11 分で分かる設定ガイド","DNS、Email Routing、Email Worker、send_email binding から REST、SMTP、Nuxt、D1、5 分間の認証コード、本番確認までを解説します。",661,1920,1080,"qW1O2KtlO0gzPc1CCQSN2y2VBaCtC21jxaEJwSq98fI",{"id":2904,"title":2905,"accent":2906,"body":2907,"description":4604,"draft":405,"experience":406,"extension":407,"featured":415,"image":4605,"meta":4609,"navigation":415,"path":4610,"publishedAt":4611,"readingTime":1031,"seo":4612,"stem":4613,"tags":4614,"updatedAt":406,"video":406,"__hash__":4618,"contentLocale":6,"isFallback":405},"posts_ja\u002Fblog\u002Fvue-3-6-rc-upgrade-guide.md","Vue 3.6 RC アップグレード全解説—3.5 から Vapor Mode と alien-signals へ","mint",{"type":13,"value":2908,"toc":4565},[2909,2916,2930,2933,2942,2946,3085,3088,3093,3097,3100,3135,3138,3144,3148,3155,3158,3271,3274,3280,3285,3288,3291,3304,3311,3314,3317,3334,3337,3340,3356,3373,3376,3380,3383,3386,3394,3397,3401,3404,3529,3532,3560,3588,3591,3595,3598,3648,3651,3655,3662,3723,3726,3729,3735,3741,3744,3748,3751,3765,3773,3820,3827,3830,3843,3846,3850,3853,3989,3996,4002,4005,4092,4095,4129,4133,4146,4149,4153,4156,4159,4162,4166,4169,4173,4176,4179,4182,4186,4189,4204,4210,4227,4230,4262,4265,4269,4272,4292,4295,4299,4302,4325,4328,4332,4335,4338,4358,4361,4365,4368,4372,4375,4379,4382,4386,4389,4392,4475,4477,4480,4488,4491,4498,4501,4504,4507,4557,4562],[16,2910,2911,2912,2915],{},"バージョン番号だけを見れば、Vue 3.6 は普通のマイナーアップデートに見えます。しかしリリースノートを最後まで読むと、印象は違います。",[22,2913,2914],{},"Vue が次の段階に向けてエンジンを交換する更新","に近いのです。",[16,2917,2918,2919,2922,2923,2922,2926,2929],{},"一方では alien-signals を基盤にリアクティビティコアが再構築され、公開されている ",[75,2920,2921],{},"ref","、",[75,2924,2925],{},"computed",[75,2927,2928],{},"watch"," の使い方はほぼ変わりません。もう一方では Vapor Mode が正式に RC へ入り、Vue の SFC を Virtual DOM を経由せず、より直接的な DOM 更新ロジックへコンパイルできるようになります。",[16,2931,2932],{},"最初に、最も重要なバージョン情報を確認します。",[484,2934,2935],{},[16,2936,2937,2938,2941],{},"2026 年 7 月 26 日時点で、Vue 3.6 の最新版は ",[22,2939,2940],{},"3.6.0-rc.2"," です。RC はリリース候補であり、安定版ではありません。この記事は評価、実験、移行準備のためのもので、すべての本番プロジェクトが直ちにアップグレードすべきだという意味ではありません。",[29,2943,2945],{"id":2944},"先に結論36-では何が変わったのか","先に結論：3.6 では何が変わったのか",[254,2947,2948,2964],{},[257,2949,2950],{},[260,2951,2952,2955,2958,2961],{},[263,2953,2954],{},"比較項目",[263,2956,2957],{},"Vue 3.5",[263,2959,2960],{},"Vue 3.6 RC",[263,2962,2963],{},"既存プロジェクトへの実際の影響",[270,2965,2966,2980,2994,3008,3022,3036,3053,3071],{},[260,2967,2968,2971,2974,2977],{},[275,2969,2970],{},"リアクティビティ",[275,2972,2973],{},"3.5 で内部最適化済み",[275,2975,2976],{},"alien-signals を基盤に大幅再構築",[275,2978,2979],{},"通常の Composition API は書き直し不要ですが、複雑な副作用とスケジューリングは回帰テストが必要",[260,2981,2982,2985,2988,2991],{},[275,2983,2984],{},"デフォルト描画",[275,2986,2987],{},"Virtual DOM",[275,2989,2990],{},"引き続き Virtual DOM",[275,2992,2993],{},"依存関係を更新するだけで Vapor へ移行することはない",[260,2995,2996,2999,3002,3005],{},[275,2997,2998],{},"Vapor Mode",[275,3000,3001],{},"3.5 の正式機能としては未提供",[275,3003,3004],{},"機能セットが RC、完全な明示的 opt-in",[275,3006,3007],{},"性能が重要な一領域から実験できる",[260,3009,3010,3013,3016,3019],{},[275,3011,3012],{},"アプリサイズ",[275,3014,3015],{},"VDOM runtime を既定で含む",[275,3017,3018],{},"純粋な Vapor アプリは VDOM runtime を省略可能",[275,3020,3021],{},"純 Vapor または境界が明確な領域でサイズ効果を得やすい",[260,3023,3024,3027,3030,3033],{},[275,3025,3026],{},"API 対応範囲",[275,3028,3029],{},"Vue 3 API の完全なエコシステム",[275,3031,3032],{},"Vapor は Vue API のサブセットをサポート",[275,3034,3035],{},"Options API やインスタンスプロキシ関連機能は直接移行できない",[260,3037,3038,3041,3044,3050],{},[275,3039,3040],{},"VDOM \u002F Vapor 混在",[275,3042,3043],{},"対象外",[275,3045,3046,3049],{},[75,3047,3048],{},"vaporInteropPlugin"," で相互運用",[275,3051,3052],{},"標準的な props、イベント、slots は対応済みですが、複雑なネストはテストが必要",[260,3054,3055,3058,3061,3068],{},[275,3056,3057],{},"イベント委譲",[275,3059,3060],{},"ネイティブなリスナー動作",[275,3062,3063,3064,3067],{},"rc.2 以降は直接 binding が既定、",[75,3065,3066],{},".delegate"," で明示的に委譲",[275,3069,3070],{},"rc.1 の自動委譲という認識を引き継がない",[260,3072,3073,3076,3079,3082],{},[275,3074,3075],{},"本番利用",[275,3077,3078],{},"安定版",[275,3080,3081],{},"現在もプレリリース",[275,3083,3084],{},"安定版と上位エコシステムの明示的対応を待つべき",[16,3086,3087],{},"一言でまとめると、",[16,3089,3090],{},[22,3091,3092],{},"Vue 3.5 の中心は開発体験と SSR 機能の向上、Vue 3.6 の中心はリアクティビティコアと描画アーキテクチャの更新です。",[29,3094,3096],{"id":3095},"_35-から-36-へ変化の重心が移った","3.5 から 3.6 へ：変化の重心が移った",[16,3098,3099],{},"Vue 3.5 には破壊的変更がなく、多くの改善を業務コードから直接感じられました。",[226,3101,3102,3105,3111,3117,3123,3126,3132],{},[229,3103,3104],{},"Reactive Props Destructure が安定版になった。",[229,3106,3107,3110],{},[75,3108,3109],{},"useTemplateRef()"," が template ref の整理方法を改善した。",[229,3112,3113,3116],{},[75,3114,3115],{},"onWatcherCleanup()"," で watcher の cleanup を自然に書けるようになった。",[229,3118,3119,3122],{},[75,3120,3121],{},"useId()"," が SSR に適した安定 ID を提供した。",[229,3124,3125],{},"非同期コンポーネントが Lazy Hydration に対応した。",[229,3127,3128,3131],{},[75,3129,3130],{},"data-allow-mismatch"," で許容する hydration 差異を宣言できるようになった。",[229,3133,3134],{},"Deferred Teleport とカスタム要素が強化された。",[16,3136,3137],{},"3.6 にも多くの修正がありますが、最も注目すべき alien-signals と Vapor は、どちらもフレームワークの下層にあります。業務コードに新 API を一つも追加しなくても、性能特性、コンパイル結果、ランタイム境界は変わります。",[16,3139,3140,3141],{},"そのためアップグレード方法も変わります。",[22,3142,3143],{},"「コンパイルできるか」だけでなく、リアクティブな動作、SSR hydration、コンポーネント間の相互運用、実際の性能を確認してください。",[29,3145,3147],{"id":3146},"変更点-1リアクティビティコアが-alien-signals-へ","変更点 1：リアクティビティコアが alien-signals へ",[16,3149,3150,3151,3154],{},"Vue 3.6 は ",[75,3152,3153],{},"@vue\u002Freactivity"," を大規模に再構築し、その基盤には StackBlitz がオープンソース化した alien-signals が使われています。",[16,3156,3157],{},"大多数のアプリケーション開発者にとって、次のコードはこれまでどおりの Vue です。",[80,3159,3161],{"className":82,"code":3160,"language":84,"meta":85,"style":85},"import { computed, ref, watchEffect } from 'vue'\n\nconst price = ref(99)\nconst count = ref(2)\nconst total = computed(() => price.value * count.value)\n\nwatchEffect(() => {\n  console.log(total.value)\n})\n",[75,3162,3163,3175,3179,3198,3216,3242,3246,3257,3267],{"__ignoreMap":85},[89,3164,3165,3167,3170,3172],{"class":91,"line":92},[89,3166,1274],{"class":95},[89,3168,3169],{"class":106}," { computed, ref, watchEffect } ",[89,3171,1280],{"class":95},[89,3173,3174],{"class":153}," 'vue'\n",[89,3176,3177],{"class":91,"line":110},[89,3178,803],{"emptyLinePlaceholder":415},[89,3180,3181,3183,3186,3188,3191,3193,3196],{"class":91,"line":124},[89,3182,1848],{"class":95},[89,3184,3185],{"class":120}," price",[89,3187,103],{"class":95},[89,3189,3190],{"class":99}," ref",[89,3192,824],{"class":106},[89,3194,3195],{"class":120},"99",[89,3197,947],{"class":106},[89,3199,3200,3202,3205,3207,3209,3211,3214],{"class":91,"line":134},[89,3201,1848],{"class":95},[89,3203,3204],{"class":120}," count",[89,3206,103],{"class":95},[89,3208,3190],{"class":99},[89,3210,824],{"class":106},[89,3212,3213],{"class":120},"2",[89,3215,947],{"class":106},[89,3217,3218,3220,3223,3225,3228,3231,3233,3236,3239],{"class":91,"line":145},[89,3219,1848],{"class":95},[89,3221,3222],{"class":120}," total",[89,3224,103],{"class":95},[89,3226,3227],{"class":99}," computed",[89,3229,3230],{"class":106},"(() ",[89,3232,1926],{"class":95},[89,3234,3235],{"class":106}," price.value ",[89,3237,3238],{"class":95},"*",[89,3240,3241],{"class":106}," count.value)\n",[89,3243,3244],{"class":91,"line":173},[89,3245,803],{"emptyLinePlaceholder":415},[89,3247,3248,3251,3253,3255],{"class":91,"line":183},[89,3249,3250],{"class":99},"watchEffect",[89,3252,3230],{"class":106},[89,3254,1926],{"class":95},[89,3256,107],{"class":106},[89,3258,3259,3262,3264],{"class":91,"line":194},[89,3260,3261],{"class":106},"  console.",[89,3263,989],{"class":99},[89,3265,3266],{"class":106},"(total.value)\n",[89,3268,3269],{"class":91,"line":215},[89,3270,1897],{"class":106},[16,3272,3273],{},"変化したのは内部です。依存関係の構築方法、状態のマーク方法、computed がいつ無効になるか、effect が依存グラフをどう伝播するか、そして関係をどう cleanup し再利用するかが変わりました。",[16,3275,3276],{},[568,3277],{"alt":3278,"src":3279},"アクティブな経路と休眠中の経路から構成される細粒度リアクティブ依存ネットワーク","\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Freactivity-core-v1.webp",[16,3281,3282],{},[575,3283,3284],{},"この図は依存伝播の概念を示したもので、Vue の内部データ構造をフィールド単位で表したものではありません。",[36,3286,3287],{"id":3287},"性能数値をどう理解するか",[16,3289,3290],{},"Vue core の alien-signals マージ PR では、二種類のマイクロベンチマーク結果が示されています。",[226,3292,3293,3301],{},[229,3294,3295,3296,2922,3298,3300],{},"多数の ",[75,3297,2921],{},[75,3299,2925],{},"、effect インスタンスを生成する場面で、サンプルのメモリ使用量が約 2.3 MB から約 2.0 MB へ減り、およそ 13% 削減された。",[229,3302,3303],{},"一つの source が変化した後、多数の computed を読み取る特定のストレスケースで、30 倍を超える性能向上が見られた。",[16,3305,3306,3307,3310],{},"二つ目の数字は目を引きますが、「Vue 3.6 のアプリが 30 倍速くなる」という意味ではありません。示しているのは、",[22,3308,3309],{},"以前は規模とともにコストが急増した一部の依存グラフで、新しい伝播アルゴリズムの計算量特性が改善した","ということです。",[16,3312,3313],{},"実際のページには DOM、layout、network、コンポーネントライブラリ、serialization、業務計算も含まれます。アップグレード前後は、自分のプロジェクトの flame chart、操作遅延、メモリ曲線、bundle 結果で判断し、フレームワークのマイクロベンチマークをそのままアプリの約束にしないでください。",[36,3315,3316],{"id":3316},"効果を感じやすいプロジェクト",[226,3318,3319,3322,3325,3328,3331],{},[229,3320,3321],{},"多数の computed が相互依存するデータダッシュボード。",[229,3323,3324],{},"高頻度の変更、フィルター、多くの派生状態を持つエディター。",[229,3326,3327],{},"長時間動作し、多数の effect を生成・破棄するワークベンチ。",[229,3329,3330],{},"フィールド数が多く、検証規則と連動関係が複雑なフォーム。",[229,3332,3333],{},"Vue のリアクティブ primitive を基盤にした状態管理やツールライブラリ。",[16,3335,3336],{},"通常のコンテンツページも内部改善を受けますが、ユーザーが大きな違いを感じるとは限りません。",[36,3338,3339],{"id":3339},"特に注意すべきコード",[16,3341,3342,3343,2922,3345,2922,3348,2922,3350,2922,3352,3355],{},"公開 API の ",[75,3344,2921],{},[75,3346,3347],{},"reactive",[75,3349,2925],{},[75,3351,2928],{},[75,3353,3354],{},"effectScope"," だけを使っていれば、移行リスクは比較的限定的です。本当に注意すべきなのは次のコードです。",[226,3357,3358,3361,3364,3367,3370],{},[229,3359,3360],{},"Vue の非公開リアクティブフィールドを直接読む、または変更する。",[229,3362,3363],{},"非公開の effect、Dep、scheduler の実装詳細へ依存する。",[229,3365,3366],{},"リアクティブ関数を monkey patch する。",[229,3368,3369],{},"watcher の実行順、同期 flush、cleanup timing に暗黙の仮定がある。",[229,3371,3372],{},"複数の Vue マイナーバージョンに対応するライブラリで、境界テストがない。",[16,3374,3375],{},"内部再構築の意義は、公開 API を保ったまま Vue が実装を交換できることです。逆に、内部実装に依存するコードほど、この種のアップグレードで問題が表面化します。",[29,3377,3379],{"id":3378},"変更点-2vapor-mode-は実験的な概念だけではなくなった","変更点 2：Vapor Mode は実験的な概念だけではなくなった",[16,3381,3382],{},"Vapor Mode は Vue SFC の新しいコンパイルモードです。従来の Vue template は VNode を生成し、Virtual DOM runtime が新旧ツリーを比較して差分を実 DOM へ反映します。Vapor ではコンパイラが動的部分を事前に認識し、より直接的で細粒度な更新ロジックを生成します。",[16,3384,3385],{},"目標は二つです。",[323,3387,3388,3391],{},[229,3389,3390],{},"アプリケーションの基礎 bundle サイズを削減する。",[229,3392,3393],{},"実行時に VNode を生成、走査、比較するコストを減らす。",[16,3395,3396],{},"公式リリースノートによると、Vapor は第三者の js-framework-benchmark で Solid や Svelte 5 に近い水準に達しています。これも「ベンチマークの場面での結果」と理解すべきで、異なるフレームワークと実アプリの性能を一つの順位に圧縮するものではありません。",[36,3398,3400],{"id":3399},"アップグレードしても-vapor-は自動で有効にならない","アップグレードしても Vapor は自動で有効にならない",[16,3402,3403],{},"Vue 3.6 も既定では VDOM を使います。SFC で Vapor を使うには明示的に指定します。",[80,3405,3409],{"className":3406,"code":3407,"language":3408,"meta":85,"style":85},"language-vue shiki shiki-themes github-dark github-dark","\u003Cscript setup vapor lang=\"ts\">\nimport { ref } from 'vue'\n\nconst count = ref(0)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton @click=\"count++\">\n    Count: {{ count }}\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n","vue",[75,3410,3411,3435,3446,3450,3467,3476,3480,3489,3507,3512,3521],{"__ignoreMap":85},[89,3412,3413,3415,3419,3422,3425,3428,3430,3433],{"class":91,"line":92},[89,3414,844],{"class":106},[89,3416,3418],{"class":3417},"sxg3X","script",[89,3420,3421],{"class":99}," setup",[89,3423,3424],{"class":99}," vapor",[89,3426,3427],{"class":99}," lang",[89,3429,1944],{"class":106},[89,3431,3432],{"class":153},"\"ts\"",[89,3434,1133],{"class":106},[89,3436,3437,3439,3442,3444],{"class":91,"line":110},[89,3438,1274],{"class":95},[89,3440,3441],{"class":106}," { ref } ",[89,3443,1280],{"class":95},[89,3445,3174],{"class":153},[89,3447,3448],{"class":91,"line":124},[89,3449,803],{"emptyLinePlaceholder":415},[89,3451,3452,3454,3456,3458,3460,3462,3465],{"class":91,"line":134},[89,3453,1848],{"class":95},[89,3455,3204],{"class":120},[89,3457,103],{"class":95},[89,3459,3190],{"class":99},[89,3461,824],{"class":106},[89,3463,3464],{"class":120},"0",[89,3466,947],{"class":106},[89,3468,3469,3472,3474],{"class":91,"line":145},[89,3470,3471],{"class":106},"\u003C\u002F",[89,3473,3418],{"class":3417},[89,3475,1133],{"class":106},[89,3477,3478],{"class":91,"line":173},[89,3479,803],{"emptyLinePlaceholder":415},[89,3481,3482,3484,3487],{"class":91,"line":183},[89,3483,844],{"class":106},[89,3485,3486],{"class":3417},"template",[89,3488,1133],{"class":106},[89,3490,3491,3494,3497,3500,3502,3505],{"class":91,"line":194},[89,3492,3493],{"class":106},"  \u003C",[89,3495,3496],{"class":3417},"button",[89,3498,3499],{"class":99}," @click",[89,3501,1944],{"class":106},[89,3503,3504],{"class":153},"\"count++\"",[89,3506,1133],{"class":106},[89,3508,3509],{"class":91,"line":215},[89,3510,3511],{"class":106},"    Count: {{ count }}\n",[89,3513,3514,3517,3519],{"class":91,"line":905},[89,3515,3516],{"class":106},"  \u003C\u002F",[89,3518,3496],{"class":3417},[89,3520,1133],{"class":106},[89,3522,3523,3525,3527],{"class":91,"line":933},[89,3524,3471],{"class":106},[89,3526,3486],{"class":3417},[89,3528,1133],{"class":106},[16,3530,3531],{},"同等の入口がほかに二つあります。",[80,3533,3535],{"className":3406,"code":3534,"language":3408,"meta":85,"style":85},"\u003Cscript vapor>\n\u002F\u002F \u003Cscript setup vapor> の短縮形\n\u003C\u002Fscript>\n",[75,3536,3537,3547,3552],{"__ignoreMap":85},[89,3538,3539,3541,3543,3545],{"class":91,"line":92},[89,3540,844],{"class":106},[89,3542,3418],{"class":3417},[89,3544,3424],{"class":99},[89,3546,1133],{"class":106},[89,3548,3549],{"class":91,"line":110},[89,3550,3551],{"class":2008},"\u002F\u002F \u003Cscript setup vapor> の短縮形\n",[89,3553,3554,3556,3558],{"class":91,"line":124},[89,3555,3471],{"class":106},[89,3557,3418],{"class":3417},[89,3559,1133],{"class":106},[80,3561,3563],{"className":3406,"code":3562,"language":3408,"meta":85,"style":85},"\u003Ctemplate vapor>\n  \u003C!-- SFC 全体を Vapor でコンパイル -->\n\u003C\u002Ftemplate>\n",[75,3564,3565,3575,3580],{"__ignoreMap":85},[89,3566,3567,3569,3571,3573],{"class":91,"line":92},[89,3568,844],{"class":106},[89,3570,3486],{"class":3417},[89,3572,3424],{"class":99},[89,3574,1133],{"class":106},[89,3576,3577],{"class":91,"line":110},[89,3578,3579],{"class":2008},"  \u003C!-- SFC 全体を Vapor でコンパイル -->\n",[89,3581,3582,3584,3586],{"class":91,"line":124},[89,3583,3471],{"class":106},[89,3585,3486],{"class":3417},[89,3587,1133],{"class":106},[16,3589,3590],{},"つまり 3.6 へアップグレードした後も既存の VDOM アプリを維持し、特定のコンポーネントやページだけを選んで移行できます。",[36,3592,3594],{"id":3593},"純粋な-vapor-アプリケーション","純粋な Vapor アプリケーション",[16,3596,3597],{},"アプリケーションが完全に Vapor コンポーネントで構成される場合は、次を使えます。",[80,3599,3601],{"className":82,"code":3600,"language":84,"meta":85,"style":85},"import { createVaporApp } from 'vue'\nimport App from '.\u002FApp.vue'\n\ncreateVaporApp(App).mount('#app')\n",[75,3602,3603,3614,3626,3630],{"__ignoreMap":85},[89,3604,3605,3607,3610,3612],{"class":91,"line":92},[89,3606,1274],{"class":95},[89,3608,3609],{"class":106}," { createVaporApp } ",[89,3611,1280],{"class":95},[89,3613,3174],{"class":153},[89,3615,3616,3618,3621,3623],{"class":91,"line":110},[89,3617,1274],{"class":95},[89,3619,3620],{"class":106}," App ",[89,3622,1280],{"class":95},[89,3624,3625],{"class":153}," '.\u002FApp.vue'\n",[89,3627,3628],{"class":91,"line":124},[89,3629,803],{"emptyLinePlaceholder":415},[89,3631,3632,3635,3638,3641,3643,3646],{"class":91,"line":134},[89,3633,3634],{"class":99},"createVaporApp",[89,3636,3637],{"class":106},"(App).",[89,3639,3640],{"class":99},"mount",[89,3642,824],{"class":106},[89,3644,3645],{"class":153},"'#app'",[89,3647,947],{"class":106},[16,3649,3650],{},"このモードは Virtual DOM runtime を取り込む必要がなく、基礎 bundle サイズの効果を最も得やすい方法です。小さな新規アプリ、インタラクティブなツール、技術スタックを明確に制約できる独立ページに向いています。",[36,3652,3654],{"id":3653},"既存の-vdom-アプリケーションで-vapor-を使う","既存の VDOM アプリケーションで Vapor を使う",[16,3656,3657,3658,3661],{},"既存の ",[75,3659,3660],{},"createApp()"," プロジェクトには相互運用プラグインが必要です。",[80,3663,3665],{"className":82,"code":3664,"language":84,"meta":85,"style":85},"import { createApp, vaporInteropPlugin } from 'vue'\nimport App from '.\u002FApp.vue'\n\ncreateApp(App)\n  .use(vaporInteropPlugin)\n  .mount('#app')\n",[75,3666,3667,3678,3688,3692,3700,3711],{"__ignoreMap":85},[89,3668,3669,3671,3674,3676],{"class":91,"line":92},[89,3670,1274],{"class":95},[89,3672,3673],{"class":106}," { createApp, vaporInteropPlugin } ",[89,3675,1280],{"class":95},[89,3677,3174],{"class":153},[89,3679,3680,3682,3684,3686],{"class":91,"line":110},[89,3681,1274],{"class":95},[89,3683,3620],{"class":106},[89,3685,1280],{"class":95},[89,3687,3625],{"class":153},[89,3689,3690],{"class":91,"line":124},[89,3691,803],{"emptyLinePlaceholder":415},[89,3693,3694,3697],{"class":91,"line":134},[89,3695,3696],{"class":99},"createApp",[89,3698,3699],{"class":106},"(App)\n",[89,3701,3702,3705,3708],{"class":91,"line":145},[89,3703,3704],{"class":106},"  .",[89,3706,3707],{"class":99},"use",[89,3709,3710],{"class":106},"(vaporInteropPlugin)\n",[89,3712,3713,3715,3717,3719,3721],{"class":91,"line":173},[89,3714,3704],{"class":106},[89,3716,3640],{"class":99},[89,3718,824],{"class":106},[89,3720,3645],{"class":153},[89,3722,947],{"class":106},[16,3724,3725],{},"逆に、Vapor アプリもこのプラグインを導入して VDOM コンポーネントを含められます。ただし VDOM runtime が再び入り、bundle サイズの利点は小さくなります。",[16,3727,3728],{},"Render Function や JSX で書いたコンポーネントも VDOM コンポーネントのままです。純 Vapor アプリで使う場合は相互運用が必要です。",[16,3730,3731],{},[568,3732],{"alt":3733,"src":3734},"既存の VDOM 領域が明示的な相互運用境界を通じて独立した Vapor 領域へ接続される","\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Fvapor-boundaries-v1.webp",[16,3736,3737,3738],{},"私の提案も公式ガイダンスと同じです。",[22,3739,3740],{},"二つの描画モードを境界の明確な領域へ整理し、コンポーネント階層の各段で交互にネストしないでください。",[16,3742,3743],{},"例えば、高頻度に更新するデータキャンバスだけを Vapor ページまたは subtree にし、周囲の管理ナビゲーション、dialog、成熟したコンポーネントライブラリは VDOM のままにします。そのほうが効果、障害範囲、ロールバック経路を測定しやすくなります。",[29,3745,3747],{"id":3746},"rc2-で最も陥りやすい変更イベント委譲が明示的-opt-in-になった","rc.2 で最も陥りやすい変更：イベント委譲が明示的 opt-in になった",[16,3749,3750],{},"rc.1 の段階で Vapor を試した場合、この節は特に重要です。",[16,3752,3753,3754,3757,3758,3761,3762,3764],{},"rc.1 では条件を満たすイベントが自動的に ",[75,3755,3756],{},"document"," へ委譲されました。しかし、これはネイティブ DOM と標準 Vue の動作と異なります。祖先要素が先に ",[75,3759,3760],{},"stopPropagation()"," を呼ぶとイベントは ",[75,3763,3756],{}," へ到達せず、子要素の委譲 handler も実行されません。",[16,3766,3767,3769,3770,3772],{},[22,3768,2940],{}," 以降、Vapor は既定で DOM listener を要素へ直接 binding します。document レベルの委譲を明示的に使いたい場合だけ、Vapor 専用の ",[75,3771,3066],{}," modifier を付けます。",[80,3774,3776],{"className":3406,"code":3775,"language":3408,"meta":85,"style":85},"\u003Cbutton @click.delegate=\"onClick\">\n  保存\n\u003C\u002Fbutton>\n",[75,3777,3778,3807,3812],{"__ignoreMap":85},[89,3779,3780,3782,3784,3787,3790,3792,3795,3797,3800,3803,3805],{"class":91,"line":92},[89,3781,844],{"class":106},[89,3783,3496],{"class":3417},[89,3785,3786],{"class":106}," @",[89,3788,3789],{"class":99},"click",[89,3791,1686],{"class":106},[89,3793,3794],{"class":99},"delegate",[89,3796,1944],{"class":106},[89,3798,3799],{"class":153},"\"",[89,3801,3802],{"class":106},"onClick",[89,3804,3799],{"class":153},[89,3806,1133],{"class":106},[89,3808,3809],{"class":91,"line":110},[89,3810,3811],{"class":106},"  保存\n",[89,3813,3814,3816,3818],{"class":91,"line":124},[89,3815,3471],{"class":106},[89,3817,3496],{"class":3417},[89,3819,1133],{"class":106},[16,3821,3822,3823,3826],{},"古い ",[75,3824,3825],{},"compilerOptions.eventDelegation"," は削除されました。",[16,3828,3829],{},"移行では二つの作業が必要です。",[323,3831,3832,3837],{},[229,3833,3657,3834,3836],{},[75,3835,3825],{}," を削除する。",[229,3838,3839,3840,3842],{},"数が多く、イベントが安定し、伝播経路を検証済みの listener だけに ",[75,3841,3066],{}," を使う。",[16,3844,3845],{},"これは全体へ機械的に置換するための最適化ではありません。委譲に価値があるかは、ノード数、ライフサイクル、伝播セマンティクスを合わせて判断します。",[29,3847,3849],{"id":3848},"vapor-と標準-vue-の互換性境界","Vapor と標準 Vue の互換性境界",[16,3851,3852],{},"Vapor は初日から Vue の歴史的な機能をすべて覆うことを目標にしていません。公式には既存 Vue API のサブセットをサポートし、VNode やコンポーネントの公開インスタンスプロキシへ依存する機能は現在直接利用できません。",[254,3854,3855,3867],{},[257,3856,3857],{},[260,3858,3859,3861,3864],{},[263,3860,1152],{},[263,3862,3863],{},"VDOM コンポーネント",[263,3865,3866],{},"Vapor コンポーネント",[270,3868,3869,3880,3890,3901,3917,3928,3940,3952,3978],{},[260,3870,3871,3874,3877],{},[275,3872,3873],{},"Composition API",[275,3875,3876],{},"対応",[275,3878,3879],{},"主なプログラミングモデル",[260,3881,3882,3885,3887],{},[275,3883,3884],{},"Options API",[275,3886,3876],{},[275,3888,3889],{},"非対応",[260,3891,3892,3897,3899],{},[275,3893,3894],{},[75,3895,3896],{},"app.config.globalProperties",[275,3898,3876],{},[275,3900,3889],{},[260,3902,3903,3908,3911],{},[275,3904,3905],{},[75,3906,3907],{},"getCurrentInstance()",[275,3909,3910],{},"現在のインスタンスを返す",[275,3912,3913,3916],{},[75,3914,3915],{},"null"," を返す",[260,3918,3919,3922,3925],{},[275,3920,3921],{},"Render Function \u002F JSX",[275,3923,3924],{},"ネイティブ VDOM",[275,3926,3927],{},"VDOM のまま、混在には interop が必要",[260,3929,3930,3935,3937],{},[275,3931,3932],{},[75,3933,3934],{},"v-memo",[275,3936,3876],{},[275,3938,3939],{},"対象外 \u002F 非対応",[260,3941,3942,3948,3950],{},[275,3943,3944,3947],{},[75,3945,3946],{},"@vue:xxx"," 要素ライフサイクルイベント",[275,3949,3876],{},[275,3951,3889],{},[260,3953,3954,3957,3960],{},[275,3955,3956],{},"コンポーネント template ref",[275,3958,3959],{},"公開インスタンス機能を公開",[275,3961,3962,2922,3965,2922,3968,2922,3971,2922,3974,3977],{},[75,3963,3964],{},"$el",[75,3966,3967],{},"$props",[75,3969,3970],{},"$attrs",[75,3972,3973],{},"$slots",[75,3975,3976],{},"$refs"," などを公開しない",[260,3979,3980,3983,3986],{},[275,3981,3982],{},"カスタム directive",[275,3984,3985],{},"標準 directive lifecycle object",[275,3987,3988],{},"Vapor 専用の関数インターフェース",[36,3990,3992,3995],{"id":3991},"slotsdefault-を副作用のない確認に使わない",[75,3993,3994],{},"slots.default()"," を副作用のない確認に使わない",[16,3997,3998,3999,4001],{},"VDOM では、先に ",[75,4000,3994],{}," を呼び、返った内容を確認してから fallback を描画するコンポーネントがあります。Vapor で slot を呼ぶと、Block と DOM の生成、リアクティブ effect の登録、hydration 時の既存 SSR DOM の取得が直ちに行われる可能性があります。",[16,4003,4004],{},"次のように書かないでください。",[80,4006,4008],{"className":3406,"code":4007,"language":3408,"meta":85,"style":85},"\u003Cscript setup vapor>\nimport { useSlots } from 'vue'\n\nconst slots = useSlots()\nconst content = slots.default?.()\nconst showFallback = !content\n\u003C\u002Fscript>\n",[75,4009,4010,4022,4033,4037,4051,4069,4084],{"__ignoreMap":85},[89,4011,4012,4014,4016,4018,4020],{"class":91,"line":92},[89,4013,844],{"class":106},[89,4015,3418],{"class":3417},[89,4017,3421],{"class":99},[89,4019,3424],{"class":99},[89,4021,1133],{"class":106},[89,4023,4024,4026,4029,4031],{"class":91,"line":110},[89,4025,1274],{"class":95},[89,4027,4028],{"class":106}," { useSlots } ",[89,4030,1280],{"class":95},[89,4032,3174],{"class":153},[89,4034,4035],{"class":91,"line":124},[89,4036,803],{"emptyLinePlaceholder":415},[89,4038,4039,4041,4044,4046,4049],{"class":91,"line":134},[89,4040,1848],{"class":95},[89,4042,4043],{"class":120}," slots",[89,4045,103],{"class":95},[89,4047,4048],{"class":99}," useSlots",[89,4050,869],{"class":106},[89,4052,4053,4055,4058,4060,4063,4066],{"class":91,"line":145},[89,4054,1848],{"class":95},[89,4056,4057],{"class":120}," content",[89,4059,103],{"class":95},[89,4061,4062],{"class":106}," slots.",[89,4064,4065],{"class":99},"default",[89,4067,4068],{"class":106},"?.()\n",[89,4070,4071,4073,4076,4078,4081],{"class":91,"line":173},[89,4072,1848],{"class":95},[89,4074,4075],{"class":120}," showFallback",[89,4077,103],{"class":95},[89,4079,4080],{"class":95}," !",[89,4082,4083],{"class":106},"content\n",[89,4085,4086,4088,4090],{"class":91,"line":183},[89,4087,3471],{"class":106},[89,4089,3418],{"class":3417},[89,4091,1133],{"class":106},[16,4093,4094],{},"実際の slot 描画は template に任せるほうが安全です。",[80,4096,4098],{"className":3406,"code":4097,"language":3408,"meta":85,"style":85},"\u003Ctemplate>\n  \u003Cslot \u002F>\n\u003C\u002Ftemplate>\n",[75,4099,4100,4108,4121],{"__ignoreMap":85},[89,4101,4102,4104,4106],{"class":91,"line":92},[89,4103,844],{"class":106},[89,4105,3486],{"class":3417},[89,4107,1133],{"class":106},[89,4109,4110,4112,4115,4119],{"class":91,"line":110},[89,4111,3493],{"class":106},[89,4113,4114],{"class":3417},"slot",[89,4116,4118],{"class":4117},"sX7ps"," \u002F",[89,4120,1133],{"class":106},[89,4122,4123,4125,4127],{"class":91,"line":124},[89,4124,3471],{"class":106},[89,4126,3486],{"class":3417},[89,4128,1133],{"class":106},[36,4130,4132],{"id":4131},"カスタム-directive-は個別に移行する","カスタム directive は個別に移行する",[16,4134,4135,4136,2922,4139,2922,4142,4145],{},"Vapor のカスタム directive はノードとリアクティブ getter を受け取り、cleanup 関数を返せます。VDOM directive の ",[75,4137,4138],{},"mounted",[75,4140,4141],{},"updated",[75,4143,4144],{},"unmounted"," オブジェクトをそのまま移すものではありません。",[16,4147,4148],{},"多くのカスタム directive に依存するプロジェクトやコンポーネントライブラリでは、独立した移行項目として一覧化し、リアクティブ更新、cleanup、SSR 動作を一つずつ検証してください。",[29,4150,4152],{"id":4151},"nuxtrouterpiniaコンポーネントライブラリをどう見るべきか","Nuxt、Router、Pinia、コンポーネントライブラリをどう見るべきか",[16,4154,4155],{},"Vue core で利用できることは、その上のフレームワークが Vapor 統合を自動的に完了したことを意味しません。",[16,4157,4158],{},"公式 Vapor Roadmap は Router、Pinia、Nuxt、DevTools、Vue Test Utils などのエコシステム作業を個別に追跡しています。Nuxt はさらに SSR hydration 機能へ依存します。実プロジェクトでは、コンパイラの Vapor 認識、サーバー出力、クライアント hydration、開発ツールでのコンポーネント確認までが一つの完全なチェーンです。",[16,4160,4161],{},"そのため、プロジェクトを二種類に分けて考えます。",[36,4163,4165],{"id":4164},"通常の-vue-vite-アプリケーション","通常の Vue \u002F Vite アプリケーション",[16,4167,4168],{},"ブランチ上で 3.6 RC へアップグレードし、既存 VDOM モードを先に検証してから、一つの leaf ページで Vapor を試します。依存関係が比較的直接的なので、問題の原因も特定しやすくなります。",[36,4170,4172],{"id":4171},"nuxtssrまたはコンポーネントライブラリを多用するアプリ","Nuxt、SSR、またはコンポーネントライブラリを多用するアプリ",[16,4174,4175],{},"package override で下層 Vue だけを RC に変え、Vapor 移行が完了したと考えないでください。フレームワーク、build plugin、SSR チェーンが対象バージョンを明示的にサポートすることを確認してから、hydration、ルート遷移、非同期コンポーネント、コンポーネントライブラリとの相互運用をテストします。",[16,4177,4178],{},"Vapor を使わなくても、Vue 3.6 の新しいリアクティビティコアを試す価値はあります。ただし独立ブランチかテスト環境に置くのが安全です。",[29,4180,4181],{"id":4181},"より安全なアップグレード手順",[36,4183,4185],{"id":4184},"第-1-段階vapor-を有効にせず依存関係だけを更新","第 1 段階：Vapor を有効にせず、依存関係だけを更新",[16,4187,4188],{},"通常の Vue \u002F Vite プロジェクトでは、実験ブランチに RC をインストールできます。",[80,4190,4192],{"className":2542,"code":4191,"language":2544,"meta":85,"style":85},"pnpm add vue@3.6.0-rc.2\n",[75,4193,4194],{"__ignoreMap":85},[89,4195,4196,4198,4201],{"class":91,"line":92},[89,4197,2551],{"class":99},[89,4199,4200],{"class":153}," add",[89,4202,4203],{"class":153}," vue@3.6.0-rc.2\n",[16,4205,4206,4209],{},[75,4207,4208],{},"@vue\u002Fserver-renderer"," に直接依存している場合は Vue と同じバージョンに揃えます。",[80,4211,4213],{"className":2542,"code":4212,"language":2544,"meta":85,"style":85},"pnpm add vue@3.6.0-rc.2 @vue\u002Fserver-renderer@3.6.0-rc.2\n",[75,4214,4215],{"__ignoreMap":85},[89,4216,4217,4219,4221,4224],{"class":91,"line":92},[89,4218,2551],{"class":99},[89,4220,4200],{"class":153},[89,4222,4223],{"class":153}," vue@3.6.0-rc.2",[89,4225,4226],{"class":153}," @vue\u002Fserver-renderer@3.6.0-rc.2\n",[16,4228,4229],{},"まず従来の VDOM アプリを検証します。",[226,4231,4232,4235,4238,4247,4250,4253,4256,4259],{},[229,4233,4234],{},"unit test と component test。",[229,4236,4237],{},"初期 SSR とクライアント hydration。",[229,4239,4240,4241,2922,4243,2922,4245,2474],{},"複雑な ",[75,4242,2928],{},[75,4244,2925],{},[75,4246,3354],{},[229,4248,4249],{},"高速なルート遷移後の副作用 cleanup。",[229,4251,4252],{},"カスタム directive、Teleport、Suspense、非同期コンポーネント。",[229,4254,4255],{},"開発 build と本番 build。",[229,4257,4258],{},"ブラウザコンソールの warning と error。",[229,4260,4261],{},"重要な操作の時間とメモリ基準。",[16,4263,4264],{},"この段階で失敗した場合、原因は Vapor よりも新しいリアクティビティ実装や RC の回帰である可能性が高く、切り分けが簡単になります。",[36,4266,4268],{"id":4267},"第-2-段階境界が明確な領域を一つ選ぶ","第 2 段階：境界が明確な領域を一つ選ぶ",[16,4270,4271],{},"良い試験領域には通常、次の条件がそろっています。",[226,4273,4274,4277,4280,4283,4286,4289],{},[229,4275,4276],{},"Composition API を使っている。",[229,4278,4279],{},"コンポーネントインスタンスへの依存が少ない。",[229,4281,4282],{},"複雑な VDOM コンポーネントライブラリへ依存しない。",[229,4284,4285],{},"更新頻度が高く、実際の性能負荷がある。",[229,4287,4288],{},"独立したルートまたは明確なコンポーネントツリー境界がある。",[229,4290,4291],{},"再現可能な性能テストがある。",[16,4293,4294],{},"移行前の JavaScript サイズ、操作遅延、CPU 時間、メモリを記録してから Vapor を有効にします。基準がなければ「技術的に動いた」を「移行する価値があった」と誤認しやすくなります。",[36,4296,4298],{"id":4297},"第-3-段階相互運用と-ssr-を検証","第 3 段階：相互運用と SSR を検証",[16,4300,4301],{},"最低でも次の組み合わせを確認します。",[323,4303,4304,4307,4310,4313,4316,4319,4322],{},[229,4305,4306],{},"VDOM 親コンポーネントに Vapor 子コンポーネント。",[229,4308,4309],{},"Vapor 親コンポーネントに VDOM 子コンポーネント。",[229,4311,4312],{},"props、emit、双方向 binding。",[229,4314,4315],{},"default、named、scoped slot。",[229,4317,4318],{},"Suspense、非同期コンポーネント、error boundary。",[229,4320,4321],{},"サーバー出力、hydration、mismatch recovery。",[229,4323,4324],{},"unmount 後の watcher、event、directive cleanup。",[16,4326,4327],{},"rc.2 の修正には hydration、Suspense、slot anchor、VDOM interop に集中する項目が多くあります。これらの領域が急速に収束している証拠であると同時に、今最も重点的にテストすべき場所でもあります。",[36,4329,4331],{"id":4330},"第-4-段階安定版を待って本番スケジュールを決める","第 4 段階：安定版を待って本番スケジュールを決める",[16,4333,4334],{},"RC は機能セットがほぼ確定し、回帰と境界問題の発見へ重点が移った状態です。エコシステム全体が安定したという意味ではありません。",[16,4336,4337],{},"本番プロジェクトは少なくとも次の条件を満たす必要があります。",[226,4339,4340,4343,4346,4349,4352,4355],{},[229,4341,4342],{},"Vue 3.6 の安定版が公開されている。",[229,4344,4345],{},"利用するフレームワークと build tool が明示的に対応している。",[229,4347,4348],{},"主要コンポーネントライブラリが相互運用テストを通過している。",[229,4350,4351],{},"DevTools、テストツール、SSR チェーンが利用できる。",[229,4353,4354],{},"性能効果が移行・保守コストを上回る。",[229,4356,4357],{},"いつでも VDOM へ戻せる。",[29,4359,4360],{"id":4360},"推奨しない四つのこと",[36,4362,4364],{"id":4363},"_1-rc-を通常の-patch-と同じように扱わない","1. RC を通常の patch と同じように扱わない",[16,4366,4367],{},"プレリリースでは動作が変わる可能性があります。rc.1 から rc.2 へのイベント委譲の変更が最も分かりやすい例です。",[36,4369,4371],{"id":4370},"_2-vapor-を使うために全コンポーネントを移行しない","2. 「Vapor を使う」ために全コンポーネントを移行しない",[16,4373,4374],{},"ナビゲーション、静的コンテンツ、更新頻度の低い設定ページでは意味のある効果を得られないかもしれません。移行率ではなく性能 bottleneck を優先します。",[36,4376,4378],{"id":4377},"_3-マイクロベンチマークをアプリの約束にしない","3. マイクロベンチマークをアプリの約束にしない",[16,4380,4381],{},"「30 倍超」は特定のリアクティブストレスケース、「Solid や Svelte 5 と同水準」は第三者 benchmark の結果です。技術的な方向性は証明しますが、プロジェクト自身の測定には代えられません。",[36,4383,4385],{"id":4384},"_4-エコシステム境界を無視しない","4. エコシステム境界を無視しない",[16,4387,4388],{},"Vue core、SFC compiler、Vite plugin、Nuxt、SSR、Router、Pinia、コンポーネントライブラリ、DevTools、テストツールが一緒に開発体験を構成します。一つの package を RC にするだけでは、チェーン全体の更新にはなりません。",[29,4390,4391],{"id":4391},"プロジェクト種別ごとの提案",[254,4393,4394,4407],{},[257,4395,4396],{},[260,4397,4398,4401,4404],{},[263,4399,4400],{},"プロジェクト種別",[263,4402,4403],{},"今試す価値",[263,4405,4406],{},"提案",[270,4408,4409,4420,4431,4442,4453,4464],{},[260,4410,4411,4414,4417],{},[275,4412,4413],{},"小規模な新規ツール、社内実験",[275,4415,4416],{},"ある",[275,4418,4419],{},"純 Vapor を試し、bundle サイズと操作性能を測る",[260,4421,4422,4425,4428],{},[275,4423,4424],{},"既存 Vue SPA",[275,4426,4427],{},"ブランチ上で試す価値がある",[275,4429,4430],{},"まず VDOM を維持し、性能が重要な一ページだけを移行",[260,4432,4433,4436,4439],{},[275,4434,4435],{},"Nuxt \u002F SSR サイト",[275,4437,4438],{},"研究には適するが本番は待つ",[275,4440,4441],{},"フレームワーク対応を確認し hydration を重点的にテスト",[260,4443,4444,4447,4450],{},[275,4445,4446],{},"コンポーネントライブラリ",[275,4448,4449],{},"早期に互換テストを始める",[275,4451,4452],{},"instance proxy、slots、カスタム directive、interop を確認",[260,4454,4455,4458,4461],{},[275,4456,4457],{},"大規模ダッシュボード \u002F エディター",[275,4459,4460],{},"stress test の価値が高い",[275,4462,4463],{},"alien-signals と Vapor それぞれの効果を分けて比較",[260,4465,4466,4469,4472],{},[275,4467,4468],{},"コンテンツ中心のサイト",[275,4470,4471],{},"優先度は低い",[275,4473,4474],{},"限定的な効果のために複雑さを増やさず、安定版と成熟したエコシステムを待つ",[29,4476,2714],{"id":2714},[16,4478,4479],{},"Vue 3.6 RC の最も価値ある点は、業務コードですぐ使える新 API の数ではなく、二つの長期的な問題を同時に進めていることです。",[226,4481,4482,4485],{},[229,4483,4484],{},"Vue のリアクティビティは、より複雑な依存グラフでも効率を保てるか。",[229,4486,4487],{},"Vue の template は、慣れた開発体験を保ちながら Virtual DOM のコストを必須ではなくせるか。",[16,4489,4490],{},"alien-signals は一つ目に答え、Vapor は二つ目に答えようとしています。",[16,4492,4493,4494,4497],{},"既存プロジェクトで最も合理的な戦略は、",[22,4495,4496],{},"まず 3.6 をリアクティビティコアの更新としてテストし、その後 Vapor を任意の局所コンパイルモードとして評価すること","です。二つを分けて検証すれば、性能変化の原因を理解でき、リスクを明確な境界内に保てます。",[16,4499,4500],{},"Vue 3.6 はまだ「全員が今日アップグレードすべき」段階ではありません。しかし「すべての Vue チームが理解を始める価値がある」段階には達しています。",[4502,4503],"hr",{},[29,4505,4506],{"id":4506},"資料と注記",[226,4508,4509,4517,4525,4533,4541,4549],{},[229,4510,4511,4516],{},[2756,4512,4515],{"href":4513,"rel":4514},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fblob\u002Fminor\u002FCHANGELOG.md",[2760],"Vue 3.6 minor ブランチ Changelog","：3.6.0-rc.1 の Vapor 概要と互換性境界、rc.2 のイベント委譲変更。",[229,4518,4519,4524],{},[2756,4520,4523],{"href":4521,"rel":4522},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Freleases\u002Ftag\u002Fv3.6.0-rc.2",[2760],"Vue 3.6.0-rc.2 Release","：この記事公開時点の最新リリース候補。",[229,4526,4527,4532],{},[2756,4528,4531],{"href":4529,"rel":4530},"https:\u002F\u002Fblog.vuejs.org\u002Fposts\u002Fvue-3-5",[2760],"Vue 3.5 リリースノート","：Vue 3.5 のリアクティビティ改善、SSR、開発者 API の基準。",[229,4534,4535,4540],{},[2756,4536,4539],{"href":4537,"rel":4538},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fpull\u002F12349",[2760],"Vue core：alien-signals 再構築 PR","：リアクティビティ実装、メモリ、特定ストレスケースのマイクロベンチマーク。",[229,4542,4543,4548],{},[2756,4544,4547],{"href":4545,"rel":4546},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fissues\u002F13687",[2760],"Vapor Mode Roadmap","：コア機能と Vue エコシステム統合のロードマップ。",[229,4550,4551,4556],{},[2756,4552,4555],{"href":4553,"rel":4554},"https:\u002F\u002Fgithub.com\u002Fkrausest\u002Fjs-framework-benchmark",[2760],"js-framework-benchmark","：Vue 公式リリースノートが参照した第三者フロントエンド benchmark。",[484,4558,4559],{},[16,4560,4561],{},"この記事のアップグレード提案は、公式資料とエンジニアリング経験に基づく私の判断です。AI が資料整理と言語校正を補助しましたが、各プロジェクトでは自身のテスト結果を基準にしてください。",[388,4563,4564],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sxg3X, html code.shiki .sxg3X{--shiki-default:#85E89D;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sX7ps, html code.shiki .sX7ps{--shiki-default:#FDAEB7;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":85,"searchDepth":124,"depth":124,"links":4566},[4567,4568,4569,4574,4579,4580,4585,4589,4595,4601,4602,4603],{"id":2944,"depth":110,"text":2945},{"id":3095,"depth":110,"text":3096},{"id":3146,"depth":110,"text":3147,"children":4570},[4571,4572,4573],{"id":3287,"depth":124,"text":3287},{"id":3316,"depth":124,"text":3316},{"id":3339,"depth":124,"text":3339},{"id":3378,"depth":110,"text":3379,"children":4575},[4576,4577,4578],{"id":3399,"depth":124,"text":3400},{"id":3593,"depth":124,"text":3594},{"id":3653,"depth":124,"text":3654},{"id":3746,"depth":110,"text":3747},{"id":3848,"depth":110,"text":3849,"children":4581},[4582,4584],{"id":3991,"depth":124,"text":4583},"slots.default() を副作用のない確認に使わない",{"id":4131,"depth":124,"text":4132},{"id":4151,"depth":110,"text":4152,"children":4586},[4587,4588],{"id":4164,"depth":124,"text":4165},{"id":4171,"depth":124,"text":4172},{"id":4181,"depth":110,"text":4181,"children":4590},[4591,4592,4593,4594],{"id":4184,"depth":124,"text":4185},{"id":4267,"depth":124,"text":4268},{"id":4297,"depth":124,"text":4298},{"id":4330,"depth":124,"text":4331},{"id":4360,"depth":110,"text":4360,"children":4596},[4597,4598,4599,4600],{"id":4363,"depth":124,"text":4364},{"id":4370,"depth":124,"text":4371},{"id":4377,"depth":124,"text":4378},{"id":4384,"depth":124,"text":4385},{"id":4391,"depth":110,"text":4391},{"id":2714,"depth":110,"text":2714},{"id":4506,"depth":110,"text":4506},"Vue 3.6.0-rc.2 時点で、Vue 3.5 と 3.6 のリアクティビティコア、Vapor Mode、イベント委譲、相互運用境界、段階的な移行戦略を比較します。",{"src":4606,"alt":4607,"caption":4608,"width":412,"height":413},"\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Fcover-v1.webp","Vue 3.5 の Virtual DOM コンポーネントツリーが結晶の通路を抜け、Vue 3.6 の細粒度シグナルネットワークへ移行する","Vue 3.6 RC は新 API の一覧というより、コンパイラとランタイムのアーキテクチャを切り替える更新です",{},"\u002Fblog\u002Fvue-3-6-rc-upgrade-guide","2026-07-26",{"title":2905,"description":4604},"blog\u002Fvue-3-6-rc-upgrade-guide",[4615,4616,4617],"Vue","JavaScript","フロントエンド","Nh6ytLUqnzdHOnmVTD55dslpQ84UZ1_-faPlGdYtf_o",{"id":4620,"title":4621,"accent":11,"body":4622,"description":4857,"draft":405,"experience":4858,"extension":407,"featured":415,"image":4863,"meta":4867,"navigation":415,"path":4868,"publishedAt":4869,"readingTime":194,"seo":4870,"stem":4871,"tags":4872,"updatedAt":406,"video":406,"__hash__":4875,"contentLocale":6,"isFallback":405},"posts_ja\u002Fblog\u002Ffrom-gpt-4-invite-to-agents.md","一通の GPT‑4 招待メールから、AI エージェントの時代へ",{"type":13,"value":4623,"toc":4849},[4624,4627,4630,4635,4642,4648,4651,4654,4658,4661,4664,4670,4673,4676,4680,4683,4697,4700,4717,4723,4726,4729,4733,4736,4739,4753,4756,4770,4773,4777,4780,4783,4800,4803,4807,4810,4813,4816,4819,4821,4823],[16,4625,4626],{},"今日、古いメールを整理していると、2023 年 3 月 18 日に届いた一通のメールが再び目に入りました。",[16,4628,4629],{},"件名はとても簡潔でした。",[484,4631,4632],{},[16,4633,4634],{},"You’re invited to use the OpenAI GPT-4 API.",[16,4636,4637,4638,4641],{},"本文には、",[22,4639,4640],{},"8K のコンテキスト","を持つ GPT‑4 を API から利用できるようになったと書かれていました。",[16,4643,4644],{},[568,4645],{"alt":4646,"src":4647},"8K コンテキストを利用できると記された、2023 年の GPT‑4 API 招待メール","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Femail-invite-v1.webp",[16,4649,4650],{},"GPT‑4 の公開から、まだ数日しか経っていない頃です。API を使うには申請して待ち、OpenAI から段階的に招待される必要がありました。公開時の資料では、標準版 GPT‑4 のコンテキスト長は 8,192 トークンで、32,768 トークン版は限定的に提供されていました。当時の私には、API の利用権そのものが、開いたばかりの小さな扉のように感じられました。",[16,4652,4653],{},"3 年以上が過ぎた今、このメールは製品のお知らせというより、一つのタイムカプセルに見えます。",[29,4655,4657],{"id":4656},"数字が大きくなったことはもっとも見えやすい変化にすぎない","数字が大きくなったことは、もっとも見えやすい変化にすぎない",[16,4659,4660],{},"初期の開発者にとって、4K や 8K のコンテキストは身近な制約でした。プロンプトを書くたびに残りの容量を数え、資料を削り、長い文章を分割し、前のリクエストの結果を次へ引き継ぐ方法を考えていました。",[16,4662,4663],{},"2026 年になると、100 万トークン規模のコンテキストは最先端モデルの主流に入りました。OpenAI の現在のモデル一覧では、GPT‑5.6 系列に約 105 万トークンのコンテキストウィンドウが記載されています。Google の長文コンテキスト資料でも、複数の Gemini モデルが 100 万トークン以上に対応すると説明されています。",[16,4665,4666],{},[568,4667],{"alt":4668,"src":4669},"4K、8K から 100 万トークン以上へ。短いメモがマルチモーダルな資料回廊へ伸びていく","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fcontext-window-v1.webp",[16,4671,4672],{},"これは単に「より多くの文章を詰め込める」という話ではありません。コード、画像、音声、動画、大量の文書を、一つのタスクの背景として扱えるようになりました。以前なら分割、索引化、要約が必要だった資料を、同じ作業コンテキストに置ける可能性が広がっています。",[16,4674,4675],{},"しかし、長いコンテキストは完全な記憶ではなく、モデルが自動的に重要な点を見抜く保証でもありません。資料が増えれば、ノイズ、コスト、遅延、誤った関連付けも増えます。重要なのは今も、どの情報を選び、どの順序で渡し、モデルが重要な制約を理解したかをどう検証するかです。",[29,4677,4679],{"id":4678},"本当に変わったのはやり取りの単位","本当に変わったのは、やり取りの単位",[16,4681,4682],{},"3 年前、私がよく行っていたのは「質問と回答」でした。",[323,4684,4685,4688,4691,4694],{},[229,4686,4687],{},"プロンプトを書く。",[229,4689,4690],{},"モデルの生成を待つ。",[229,4692,4693],{},"結果をコピーする。",[229,4695,4696],{},"残りの作業を自分で終える。",[16,4698,4699],{},"今では、より多くのタスクが「目標、文脈、行動、検証」という形になっています。",[323,4701,4702,4705,4708,4711,4714],{},[229,4703,4704],{},"最終目標と完了条件を示す。",[229,4706,4707],{},"リポジトリ、文書、現在の状態を渡す。",[229,4709,4710],{},"検索、ファイル、ターミナルなどのツール利用を許可する。",[229,4712,4713],{},"相互に依存する一連の手順を実行させる。",[229,4715,4716],{},"テスト、ビルド、差分確認、人のレビューで結果を確かめる。",[16,4718,4719],{},[568,4720],{"alt":4721,"src":4722},"チャット欄が文書、コード、ブラウザ、ツール呼び出し、人の確認を含むエージェントのワークフローへ広がる","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fagent-workflow-v1.webp",[16,4724,4725],{},"OpenAI の現在のモデルおよび Responses API の資料には、関数呼び出し、ウェブ検索、ファイル検索、コンピューター操作が利用可能なツールとして記載されています。モデルは「どうすればよいか」を説明するだけでなく、与えられた権限の範囲で実際の作業に参加し始めました。",[16,4727,4728],{},"これが、私の考える「チャットモデル」と「エージェント」のもっとも重要な違いです。前者はコンテンツを返し、後者は状態が変化するタスクを前へ進めます。",[29,4730,4732],{"id":4731},"プログラマーの仕事は消えず両端へ移動した","プログラマーの仕事は消えず、両端へ移動した",[16,4734,4735],{},"モデルがコードを読み、ファイルを編集し、コマンドを実行し、結果を確認できるようになっても、プログラマーが不要になるわけではありません。むしろ私たちの注意は、タスクの前後へ移っていきます。",[16,4737,4738],{},"行動の前には、問題をより明確に定義する必要があります。",[226,4740,4741,4744,4747,4750],{},[229,4742,4743],{},"目標は何か。何が対象外なのか。",[229,4745,4746],{},"どの資料を信頼でき、どれは参考にすぎないのか。",[229,4748,4749],{},"どの権限を渡せるか。どの操作は確認を必要とするか。",[229,4751,4752],{},"失敗したとき、停止、再試行、ロールバックのどれを選ぶか。",[16,4754,4755],{},"行動の後には、結果をより確実に検証する必要があります。",[226,4757,4758,4761,4764,4767],{},[229,4759,4760],{},"コードはテストとビルドを通過するか。",[229,4762,4763],{},"ページは本当に期待どおりか。「生成できた」だけではないか。",[229,4765,4766],{},"外部の状態は実際に変わったか。",[229,4768,4769],{},"結果を説明、再現、監査できるか。",[16,4771,4772],{},"3 年前、私たちはより良いプロンプトの書き方を学んでいました。今はそれに加えて、文脈、ツール、権限、チェックポイント、受け入れ条件の設計も学ぶ必要があります。",[29,4774,4776],{"id":4775},"能力は委ねられても責任は外部化できない","能力は委ねられても、責任は外部化できない",[16,4778,4779],{},"エージェントが現実の操作を実行できるほど、境界は重要になります。",[16,4781,4782],{},"記事の要約を頼むことと、本番データベースの変更を許可することは、同じ権限ではありません。メールの下書きを頼むことと、顧客へ直接送信させることも、同じリスクではありません。成熟したエージェントシステムは「何ができるか」だけでなく、次の点を明確にするべきです。",[226,4784,4785,4788,4791,4794,4797],{},[229,4786,4787],{},"デフォルトは読み取り専用か、書き込み可能か。",[229,4789,4790],{},"実行前に人の確認が必要な操作は何か。",[229,4792,4793],{},"対象、ディレクトリ、アカウント、データ範囲をどう制限するか。",[229,4795,4796],{},"必要な操作履歴をどう保存するか。",[229,4798,4799],{},"問題が起きたとき、どう停止し復旧するか。",[16,4801,4802],{},"モデルがより多くの行動を担っても、判断と責任は消えません。なぜ行うのか、根拠は何か、誰がリスクを負うのか、結果を受け入れられるかという問いには、最後まで人が答える必要があります。",[29,4804,4806],{"id":4805},"_3-年後私たちは今日をどう見るだろう","3 年後、私たちは今日をどう見るだろう",[16,4808,4809],{},"この古いメールは、偶然残しておいた一枚の切符のようです。",[16,4811,4812],{},"2023 年、私が申請していたのはモデルへアクセスする機会でした。2026 年には、長期目標を理解させ、ツールを使わせ、複数の工程を進め、人の監督のもとで仕事を完了させる方法を学んでいます。",[16,4814,4815],{},"心を動かされるのは、パラメーターが何倍になったかではなく、時間が圧縮されたことです。遠い未来にしかないと思われた能力は、何十年もかけて少しずつ来たのではありません。数回のバージョン更新の間に、静かに日常へ入り込みました。",[16,4817,4818],{},"さらに 3 年が経てば、今の私たちを驚かせる 100 万トークンのコンテキストやエージェントも、このメールと同じように見えるかもしれません。未来が始まったばかりの瞬間に残された、古い品物として。",[4502,4820],{},[29,4822,2750],{"id":2750},[226,4824,4825,4833,4841],{},[229,4826,4827,4832],{},[2756,4828,4831],{"href":4829,"rel":4830},"https:\u002F\u002Fopenai.com\u002Findex\u002Fgpt-4-research\u002F",[2760],"OpenAI: GPT‑4 research"," — GPT‑4 は 2023 年 3 月に公開され、標準のコンテキストは 8,192 トークン、32,768 トークン版は限定提供でした。",[229,4834,4835,4840],{},[2756,4836,4839],{"href":4837,"rel":4838},"https:\u002F\u002Fdevelopers.openai.com\u002Fapi\u002Fdocs\u002Fmodels",[2760],"OpenAI: model catalog"," — 記事公開時点で、最先端の GPT‑5.6 系列には約 105 万トークンのコンテキストと複数カテゴリのツール対応が記載されています。",[229,4842,4843,4848],{},[2756,4844,4847],{"href":4845,"rel":4846},"https:\u002F\u002Fai.google.dev\u002Fgemini-api\u002Fdocs\u002Flong-context",[2760],"Google AI: Long context"," — 100 万トークン以上のコンテキストを持つ Gemini モデルの用途と制約を説明しています。",{"title":85,"searchDepth":124,"depth":124,"links":4850},[4851,4852,4853,4854,4855,4856],{"id":4656,"depth":110,"text":4657},{"id":4678,"depth":110,"text":4679},{"id":4731,"depth":110,"text":4732},{"id":4775,"depth":110,"text":4776},{"id":4805,"depth":110,"text":4806},{"id":2750,"depth":110,"text":2750},"2023 年に待っていたのは 8K コンテキストの API 利用権でした。3 年後の今、私たちはエージェントに目標、権限、境界、完了条件を伝える方法を学んでいます。",{"src":4859,"title":4860,"description":4861,"duration":4862,"hasAudio":415},"\u002Fexperiences\u002Ffrom-gpt-4-invite-to-agents","GPT-4 の招待からエージェント時代へ · 6 幕のインタラクティブストーリー","コンテキスト、エージェントのワークフロー、能力と責任の変化を 2 分 26 秒で振り返ります。ナレーションは中国語です。",146,{"src":4864,"alt":4865,"caption":4866,"width":412,"height":413},"\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fcover-ja-v1.webp","2023 年のメールがガラスのタイムカプセルから現代のエージェント用ワークスペースへとつながる","3 年前はモデルへのアクセスを申請し、今はモデルと協働する方法を学んでいる",{},"\u002Fblog\u002Ffrom-gpt-4-invite-to-agents","2026-07-25",{"title":4621,"description":4857},"blog\u002Ffrom-gpt-4-invite-to-agents",[421,4873,4874],"GPT-4","エージェント","MMkyyNWTPG5slI4EvXqp_pD8TJrZjZ-_JOgC08cs05o",{"id":4877,"title":4878,"accent":429,"body":4879,"description":5317,"draft":405,"experience":406,"extension":407,"featured":415,"image":5318,"meta":5322,"navigation":415,"path":5323,"publishedAt":5324,"readingTime":194,"seo":5325,"stem":5326,"tags":5327,"updatedAt":5328,"video":406,"__hash__":5329,"contentLocale":6,"isFallback":405},"posts_ja\u002Fblog\u002Fedge-stateful-apps.md","エッジでステートフルなアプリケーションを構築する",{"type":13,"value":4880,"toc":5309},[4881,4888,4891,4894,4897,4947,4950,4955,4958,4962,4965,5035,5038,5120,5123,5126,5129,5206,5221,5224,5227,5230,5233,5244,5247,5250,5253,5256,5270,5273,5277,5280,5283,5300,5306],[16,4882,4883,4884,4887],{},"エッジコンピューティングの最も魅力的な約束は、コードをユーザーの近くへ届けることです。しかし、アプリケーションがアカウント、セッション、コメントを保存し始めると、問題は単に「関数をより多くの拠点へデプロイする」ことではなくなります。",[22,4885,4886],{},"状態は実際にどこにあり、誰が責任を持ち、障害時にシステムはどう縮退するのか","を考えなければなりません。",[16,4889,4890],{},"この記事では、ログインとコメント機能を備えたブログを最小の例として、Cloudflare Workers と D1 でステートフルなアプリケーションを構築するときに重要な設計境界を整理します。",[29,4892,4893],{"id":4893},"まず状態の地図を描く",[16,4895,4896],{},"小さく見えるブログにも、少なくとも三種類のまったく異なる状態があります。",[254,4898,4899,4912],{},[257,4900,4901],{},[260,4902,4903,4906,4909],{},[263,4904,4905],{},"状態",[263,4907,4908],{},"保存元",[263,4910,4911],{},"ライフサイクル",[270,4913,4914,4925,4936],{},[260,4915,4916,4919,4922],{},[275,4917,4918],{},"記事本文",[275,4920,4921],{},"Git 内の Markdown",[275,4923,4924],{},"デプロイとともに更新",[260,4926,4927,4930,4933],{},[275,4928,4929],{},"ユーザーとコメント",[275,4931,4932],{},"D1",[275,4934,4935],{},"長期的に永続化",[260,4937,4938,4941,4944],{},[275,4939,4940],{},"ログインセッション",[275,4942,4943],{},"D1 + Cookie",[275,4945,4946],{},"明確な有効期限",[16,4948,4949],{},"記事コンテンツはコードのバージョンと一緒に管理するのが適しています。レビュー、ロールバック、ビルド時検証は必要ですが、読者が実行時に変更する必要はありません。ユーザーデータはその逆で、デプロイのライフサイクルから独立して存在し続けなければなりません。",[484,4951,4952],{},[16,4953,4954],{},"データベースを選ぶ前に、そのデータがコードのバージョンに従うべきか、ユーザーの行動に従うべきかを判断します。",[16,4956,4957],{},"この区別から、自然にシンプルな構成が導かれます。Nuxt Content が Markdown を、D1 が実行時の業務データを担当し、Worker が一つのリクエストの中で両者を組み合わせます。",[29,4959,4961],{"id":4960},"ネットワークリクエストより-binding-が重要","ネットワークリクエストより binding が重要",[16,4963,4964],{},"Worker が D1 にアクセスするために API Token を保存する必要はなく、実行時に Cloudflare REST API を呼び出すべきでもありません。D1 は binding を通じてランタイムへ直接渡されます。",[80,4966,4969],{"className":4967,"code":4968,"filename":1489,"language":1780,"meta":85,"style":85},"language-json shiki shiki-themes github-dark github-dark","{\n  \"d1_databases\": [\n    {\n      \"binding\": \"DB\",\n      \"database_name\": \"signal-log-db\",\n      \"database_id\": \"\u003CYOUR_DATABASE_ID>\"\n    }\n  ]\n}\n",[75,4970,4971,4975,4983,4987,5000,5012,5022,5026,5031],{"__ignoreMap":85},[89,4972,4973],{"class":91,"line":92},[89,4974,1502],{"class":106},[89,4976,4977,4980],{"class":91,"line":110},[89,4978,4979],{"class":120},"  \"d1_databases\"",[89,4981,4982],{"class":106},": [\n",[89,4984,4985],{"class":91,"line":124},[89,4986,1512],{"class":106},[89,4988,4989,4992,4995,4998],{"class":91,"line":134},[89,4990,4991],{"class":120},"      \"binding\"",[89,4993,4994],{"class":106},": ",[89,4996,4997],{"class":153},"\"DB\"",[89,4999,1040],{"class":106},[89,5001,5002,5005,5007,5010],{"class":91,"line":145},[89,5003,5004],{"class":120},"      \"database_name\"",[89,5006,4994],{"class":106},[89,5008,5009],{"class":153},"\"signal-log-db\"",[89,5011,1040],{"class":106},[89,5013,5014,5017,5019],{"class":91,"line":173},[89,5015,5016],{"class":120},"      \"database_id\"",[89,5018,4994],{"class":106},[89,5020,5021],{"class":153},"\"\u003CYOUR_DATABASE_ID>\"\n",[89,5023,5024],{"class":91,"line":183},[89,5025,959],{"class":106},[89,5027,5028],{"class":91,"line":194},[89,5029,5030],{"class":106},"  ]\n",[89,5032,5033],{"class":91,"line":215},[89,5034,218],{"class":106},[16,5036,5037],{},"サーバールートでは、データベースオブジェクトを現在のリクエストコンテキストから取得します。すべての SQL は prepared statement を使います。",[80,5039,5041],{"className":82,"code":5040,"language":84,"meta":85,"style":85},"const comment = await db\n  .prepare(`\n    SELECT id, body, created_at\n    FROM comments\n    WHERE post_slug = ?1 AND status = 'published'\n    ORDER BY created_at DESC\n    LIMIT 100\n  `)\n  .bind(postSlug)\n  .all()\n",[75,5042,5043,5057,5069,5074,5079,5084,5089,5094,5101,5111],{"__ignoreMap":85},[89,5044,5045,5047,5050,5052,5054],{"class":91,"line":92},[89,5046,1848],{"class":95},[89,5048,5049],{"class":120}," comment",[89,5051,103],{"class":95},[89,5053,1329],{"class":95},[89,5055,5056],{"class":106}," db\n",[89,5058,5059,5061,5064,5066],{"class":91,"line":110},[89,5060,3704],{"class":106},[89,5062,5063],{"class":99},"prepare",[89,5065,824],{"class":106},[89,5067,5068],{"class":153},"`\n",[89,5070,5071],{"class":91,"line":124},[89,5072,5073],{"class":153},"    SELECT id, body, created_at\n",[89,5075,5076],{"class":91,"line":134},[89,5077,5078],{"class":153},"    FROM comments\n",[89,5080,5081],{"class":91,"line":145},[89,5082,5083],{"class":153},"    WHERE post_slug = ?1 AND status = 'published'\n",[89,5085,5086],{"class":91,"line":173},[89,5087,5088],{"class":153},"    ORDER BY created_at DESC\n",[89,5090,5091],{"class":91,"line":183},[89,5092,5093],{"class":153},"    LIMIT 100\n",[89,5095,5096,5099],{"class":91,"line":194},[89,5097,5098],{"class":153},"  `",[89,5100,947],{"class":106},[89,5102,5103,5105,5108],{"class":91,"line":215},[89,5104,3704],{"class":106},[89,5106,5107],{"class":99},"bind",[89,5109,5110],{"class":106},"(postSlug)\n",[89,5112,5113,5115,5118],{"class":91,"line":905},[89,5114,3704],{"class":106},[89,5116,5117],{"class":99},"all",[89,5119,869],{"class":106},[16,5121,5122],{},"パラメータ binding は単なるコードスタイルではなく、入力境界の一部です。記事パス、メールアドレス、表示名、コメント本文はいずれもユーザー由来です。どの値も SQL 文字列へ連結してはいけません。",[29,5124,5125],{"id":5125},"セッションには不可逆なダイジェストだけを保存する",[16,5127,5128],{},"ログインに成功すると、ブラウザはランダムなセッショントークンを受け取ります。一方、データベースに保存するのは SHA-256 ダイジェストだけです。セッションテーブルが誤って公開されても、元のトークンをそのままログインに利用することはできません。",[80,5130,5132],{"className":82,"code":5131,"language":84,"meta":85,"style":85},"const token = createRandomToken()\nconst tokenHash = await sha256(token)\n\nawait db\n  .prepare('INSERT INTO sessions (token_hash, user_id, expires_at) VALUES (?1, ?2, ?3)')\n  .bind(tokenHash, userId, expiresAt)\n  .run()\n",[75,5133,5134,5148,5165,5169,5175,5188,5197],{"__ignoreMap":85},[89,5135,5136,5138,5141,5143,5146],{"class":91,"line":92},[89,5137,1848],{"class":95},[89,5139,5140],{"class":120}," token",[89,5142,103],{"class":95},[89,5144,5145],{"class":99}," createRandomToken",[89,5147,869],{"class":106},[89,5149,5150,5152,5155,5157,5159,5162],{"class":91,"line":110},[89,5151,1848],{"class":95},[89,5153,5154],{"class":120}," tokenHash",[89,5156,103],{"class":95},[89,5158,1329],{"class":95},[89,5160,5161],{"class":99}," sha256",[89,5163,5164],{"class":106},"(token)\n",[89,5166,5167],{"class":91,"line":124},[89,5168,803],{"emptyLinePlaceholder":415},[89,5170,5171,5173],{"class":91,"line":134},[89,5172,1954],{"class":95},[89,5174,5056],{"class":106},[89,5176,5177,5179,5181,5183,5186],{"class":91,"line":145},[89,5178,3704],{"class":106},[89,5180,5063],{"class":99},[89,5182,824],{"class":106},[89,5184,5185],{"class":153},"'INSERT INTO sessions (token_hash, user_id, expires_at) VALUES (?1, ?2, ?3)'",[89,5187,947],{"class":106},[89,5189,5190,5192,5194],{"class":91,"line":173},[89,5191,3704],{"class":106},[89,5193,5107],{"class":99},[89,5195,5196],{"class":106},"(tokenHash, userId, expiresAt)\n",[89,5198,5199,5201,5204],{"class":91,"line":183},[89,5200,3704],{"class":106},[89,5202,5203],{"class":99},"run",[89,5205,869],{"class":106},[16,5207,5208,5209,5212,5213,5216,5217,5220],{},"Cookie には ",[75,5210,5211],{},"HttpOnly"," を設定してフロントエンド JavaScript から読めないようにし、",[75,5214,5215],{},"SameSite=Lax"," でクロスサイトリクエストのリスクを下げ、本番環境ではさらに ",[75,5218,5219],{},"Secure"," を付けます。",[16,5222,5223],{},"これでシステムが「絶対に安全」になるわけではありません。より正確には、各レイヤーが仕事に必要な最小限の能力だけを持つようにしています。",[29,5225,5226],{"id":5226},"一貫性はユーザー操作を中心に設計する",[16,5228,5229],{},"D1 はリレーショナルデータベースですが、エッジアプリケーションでも、どのフローが強い一貫性を必要とするかを明確にする必要があります。",[16,5231,5232],{},"ブログの重要な操作は多くありません。",[323,5234,5235,5238,5241],{},[229,5236,5237],{},"登録直後にログインできること。",[229,5239,5240],{},"コメント送信の成功直後に現在のページへ表示されること。",[229,5242,5243],{},"ログアウト直後に古いセッションが無効になること。",[16,5245,5246],{},"これらのフローは、同じプライマリデータベースに書き込み、その直後の読み取りもそこから行うべきです。記事一覧や公開コメント一覧などの読み取り専用の場面にこそ、より大きなキャッシュ余地があります。",[16,5248,5249],{},"「どこをキャッシュできるか」ではなく、「ユーザーが今完了した約束は何か」から考えます。",[29,5251,5252],{"id":5252},"障害にもプロダクト設計が必要",[16,5254,5255],{},"エッジランタイムは失敗することがあり、データベースも一時的に利用できなくなることがあります。違いは、その失敗をユーザーが理解できる状態に変えられるかどうかです。",[226,5257,5258,5261,5264,5267],{},[229,5259,5260],{},"ログイン失敗時は「メールアドレスが存在しない」と「パスワードが違う」を区別せず、アカウント列挙を防ぎます。",[229,5262,5263],{},"コメント書き込みに失敗したら入力内容を保持し、再試行できるようにします。",[229,5265,5266],{},"コンテンツ取得に失敗したら、空白ページではなく正しいエラー状態を返します。",[229,5268,5269],{},"サーバーエラーは構造化ログに記録しますが、内部 SQL やスタックトレースをブラウザへ返しません。",[16,5271,5272],{},"信頼性とは「絶対に失敗しない」ことではなく、失敗が起きても境界が壊れないことです。",[29,5274,5276],{"id":5275},"最後に退屈なアーキテクチャを保つ","最後に、退屈なアーキテクチャを保つ",[16,5278,5279],{},"この構成にはメッセージキューも、分散ロックも、追加の認証サービスもありません。それらに価値がないからではなく、現在の問題がまだ必要としていないからです。",[16,5281,5282],{},"長期的に保守しやすい個人サイトの基礎は、たいてい次のようにシンプルです。",[226,5284,5285,5288,5291,5294,5297],{},[229,5286,5287],{},"コンテンツは Git とともに管理する。",[229,5289,5290],{},"実行時データは D1 に保存する。",[229,5292,5293],{},"シークレットは Worker secret に置く。",[229,5295,5296],{},"入力は境界で検証する。",[229,5298,5299],{},"障害を明確にし、観測可能かつ復旧可能にする。",[16,5301,5302,5303],{},"トラフィックやプロダクト要件が本当に変化したときに、証拠に合わせてアーキテクチャを進化させれば十分です。",[22,5304,5305],{},"エッジはすでに十分高速です。エンジニアリングの仕事は、その単純さを保つことです。",[388,5307,5308],{},"html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}",{"title":85,"searchDepth":124,"depth":124,"links":5310},[5311,5312,5313,5314,5315,5316],{"id":4893,"depth":110,"text":4893},{"id":4960,"depth":110,"text":4961},{"id":5125,"depth":110,"text":5125},{"id":5226,"depth":110,"text":5226},{"id":5252,"depth":110,"text":5252},{"id":5275,"depth":110,"text":5276},"Cloudflare Workers は高速で D1 は近くにあります。しかし本当に難しいのは、状態、一貫性、障害の境界を明確に設計することです。",{"src":5319,"alt":5320,"caption":5321,"width":412,"height":413},"\u002Fmedia\u002Fposts\u002Fedge-stateful-apps\u002Fcover-v1.webp","中央のデータベースを囲むエッジノードがステートフルなアプリケーショントポロジーを構成している","状態、エッジノード、一貫性の境界",{},"\u002Fblog\u002Fedge-stateful-apps","2026-07-21",{"title":4878,"description":5317},"blog\u002Fedge-stateful-apps",[2891,4932,423],"2026-07-24","45JW9j2ScEo7kmmOD41SBwFEzTUp3ok22gVd9dOmZ9k",{"id":5331,"title":5332,"accent":2906,"body":5333,"description":5718,"draft":405,"experience":406,"extension":407,"featured":405,"image":5719,"meta":5723,"navigation":415,"path":5724,"publishedAt":5725,"readingTime":173,"seo":5726,"stem":5727,"tags":5728,"updatedAt":406,"video":406,"__hash__":5731,"contentLocale":6,"isFallback":405},"posts_ja\u002Fblog\u002Fnuxt-4-architecture-subtraction.md","Nuxt 4 におけるアーキテクチャの引き算",{"type":13,"value":5334,"toc":5710},[5335,5338,5345,5349,5352,5403,5406,5413,5416,5419,5543,5546,5564,5567,5570,5573,5576,5579,5584,5588,5595,5601,5604,5607,5610,5613,5678,5681,5684,5687,5704,5707],[16,5336,5337],{},"Nuxt プロジェクトが複雑になる理由は、ビジネスが本当に複雑だからとは限りません。あらゆる将来の可能性を、あまりに早くアーキテクチャへ入れてしまうことがよくあります。",[16,5339,5340,5341,5344],{},"個人ブログは良い調整基準になります。ページ数は限られ、コンテンツ構造は安定し、インタラクションは少数の領域に集中しています。フレームワークの最も価値ある役割は、機能を増やすことではなく、",[22,5342,5343],{},"不要な選択肢を削除すること","だと気づかせてくれます。",[29,5346,5348],{"id":5347},"デフォルトの-ssr-から始める","デフォルトの SSR から始める",[16,5350,5351],{},"技術記事は検索エンジンに理解される必要があり、低速な回線でも本文をできるだけ早く表示したいものです。Nuxt のデフォルトであるサーバーサイドレンダリングは、その目的に合っています。",[80,5353,5356],{"className":82,"code":5354,"filename":5355,"language":84,"meta":85,"style":85},"export default defineNuxtConfig({\n  modules: ['@nuxt\u002Fcontent'],\n  nitro: {\n    preset: 'cloudflare',\n  },\n})\n","nuxt.config.ts",[75,5357,5358,5369,5380,5385,5395,5399],{"__ignoreMap":85},[89,5359,5360,5362,5364,5367],{"class":91,"line":92},[89,5361,808],{"class":95},[89,5363,811],{"class":95},[89,5365,5366],{"class":99}," defineNuxtConfig",[89,5368,1352],{"class":106},[89,5370,5371,5374,5377],{"class":91,"line":110},[89,5372,5373],{"class":106},"  modules: [",[89,5375,5376],{"class":153},"'@nuxt\u002Fcontent'",[89,5378,5379],{"class":106},"],\n",[89,5381,5382],{"class":91,"line":124},[89,5383,5384],{"class":106},"  nitro: {\n",[89,5386,5387,5390,5393],{"class":91,"line":134},[89,5388,5389],{"class":106},"    preset: ",[89,5391,5392],{"class":153},"'cloudflare'",[89,5394,1040],{"class":106},[89,5396,5397],{"class":91,"line":145},[89,5398,1113],{"class":106},[89,5400,5401],{"class":91,"line":173},[89,5402,1897],{"class":106},[16,5404,5405],{},"サイト全体を SPA に変える必要も、ページごとに別のデータ取得方法を発明する必要もありません。最初のクエリとレンダリングはサーバーで完了し、クライアントが引き継いだ後も同じルーターを使います。",[16,5407,5408,5409,5412],{},"ブラウザ API に依存する小さな領域だけ、局所的に ",[75,5410,5411],{},"ClientOnly"," を使えば十分です。一つのクライアントコンポーネントのために、ページ全体のセマンティクスとファーストビューを犠牲にしてはいけません。",[29,5414,5415],{"id":5415},"コンテンツクエリをページ境界に置く",[16,5417,5418],{},"ホーム、記事一覧、記事詳細では必要なデータ粒度が異なります。クエリは利用するページの近くに置きます。",[80,5420,5422],{"className":82,"code":5421,"language":84,"meta":85,"style":85},"const { data: posts } = await useAsyncData('latest-posts', () => {\n  return queryCollection('posts')\n    .where('draft', '=', false)\n    .order('publishedAt', 'DESC')\n    .limit(6)\n    .all()\n})\n",[75,5423,5424,5459,5473,5498,5517,5531,5539],{"__ignoreMap":85},[89,5425,5426,5428,5430,5433,5435,5438,5440,5442,5444,5447,5449,5452,5455,5457],{"class":91,"line":92},[89,5427,1848],{"class":95},[89,5429,1936],{"class":106},[89,5431,5432],{"class":113},"data",[89,5434,4994],{"class":106},[89,5436,5437],{"class":120},"posts",[89,5439,1941],{"class":106},[89,5441,1944],{"class":95},[89,5443,1329],{"class":95},[89,5445,5446],{"class":99}," useAsyncData",[89,5448,824],{"class":106},[89,5450,5451],{"class":153},"'latest-posts'",[89,5453,5454],{"class":106},", () ",[89,5456,1926],{"class":95},[89,5458,107],{"class":106},[89,5460,5461,5463,5466,5468,5471],{"class":91,"line":110},[89,5462,2117],{"class":95},[89,5464,5465],{"class":99}," queryCollection",[89,5467,824],{"class":106},[89,5469,5470],{"class":153},"'posts'",[89,5472,947],{"class":106},[89,5474,5475,5478,5481,5483,5486,5488,5491,5493,5496],{"class":91,"line":124},[89,5476,5477],{"class":106},"    .",[89,5479,5480],{"class":99},"where",[89,5482,824],{"class":106},[89,5484,5485],{"class":153},"'draft'",[89,5487,830],{"class":106},[89,5489,5490],{"class":153},"'='",[89,5492,830],{"class":106},[89,5494,5495],{"class":120},"false",[89,5497,947],{"class":106},[89,5499,5500,5502,5505,5507,5510,5512,5515],{"class":91,"line":134},[89,5501,5477],{"class":106},[89,5503,5504],{"class":99},"order",[89,5506,824],{"class":106},[89,5508,5509],{"class":153},"'publishedAt'",[89,5511,830],{"class":106},[89,5513,5514],{"class":153},"'DESC'",[89,5516,947],{"class":106},[89,5518,5519,5521,5524,5526,5529],{"class":91,"line":145},[89,5520,5477],{"class":106},[89,5522,5523],{"class":99},"limit",[89,5525,824],{"class":106},[89,5527,5528],{"class":120},"6",[89,5530,947],{"class":106},[89,5532,5533,5535,5537],{"class":91,"line":173},[89,5534,5477],{"class":106},[89,5536,5117],{"class":99},[89,5538,869],{"class":106},[89,5540,5541],{"class":91,"line":183},[89,5542,1897],{"class":106},[16,5544,5545],{},"この方法には直接的な利点があります。",[226,5547,5548,5555,5558,5561],{},[229,5549,5550,5551,5554],{},"データが SSR に参加し、",[75,5552,5553],{},"onMounted"," 後に点滅しながら表示されません。",[229,5556,5557],{},"クエリキーが明確なので、Nuxt は hydration 時に payload を再利用できます。",[229,5559,5560],{},"ページ自身が必要なものを理解し、汎用 store を経由する必要がありません。",[229,5562,5563],{},"下書きの除外と並び順が、本当に関係する場所に現れます。",[16,5565,5566],{},"複数のページが同じ複雑な業務状態を共有するようになったときにだけ、composable や store へ抽出する価値があります。",[29,5568,5569],{"id":5569},"安定して繰り返すものだけをコンポーネント化する",[16,5571,5572],{},"「再利用できそう」はコンポーネント境界ではありません。本当に安定した繰り返しには、通常、構造、意味、動作が同時に含まれます。",[16,5574,5575],{},"このブログでは記事カードが適切なコンポーネントです。ホームと一覧ページの両方に現れ、メタデータ、タイトル、概要、タグ、遷移動作が固定されています。",[16,5577,5578],{},"一方、ホームの Hero は大きくても、必ずしも五つの小さなコンポーネントへ分割する必要はありません。一つのページにしか現れず、内部要素がまとまって一つの視覚的な物語を作ります。早すぎる分割は、スタイルの関係を複数ファイルへ散らすだけです。",[484,5580,5581],{},[16,5582,5583],{},"コンポーネントはファイル整理の道具ではなく、独立して理解し再利用できる UI 契約です。",[29,5585,5587],{"id":5586},"サーバー-api-のインターフェースを狭く保つ","サーバー API のインターフェースを狭く保つ",[16,5589,5590,5591,5594],{},"ログインとコメントは実行時データなので ",[75,5592,5593],{},"server\u002Fapi"," に置きます。各 endpoint は一つの仕事だけを行います。",[80,5596,5599],{"className":5597,"code":5598,"language":381,"meta":85},[379],"POST \u002Fapi\u002Fauth\u002Fregister\nPOST \u002Fapi\u002Fauth\u002Flogin\nPOST \u002Fapi\u002Fauth\u002Flogout\nGET  \u002Fapi\u002Fauth\u002Fsession\nGET  \u002Fapi\u002Fcomments\u002F:post\nPOST \u002Fapi\u002Fcomments\u002F:post\n",[75,5600,5598],{"__ignoreMap":85},[16,5602,5603],{},"狭いインターフェースは認可規則を確認しやすくします。コメントの読み取りは公開、作成にはセッションが必要、ログアウトはユーザープロフィールに触れず現在のトークンだけを破棄します。",[16,5605,5606],{},"一つの endpoint が読み取り、書き込み、ログイン、形式変換を同時に扱うなら、すでに明確な境界を失っています。",[29,5608,5609],{"id":5609},"状態を正しい場所へ戻す",[16,5611,5612],{},"Vue のリアクティブな状態は便利ですが、すべての状態を JavaScript に置くべきではありません。",[254,5614,5615,5624],{},[257,5616,5617],{},[260,5618,5619,5621],{},[263,5620,4905],{},[263,5622,5623],{},"最適な場所",[270,5625,5626,5634,5644,5652,5663,5670],{},[260,5627,5628,5631],{},[275,5629,5630],{},"現在の記事",[275,5632,5633],{},"ルート",[260,5635,5636,5639],{},[275,5637,5638],{},"検索語",[275,5640,5641,5642],{},"ページの ",[75,5643,2921],{},[260,5645,5646,5649],{},[275,5647,5648],{},"タグ絞り込み",[275,5650,5651],{},"URL query + ページ状態",[260,5653,5654,5657],{},[275,5655,5656],{},"ログインユーザー",[275,5658,5659,5660],{},"SSR 安全な ",[75,5661,5662],{},"useState",[260,5664,5665,5667],{},[275,5666,4918],{},[275,5668,5669],{},"Nuxt Content",[260,5671,5672,5675],{},[275,5673,5674],{},"セッションの真正性",[275,5676,5677],{},"サーバー側の D1",[16,5679,5680],{},"状態を正しい場所へ置けば、大量の「同期ロジック」が自然に消えます。ルートを監視して store を修復する必要も、localStorage でログイン状態が信頼できるふりをする必要もなくなります。",[29,5682,5683],{"id":5683},"引き算は機能を減らすことではない",[16,5685,5686],{},"良いアーキテクチャの引き算は、ユーザーから何も奪いません。むしろ、ユーザーが実際に感じる部分へ複雑さの予算を残します。",[226,5688,5689,5692,5695,5698,5701],{},[229,5690,5691],{},"より明確なタイポグラフィ。",[229,5693,5694],{},"より速いファーストビュー。",[229,5696,5697],{},"より信頼できるログイン。",[229,5699,5700],{},"より快適なモバイル読書。",[229,5702,5703],{},"続けやすい Markdown 執筆フロー。",[16,5705,5706],{},"Nuxt 4 には十分な機能があります。成熟したプロジェクトとは、それらをすべて使うものではなく、今は使わなくてよい機能を知っているものです。",[388,5708,5709],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}",{"title":85,"searchDepth":124,"depth":124,"links":5711},[5712,5713,5714,5715,5716,5717],{"id":5347,"depth":110,"text":5348},{"id":5415,"depth":110,"text":5415},{"id":5569,"depth":110,"text":5569},{"id":5586,"depth":110,"text":5587},{"id":5609,"depth":110,"text":5609},{"id":5683,"depth":110,"text":5683},"コンテンツ中心のサイトに複雑なフロントエンド状態は不要です。ルーティング、SSR、コンテンツクエリを、それぞれ得意な場所へ戻します。",{"src":5720,"alt":5721,"caption":5722,"width":412,"height":413},"\u002Fmedia\u002Fposts\u002Fnuxt-4-architecture-subtraction\u002Fcover-v1.webp","複雑なモジュールが明確でコンパクトな Nuxt アプリケーションの中心へ収束していく","アーキテクチャの引き算で境界を明確にする",{},"\u002Fblog\u002Fnuxt-4-architecture-subtraction","2026-07-16",{"title":5332,"description":5718},"blog\u002Fnuxt-4-architecture-subtraction",[5729,4615,5730],"Nuxt","アーキテクチャ","ssY4mnEKHetrOcAiq0xs3OxgnqvkR3kHHmApyaxbxhc",{"id":9,"title":10,"accent":11,"body":5733,"description":404,"draft":405,"experience":406,"extension":407,"featured":405,"image":5993,"meta":5994,"navigation":415,"path":416,"publishedAt":417,"readingTime":183,"seo":5995,"stem":419,"tags":5996,"updatedAt":406,"video":406,"__hash__":424,"contentLocale":6,"isFallback":405},{"type":13,"value":5734,"toc":5980},[5735,5737,5741,5743,5745,5747,5749,5751,5753,5755,5757,5759,5761,5763,5765,5767,5771,5865,5867,5869,5881,5883,5885,5887,5929,5931,5933,5935,5947,5949,5951,5953,5955,5965,5967,5969,5971,5976,5978],[16,5736,18],{},[16,5738,5739],{},[22,5740,24],{},[16,5742,27],{},[29,5744,31],{"id":31},[16,5746,34],{},[36,5748,39],{"id":38},[16,5750,42],{},[36,5752,46],{"id":45},[16,5754,49],{},[36,5756,53],{"id":52},[16,5758,56],{},[36,5760,60],{"id":59},[16,5762,63],{},[16,5764,66],{},[29,5766,70],{"id":69},[16,5768,73,5769,78],{},[75,5770,77],{},[80,5772,5773],{"className":82,"code":83,"language":84,"meta":85,"style":85},[75,5774,5775,5785,5793,5801,5809,5829,5837,5845,5861],{"__ignoreMap":85},[89,5776,5777,5779,5781,5783],{"class":91,"line":92},[89,5778,96],{"class":95},[89,5780,100],{"class":99},[89,5782,103],{"class":95},[89,5784,107],{"class":106},[89,5786,5787,5789,5791],{"class":91,"line":110},[89,5788,114],{"class":113},[89,5790,117],{"class":95},[89,5792,121],{"class":120},[89,5794,5795,5797,5799],{"class":91,"line":124},[89,5796,127],{"class":113},[89,5798,117],{"class":95},[89,5800,121],{"class":120},[89,5802,5803,5805,5807],{"class":91,"line":134},[89,5804,137],{"class":113},[89,5806,140],{"class":95},[89,5808,121],{"class":120},[89,5810,5811,5813,5815,5817,5819,5821,5823,5825,5827],{"class":91,"line":145},[89,5812,148],{"class":113},[89,5814,117],{"class":95},[89,5816,154],{"class":153},[89,5818,157],{"class":95},[89,5820,160],{"class":153},[89,5822,157],{"class":95},[89,5824,165],{"class":153},[89,5826,157],{"class":95},[89,5828,170],{"class":153},[89,5830,5831,5833,5835],{"class":91,"line":173},[89,5832,176],{"class":113},[89,5834,117],{"class":95},[89,5836,121],{"class":120},[89,5838,5839,5841,5843],{"class":91,"line":183},[89,5840,186],{"class":113},[89,5842,140],{"class":95},[89,5844,191],{"class":120},[89,5846,5847,5849,5851,5853,5855,5857,5859],{"class":91,"line":194},[89,5848,197],{"class":113},[89,5850,117],{"class":95},[89,5852,202],{"class":153},[89,5854,157],{"class":95},[89,5856,207],{"class":153},[89,5858,157],{"class":95},[89,5860,212],{"class":153},[89,5862,5863],{"class":91,"line":215},[89,5864,218],{"class":106},[16,5866,221],{},[16,5868,224],{},[226,5870,5871,5873,5875,5877,5879],{},[229,5872,231],{},[229,5874,234],{},[229,5876,237],{},[229,5878,240],{},[229,5880,243],{},[29,5882,246],{"id":246},[16,5884,249],{},[16,5886,252],{},[254,5888,5889,5897],{},[257,5890,5891],{},[260,5892,5893,5895],{},[263,5894,265],{},[263,5896,268],{},[270,5898,5899,5905,5911,5917,5923],{},[260,5900,5901,5903],{},[275,5902,277],{},[275,5904,280],{},[260,5906,5907,5909],{},[275,5908,285],{},[275,5910,288],{},[260,5912,5913,5915],{},[275,5914,293],{},[275,5916,296],{},[260,5918,5919,5921],{},[275,5920,301],{},[275,5922,304],{},[260,5924,5925,5927],{},[275,5926,309],{},[275,5928,312],{},[16,5930,315],{},[29,5932,318],{"id":318},[16,5934,321],{},[323,5936,5937,5939,5941,5943,5945],{},[229,5938,327],{},[229,5940,330],{},[229,5942,333],{},[229,5944,336],{},[229,5946,339],{},[16,5948,342],{},[29,5950,345],{"id":345},[16,5952,348],{},[16,5954,351],{},[226,5956,5957,5959,5961,5963],{},[229,5958,356],{},[229,5960,359],{},[229,5962,362],{},[229,5964,365],{},[16,5966,368],{},[29,5968,372],{"id":371},[16,5970,375],{},[80,5972,5974],{"className":5973,"code":380,"language":381,"meta":85},[379],[75,5975,380],{"__ignoreMap":85},[16,5977,386],{},[388,5979,390],{},{"title":85,"searchDepth":124,"depth":124,"links":5981},[5982,5988,5989,5990,5991,5992],{"id":31,"depth":110,"text":31,"children":5983},[5984,5985,5986,5987],{"id":38,"depth":124,"text":39},{"id":45,"depth":124,"text":46},{"id":52,"depth":124,"text":53},{"id":59,"depth":124,"text":60},{"id":69,"depth":110,"text":70},{"id":246,"depth":110,"text":246},{"id":318,"depth":110,"text":318},{"id":345,"depth":110,"text":345},{"id":371,"depth":110,"text":372},{"src":409,"alt":410,"caption":411,"width":412,"height":413},{},{"title":10,"description":404},[421,422,423],1785385297938]