[{"data":1,"prerenderedAt":481},["ShallowReactive",2],{"content-/cloudflare-access-private-site":3,"all-pages-for-dir":479,"og-image-/cloudflare-access-private-site":480},{"id":4,"title":5,"body":6,"category":458,"description":459,"extension":460,"meta":461,"navigation":462,"path":463,"project_name":464,"published":465,"publishedAt":466,"seo":467,"stem":468,"tags":469,"todo":476,"updatedAt":477,"__hash__":478},"pages/2026-04/2026-04-08/cloudflare-access-private-site.md","Cloudflare AccessとSSGの相性問題から別プロジェクト分離に至るまで",{"type":7,"value":8,"toc":444},"minimark",[9,13,21,24,29,36,39,41,45,56,59,61,65,75,183,186,188,192,195,201,204,206,210,213,216,229,235,237,241,260,269,276,278,282,288,292,298,326,329,333,344,350,373,375,378,388,390,393,396,399,440],[10,11,5],"h1",{"id":12},"cloudflare-accessとssgの相性問題から別プロジェクト分離に至るまで",[14,15,16,20],"p",{},[17,18,19],"code",{},"/private","以下にCloudflare Accessをかければ済む、と思ってコードを書き始めた。ところがSPA遷移でアクセス制御をすり抜ける挙動を目の当たりにし、ミドルウェアでフルページリロードを挟んで穴を塞ぎ、それでもHTMLソースを開くとコンテンツが丸見えで手が止まった。結局プロジェクトごと分離する判断を下した。ここにその試行錯誤を残す。",[22,23],"hr",{},[25,26,28],"h2",{"id":27},"最初のアプローチ-cloudflare-accessでprivateを保護","最初のアプローチ: Cloudflare Accessで/privateを保護",[14,30,31,32,35],{},"Cloudflare Accessは、特定のパスに対してメール認証やOAuthを要求するゲートを設置できる。",[17,33,34],{},"/private/*","にポリシーを設定して、認証済みユーザーだけがアクセスできるようにした。",[14,37,38],{},"サーバーに直接リクエストが飛ぶ場面では、これで問題なく動く。ところが、SSG + クライアントサイドルーティングの組み合わせで穴が開いた。",[22,40],{},[25,42,44],{"id":43},"spa遷移がcloudflare-accessをすり抜ける","SPA遷移がCloudflare Accessをすり抜ける",[14,46,47,48,51,52,55],{},"Nuxt 3のSSGモードでは、初回アクセス後のページ遷移がクライアントサイドで処理される。つまり、",[17,49,50],{},"/public-page","から",[17,53,54],{},"/private/secret","へのリンクをクリックしたとき、ブラウザはサーバーにリクエストを送らずにJavaScriptでDOM差し替えを行う。",[14,57,58],{},"Cloudflare Accessはエッジ（CDN層）でリクエストを検査する仕組みだから、クライアントサイドのルーティングはそもそも検査対象に入らない。認証画面が一度も割り込まないまま、プライベートコンテンツがそのまま画面に描画された。思わずブラウザのDevToolsを開いて二度確認した。",[22,60],{},[25,62,64],{"id":63},"対策1-ミドルウェアでフルページリロードを強制","対策1: ミドルウェアでフルページリロードを強制",[14,66,67,68,70,71,74],{},"SPA遷移をやめさせれば、すべてのナビゲーションがサーバーへのリクエストになり、Cloudflare Accessの検査を通過する。Nuxtのルートミドルウェアで",[17,69,19],{},"パスへの遷移を検知し、",[17,72,73],{},"window.location.href","によるフルページリロードに差し替えた。",[76,77,82],"pre",{"className":78,"code":79,"language":80,"meta":81,"style":81},"language-typescript shiki shiki-themes vitesse-light vitesse-light","// navigateTo()の代わりにフルリロード\nif (to.path.startsWith('/private')) {\n  window.location.href = to.fullPath\n  return abortNavigation()\n}\n","typescript","",[17,83,84,93,138,165,177],{"__ignoreMap":81},[85,86,89],"span",{"class":87,"line":88},"line",1,[85,90,92],{"class":91},"sxvE3","// navigateTo()の代わりにフルリロード\n",[85,94,96,100,104,108,111,114,116,120,123,127,130,132,135],{"class":87,"line":95},2,[85,97,99],{"class":98},"sHkkW","if",[85,101,103],{"class":102},"shFtX"," (",[85,105,107],{"class":106},"s4oTP","to",[85,109,110],{"class":102},".",[85,112,113],{"class":106},"path",[85,115,110],{"class":102},[85,117,119],{"class":118},"senZ8","startsWith",[85,121,122],{"class":102},"(",[85,124,126],{"class":125},"sMJiu","'",[85,128,19],{"class":129},"sdGka",[85,131,126],{"class":125},[85,133,134],{"class":102},"))",[85,136,137],{"class":102}," {\n",[85,139,141,144,146,149,151,154,157,160,162],{"class":87,"line":140},3,[85,142,143],{"class":106},"  window",[85,145,110],{"class":102},[85,147,148],{"class":106},"location",[85,150,110],{"class":102},[85,152,153],{"class":106},"href",[85,155,156],{"class":102}," =",[85,158,159],{"class":106}," to",[85,161,110],{"class":102},[85,163,164],{"class":106},"fullPath\n",[85,166,168,171,174],{"class":87,"line":167},4,[85,169,170],{"class":98},"  return",[85,172,173],{"class":118}," abortNavigation",[85,175,176],{"class":102},"()\n",[85,178,180],{"class":87,"line":179},5,[85,181,182],{"class":102},"}\n",[14,184,185],{},"これでSPA遷移時にはCloudflare Accessの認証画面が挟まるようになった。一瞬うまくいったように見えた。",[22,187],{},[25,189,191],{"id":190},"対策1の限界-プリレンダリングされたhtmlにコンテンツが入っている","対策1の限界: プリレンダリングされたHTMLにコンテンツが入っている",[14,193,194],{},"しかし、ブラウザのアドレスバーに直接URLを打ち込んでアクセスしてみると、Cloudflare Accessの認証画面は出るものの、SSGでプリレンダリングされた静的HTMLファイル自体にコンテンツが含まれていることに気づいた。",[14,196,197,198,200],{},"SSGビルド時に",[17,199,54],{},"のHTMLが生成され、そのHTMLの中にはページの全テキストが埋め込まれている。Cloudflare Accessは認証を挟むが、HTMLファイル自体はCDNに配置されている。認証をバイパスする手段が見つかれば（あるいはキャッシュの挙動次第で）、コンテンツが露出するリスクが残る。",[14,202,203],{},"ミドルウェアの対策はクライアントサイドルーティングの穴は塞いだが、根本的な問題---プリレンダリングされたHTMLにコンテンツが平文で含まれること---には手が届かなかった。",[22,205],{},[25,207,209],{"id":208},"最終判断-別プロジェクトに分離する","最終判断: 別プロジェクトに分離する",[14,211,212],{},"ここで方針を切り替えた。同じプロジェクト内でパスベースの保護を追い求めるより、プロジェクトごと分離して別ドメインにCloudflare Accessをかける方が確実だと判断した。",[14,214,215],{},"理由はシンプルで、別プロジェクトなら:",[217,218,219,223,226],"ul",{},[220,221,222],"li",{},"SSGビルドの成果物が完全に別のCloudflare Pagesプロジェクトに配置される",[220,224,225],{},"ドメイン全体にCloudflare Accessをかけられる（パスベースの穴がない）",[220,227,228],{},"コンテンツの漏洩リスクがゼロになる",[14,230,231,234],{},[17,232,233],{},"apps/private","ディレクトリを作成し、Nuxt 3のミニマルアプリを構築した。",[22,236],{},[25,238,240],{"id":239},"appsprivate-の構成","apps/private の構成",[14,242,243,244,247,248,251,252,255,256,259],{},"メインアプリ（",[17,245,246],{},"apps/web","）のコンポーネントを再利用するため、",[17,249,250],{},"nuxt.config.ts","の",[17,253,254],{},"components","ディレクトリ設定で",[17,257,258],{},"apps/web/app/components","を参照先に追加した。DocPageラッパーもそのまま共有できた。",[14,261,262,265,266,268],{},[17,263,264],{},"content.config.ts","は",[17,267,233],{},"用に別途定義し、プライベートコンテンツ専用のスキーマを設けた。",[14,270,271,272,275],{},"ライフプラン関連の記事を",[17,273,274],{},"life-plan","ディレクトリに配置し、プライベートサイトの最初のコンテンツとした。",[22,277],{},[25,279,281],{"id":280},"privateeurekapucom-へのデプロイ","private.eurekapu.com へのデプロイ",[14,283,284,287],{},[17,285,286],{},"private.eurekapu.com","としてCloudflare Pagesにデプロイする過程で、2つのトラブルに遭遇した。",[289,290,291],"h3",{"id":291},"wranglerがプロジェクト未作成でエラー",[14,293,294,297],{},[17,295,296],{},"wrangler pages deploy","を実行したところ、Cloudflare Pages上にプロジェクトが存在しないというエラーが返ってきた。初回デプロイ時は事前にプロジェクトを作成する必要がある。",[76,299,303],{"className":300,"code":301,"language":302,"meta":81,"style":81},"language-bash shiki shiki-themes vitesse-light vitesse-light","wrangler pages project create private-eurekapu --production-branch=main\n","bash",[17,304,305],{"__ignoreMap":81},[85,306,307,310,313,316,319,322],{"class":87,"line":88},[85,308,309],{"class":118},"wrangler",[85,311,312],{"class":129}," pages",[85,314,315],{"class":129}," project",[85,317,318],{"class":129}," create",[85,320,321],{"class":129}," private-eurekapu",[85,323,325],{"class":324},"snbK4"," --production-branch=main\n",[14,327,328],{},"これで解決。",[289,330,332],{"id":331},"production-branchの不一致main-vs-master","production branchの不一致（main vs master）",[14,334,335,336,339,340,343],{},"ローカルのブランチが",[17,337,338],{},"master","なのに、Cloudflare Pagesのproduction branchが",[17,341,342],{},"main","に設定されていた。デプロイしてもproductionとして認識されず、preview扱いになってしまった。",[14,345,346,349],{},[17,347,348],{},"--branch=main","フラグを指定してデプロイすることで、production deploymentとして正しく反映された。",[76,351,353],{"className":300,"code":352,"language":302,"meta":81,"style":81},"wrangler pages deploy dist/ --project-name=private-eurekapu --branch=main\n",[17,354,355],{"__ignoreMap":81},[85,356,357,359,361,364,367,370],{"class":87,"line":88},[85,358,309],{"class":118},[85,360,312],{"class":129},[85,362,363],{"class":129}," deploy",[85,365,366],{"class":129}," dist/",[85,368,369],{"class":324}," --project-name=private-eurekapu",[85,371,372],{"class":324}," --branch=main\n",[22,374],{},[25,376,377],{"id":377},"トップページへの導線追加",[14,379,380,381,384,385,387],{},"メインサイトのトップページに、プライベートコンテンツへの導線を追加した。鍵アイコンを",[17,382,383],{},"opacity: 0.4","で控えめに表示し、知っている人だけが気づくような配置にした。クリックすると",[17,386,286],{},"に遷移する。",[22,389],{},[25,391,392],{"id":392},"振り返り",[14,394,395],{},"パスベースの保護で済むと思って手を動かし始めたが、SPA遷移のバイパスを見つけ、ミドルウェアで塞ぎ、さらにプリレンダリングの問題に突き当たって、結局プロジェクト分離に行き着いた。最初から分離していれば30分で終わった作業かもしれない。ただ、「SSGビルドの出力HTMLをブラウザのソース表示で開いて、コンテンツが平文で並んでいるのを目で確認した」あの瞬間がなければ、エッジ認証の守備範囲を正確に把握できていなかった。回り道した分だけ、判断の根拠が手元に残った。",[14,397,398],{},"学んだこと:",[217,400,401,408,414,420,430],{},[220,402,403,407],{},[404,405,406],"strong",{},"SSG + クライアントサイドルーティング + エッジ認証は穴ができやすい","。SPA遷移がエッジを経由しないため、パスベースのアクセス制御が効かない場面がある",[220,409,410,413],{},[404,411,412],{},"プリレンダリングされたHTMLは平文","。SSGのビルド成果物にはコンテンツがそのまま含まれるため、CDN上のHTMLファイルにアクセスできれば中身が見える",[220,415,416,419],{},[404,417,418],{},"ドメイン分離が最もシンプルな解","。パスベースで頑張るより、プロジェクトごと分けてドメイン全体を保護する方が確実で、設定もシンプルになる",[220,421,422,429],{},[404,423,424,425,428],{},"wrangler pages deployの初回は",[17,426,427],{},"project create","が必要","。自動作成はされない",[220,431,432,435,436,439],{},[404,433,434],{},"production branchの名前はローカルとCloudflare側で揃える","か、デプロイ時に",[17,437,438],{},"--branch","で明示する",[441,442,443],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}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 .snbK4, html code.shiki .snbK4{--shiki-default:#A65E2B;--shiki-dark:#A65E2B}",{"title":81,"searchDepth":95,"depth":95,"links":445},[446,447,448,449,450,451,452,456,457],{"id":27,"depth":95,"text":28},{"id":43,"depth":95,"text":44},{"id":63,"depth":95,"text":64},{"id":190,"depth":95,"text":191},{"id":208,"depth":95,"text":209},{"id":239,"depth":95,"text":240},{"id":280,"depth":95,"text":281,"children":453},[454,455],{"id":291,"depth":140,"text":291},{"id":331,"depth":140,"text":332},{"id":377,"depth":95,"text":377},{"id":392,"depth":95,"text":392},"dev","Cloudflare Accessで/private以下を保護しようとしたが、SSGのプリレンダリングでバイパスされる問題が発覚。ミドルウェア対策を経て、最終的にprivate.eurekapu.comとして別プロジェクトに分離した判断過程の記録","md",{},true,"/cloudflare-access-private-site","mdx-playground",false,"2026-04-08T00:00:00.000Z",{"title":5,"description":459},"2026-04/2026-04-08/cloudflare-access-private-site",[470,471,472,473,474,475],"Cloudflare Access","SSG","Nuxt3","プライベートサイト","Cloudflare Pages","セキュリティ","memo",null,"j-WvaYujNlWDrHDwM53OPRvG4s1ibCWSp3GnCDK6aSs",[],"https://log.eurekapu.com/favicon.svg",1775680421363]