{"componentChunkName":"component---src-templates-blog-template-js","path":"/2026-07-26-weekly-knowledge","result":{"data":{"site":{"siteMetadata":{"title":"KenRp Blog"}},"markdownRemark":{"html":"<p><code class=\"language-text\">shared-shopping-app</code> は、共有買い物リストを家族や同居人どうしで管理するためのアプリである。今回の週次テーマでは、このアプリを「機能が動くだけの状態」から、「検索流入を受け止め、広告設定を安全に配信し、公開前チェックを継続できる状態」へ寄せた実装を扱う。</p>\n<p>単に公開ページを増やすだけでは、運用は安定しない。検索流入向けページ、ログイン後の操作画面、広告配信設定、Cloudflare 上でのビルドとデプロイ前監査、PC とモバイルで異なる UI 要件を、それぞれ混ぜずに分ける必要がある。この記事では、<code class=\"language-text\">shared-shopping-app</code> リポジトリで行った変更をもとに、公開運用に耐える構成へどう整理したかをまとめる。</p>\n<h2>この記事で解決すること</h2>\n<ul>\n<li>Next.js アプリで、公開ページとログイン後画面を分離しながら集客導線を作りたい。</li>\n<li>Cloudflare 前提のアプリに広告設定や <code class=\"language-text\">ads.txt</code> を足すとき、設定漏れや誤配信を減らしたい。</li>\n<li>UI 改修、テスト、ビルド、公開前監査まで含めて、変更を運用しやすい単位で整理したい。</li>\n</ul>\n<h2>実装の要点</h2>\n<p>この週に進めた変更は、大きく 4 つに分かれる。1 つ目は、公開ページとアプリ本体の責務分離である。<code class=\"language-text\">app/page.tsx</code>、<code class=\"language-text\">app/about/page.tsx</code>、<code class=\"language-text\">app/contact/page.tsx</code>、<code class=\"language-text\">app/privacy/page.tsx</code>、<code class=\"language-text\">app/terms/page.tsx</code> を公開向けの導線として整理し、<code class=\"language-text\">app/layout.tsx</code> に metadata と広告初期化を集約した。加えて <code class=\"language-text\">app/robots.ts</code> と <code class=\"language-text\">app/sitemap.ts</code> を用意し、検索対象を公開ページに限定した。</p>\n<p>2 つ目は、広告収益化の契約層を追加したことだ。<code class=\"language-text\">lib/monetization.ts</code> と <code class=\"language-text\">lib/monetization-runtime.ts</code> に、AdSense の publisher / client / slot 設定の検証を集め、<code class=\"language-text\">app/api/ads/config/route.ts</code> から placement ごとの広告設定だけを返す構成へ変えた。<code class=\"language-text\">app/ads.txt/route.ts</code> を追加して、<code class=\"language-text\">ads.txt</code> もアプリの配信経路に載せている。</p>\n<p>3 つ目は、公開前監査と Cloudflare 運用の自動化である。<code class=\"language-text\">scripts/release-audit.mjs</code>、<code class=\"language-text\">tests/release-audit.test.ts</code>、<code class=\"language-text\">.github/workflows/ci.yml</code>、<code class=\"language-text\">.github/workflows/deploy.yml</code> を更新し、必須ファイル、<code class=\"language-text\">wrangler</code> 設定、環境変数、型生成の前提が崩れていないかを build 前に確認できるようにした。さらに <code class=\"language-text\">lib/cron-worker.ts</code> と関連テストで、Cron 実行結果を構造化ログとして残すようにした。</p>\n<p>4 つ目は、PC 版買い物リスト画面の 3 カラム化である。<code class=\"language-text\">components/list-detail-client.tsx</code> と <code class=\"language-text\">app/globals.css</code> を中心に、左サイドバー、中央リスト、右サマリーのレイアウトへ変更し、モバイルでは既存のカルーセル UI を維持した。見た目の話に見えるが、実際には PC とモバイルで責務を分けてテストするための構造変更でもある。</p>\n<h2>得られた知見</h2>\n<p>成果は、公開向けの導線、広告配信、運用監査、利用画面の改修が別々の実装ではなく、同じアプリの公開運用を支える構成として接続されたことにある。検索流入は公開ページで受け、ログイン後画面や共有 URL はインデックス対象から外し、広告設定は契約層と API 層で検証してから UI に渡す。これにより、集客や収益化の追加がアプリ本体の責務を汚しにくくなった。</p>\n<p>検証の面でも前進がある。ワークログでは <code class=\"language-text\">npm test</code> と <code class=\"language-text\">npm run build</code> の成功、PC 版 E2E とモバイル E2E の成功、広告設定や監査スクリプトのテスト追加が確認できる。つまり、UI、設定、ビルド、運用前提のどこが壊れたかを、以前より狭い単位で検出しやすくなった。</p>\n<p>一方で、本番デプロイ完了や広告審査結果、実流入の改善効果までがこの週の一次証跡で揃っているわけではない。そのため、ここでの成果は「Cloudflare 上で公開運用しやすい構成へ寄せたこと」であり、「本番成果まで実証済み」とは切り分けて読む必要がある。</p>\n<h2>実装プロセス</h2>\n<p>最初に進めたのは、公開ページの整理だった。公開トップや法務ページに metadata、JSON-LD、広告導線を持たせる一方、<code class=\"language-text\">/app</code> や共有リンクのような利用者向け画面は検索対象から外す必要があった。そこで App Router のルート単位で public / private の境界を引き、<code class=\"language-text\">robots.ts</code> と <code class=\"language-text\">sitemap.ts</code> でインデックス対象を制御した。</p>\n<p>次に、広告設定を静的ファイルやコンポーネント直書きから切り離した。広告は外部スクリプト依存が強く、環境変数の typo もそのまま配信ミスにつながる。そこで ID 検証、placement 解決、<code class=\"language-text\">ads.txt</code> 生成を <code class=\"language-text\">lib/monetization.ts</code> に集約し、UI は API から返る設定を表示するだけに寄せた。失敗時は広告だけが無効になり、買い物リスト本体は壊さない構成である。</p>\n<p>そのうえで、公開前監査を CI と deploy workflow に組み込んだ。Cloudflare Workers / OpenNext 構成では、アプリコードの unit test が通っても、<code class=\"language-text\">wrangler</code> 設定や型生成、必須 env がずれていると本番で詰まりやすい。そこで <code class=\"language-text\">release-audit</code> と <code class=\"language-text\">cf:typegen</code> を build 前に挟み、アプリのテストとは別に「公開運用契約」を検査する層を作った。</p>\n<p>最後に、PC 版 UI を 3 カラムへ改修した。これは見栄えの改善だけではなく、同じデータ操作を PC とモバイルでどう見せ分けるかの設計判断だった。モバイル導線を壊さないため、状態管理は維持しつつ表示レイヤーだけを分岐させ、E2E も PC 用とモバイル用を分けて確認する流れになった。</p>\n<h2>利用した技術</h2>\n<ul>\n<li>対象アプリは共有買い物リストアプリ <code class=\"language-text\">shared-shopping-app</code>、対象リポジトリも同名の <code class=\"language-text\">shared-shopping-app</code> である。基盤は Next.js App Router、React、TypeScript、Cloudflare Workers / OpenNext for Cloudflare 構成だった。</li>\n<li>アプリ変更では <code class=\"language-text\">app/layout.tsx</code>、<code class=\"language-text\">app/page.tsx</code>、<code class=\"language-text\">app/about/page.tsx</code>、<code class=\"language-text\">app/contact/page.tsx</code>、<code class=\"language-text\">app/privacy/page.tsx</code>、<code class=\"language-text\">app/terms/page.tsx</code>、<code class=\"language-text\">app/use-cases/page.tsx</code>、<code class=\"language-text\">app/use-cases/[slug]/page.tsx</code> を更新し、公開ページと集客導線を整理した。</li>\n<li>実装では <code class=\"language-text\">lib/monetization.ts</code>、<code class=\"language-text\">lib/monetization-runtime.ts</code>、<code class=\"language-text\">app/api/ads/config/route.ts</code>、<code class=\"language-text\">app/ads.txt/route.ts</code>、<code class=\"language-text\">components/ad-unit.tsx</code>、<code class=\"language-text\">components/adsense-script.tsx</code> を追加・更新し、広告設定の検証、配信、表示を分離した。</li>\n<li>PC 版画面の改修では <code class=\"language-text\">components/list-detail-client.tsx</code> と <code class=\"language-text\">app/globals.css</code> を変更し、デスクトップ向け 3 カラム UI とモバイル向け既存 UI の共存を実装した。</li>\n<li>テストでは Vitest の <code class=\"language-text\">tests/cloudflare-config.test.ts</code>、<code class=\"language-text\">tests/monetization.test.ts</code>、<code class=\"language-text\">tests/release-audit.test.ts</code>、<code class=\"language-text\">tests/cron-worker.test.ts</code>、<code class=\"language-text\">tests/privacy.test.ts</code>、<code class=\"language-text\">tests/local-backup.test.ts</code> を追加・更新し、Playwright の <code class=\"language-text\">tests/e2e/guest-list.spec.ts</code>、<code class=\"language-text\">tests/e2e/public-pages.spec.ts</code> で PC / モバイル UI と公開ページ導線を確認した。</li>\n<li>ビルドでは <code class=\"language-text\">npm run build</code> と Cloudflare 向けの <code class=\"language-text\">cf:typegen</code>、<code class=\"language-text\">opennextjs-cloudflare build</code> に接続する前提を整え、デプロイとリリース前確認では <code class=\"language-text\">scripts/release-audit.mjs</code> と GitHub Actions の workflow 更新で、設定不整合を先に落とす運用へ寄せた。</li>\n</ul>\n<h2>アーキテクチャ</h2>\n<p>この変更群は、公開層、体験層、収益化層、運用層の 4 段で整理すると分かりやすい。公開層は検索流入を受けるトップページやユースケースページで、metadata、JSON-LD、robots、sitemap を持つ。体験層はログイン後の買い物リスト画面で、PC は 3 カラム、モバイルはカルーセル UI として同じ操作ロジックを見せ分ける。収益化層は広告設定 API と <code class=\"language-text\">ads.txt</code> 配信、広告表示コンポーネントからなり、設定の契約と表示責務を分離する。運用層は release audit、<code class=\"language-text\">cf:typegen</code>、Cron ログ、CI / deploy workflow で、公開前に構成の破綻を検知する。</p>\n<div class=\"gatsby-highlight\" data-language=\"mermaid\"><pre class=\"language-mermaid\"><code class=\"language-mermaid\">flowchart LR\n  A[&quot;公開ページ&lt;br/&gt;page.tsx / use-cases&quot;] --&gt; B[&quot;アプリ本体&lt;br/&gt;ListDetailClient&quot;]\n  C[&quot;広告設定契約&lt;br/&gt;lib/monetization.ts&quot;] --&gt; D[&quot;広告設定 API&lt;br/&gt;/api/ads/config&quot;]\n  D --&gt; A\n  C --&gt; E[&quot;ads.txt route&quot;]\n  F[&quot;release-audit / cf:typegen&quot;] --&gt; G[&quot;CI / Deploy Workflow&quot;]\n  G --&gt; A\n  G --&gt; B</code></pre></div>\n<p>重要なのは、公開ページ、広告設定、買い物リスト UI、運用監査を 1 つの巨大な層にしていないことだ。広告は UI に直接 env を持ち込まず、公開前監査はアプリ本体のコンポーネントから独立させている。これにより、変更箇所が増えても「どの責務で壊れたか」を追いやすい。</p>\n<h2>選定理由</h2>\n<p>Cloudflare 前提の既存構成を大きく崩さなかった理由は、公開運用の複雑さを増やしすぎないためである。別サービスで広告設定や監査を持つより、Next.js アプリと Cloudflare の配信経路の中で閉じたほうが、ビルド、デプロイ、運用手順を一本化しやすい。</p>\n<p>公開ページとログイン後画面を App Router のルート単位で分けたのは、SEO 強化と利用中画面の保護を同時に満たすためである。検索流入向けページには説明と広告を持たせたいが、共有リンクや個人操作画面まで検索対象にすると責務が崩れる。public / private をルーティングで切る判断は合理的だった。</p>\n<p>広告設定を <code class=\"language-text\">lib/monetization.ts</code> の契約層に集約したのは、AdSense ID や slot の形式ミスを UI で発見するのが遅すぎるからである。無効値を API 返却前に落とし、UI は「有効設定が来たら表示、無ければ表示しない」だけにした方が安全で保守しやすい。</p>\n<p><code class=\"language-text\">release-audit</code> を build 前に入れたのは、Cloudflare 系アプリではコードの unit test だけで公開品質を担保しきれないからである。必須ファイル、<code class=\"language-text\">wrangler</code> 設定、型生成、env の契約を先に検査すれば、デプロイ段階で初めて気づく種類のミスを減らせる。</p>\n<p>PC 版 UI を完全別画面へ分離せず、表示層だけを分岐させたのは、モバイルの既存操作を残したかったためである。同じロジックに対して PC とモバイルの見せ方を変える設計なら、データ操作とテストの共通部分を維持しやすい。</p>\n<h2>導入手順</h2>\n<ol>\n<li>まず App Router 上で、公開ページとログイン後画面の境界を決める。トップ、法務、ユースケースページのように検索流入を受けるルートと、利用者専用のルートを分ける。</li>\n<li><code class=\"language-text\">app/layout.tsx</code> に metadata と広告初期化の共通設定を集め、<code class=\"language-text\">app/robots.ts</code> と <code class=\"language-text\">app/sitemap.ts</code> で公開ページだけを検索対象にする。</li>\n<li>広告設定はコンポーネントへ直書きせず、<code class=\"language-text\">lib/monetization.ts</code> のような契約層へ切り出す。publisher / client / slot の形式をここで検証し、無効値は返さない。</li>\n<li><code class=\"language-text\">app/api/ads/config/route.ts</code> のような route handler から placement ごとの最小設定だけを返し、<code class=\"language-text\">app/ads.txt/route.ts</code> で <code class=\"language-text\">ads.txt</code> も同じ配信経路に載せる。</li>\n<li>UI 側は <code class=\"language-text\">components/ad-unit.tsx</code> のような部品で広告初期化失敗を隔離し、広告ブロッカーや preview 環境でも本体利用を壊さないようにする。</li>\n<li>買い物リストのような利用画面は、状態管理を共通にしたまま、PC とモバイルで表示レイヤーを分ける。今回の構成では <code class=\"language-text\">ListDetailClient</code> と CSS のメディアクエリで 3 カラム化した。</li>\n<li>テストは設定系と画面系を分ける。Vitest で広告設定、Cloudflare 設定、監査スクリプト、バックアップ、privacy ルールを確認し、Playwright で PC / モバイル UI と公開ページ導線を確認する。</li>\n<li>build と release 前には <code class=\"language-text\">npm run build</code>、<code class=\"language-text\">cf:typegen</code>、<code class=\"language-text\">release-audit</code> を通し、必要に応じて Cloudflare 向け build / deploy workflow へ接続する。本番 deploy や広告審査の成否は、別途運用ログで確認する。</li>\n</ol>\n<h2>導入時の課題</h2>\n<p>最初の課題は、集客導線を増やすほど public / private の境界が曖昧になりやすいことだった。公開トップやユースケースページに広告や metadata を足したい一方で、ログイン後画面や共有リンクまで同じ責務で扱うと、検索対象の制御が崩れやすい。</p>\n<p>次の課題は、広告設定の失敗が UI 側で発覚しやすいことだった。AdSense の ID は形式が厳密で、環境変数 typo や preview 環境の違いがそのまま無効表示につながる。設定検証を API より前に置かないと、表示部品に責務が寄りすぎる。</p>\n<p>また、Cloudflare 構成では、アプリ本体のテストが通っても <code class=\"language-text\">wrangler</code> 設定、型生成、必須ファイル不足で deploy が止まることがある。これは unit test だけでは拾いづらく、公開前監査を別レイヤーで持つ必要があった。</p>\n<p>UI 改修でも課題があった。PC 版とモバイル版の DOM が同居する構成では、E2E のセレクタが曖昧だと非表示要素を誤操作しやすい。そこで PC とモバイルのテストケース自体を分け、可視要素ベースで確認する必要があった。</p>\n<p>最後に、ワークログから確認できるのは build 成功や E2E 成功、コミット、監査スクリプト追加までであり、本番 deploy 完了や広告配信実績は別証跡が必要だった。実装済みと運用確認済みを混同しない整理も重要である。</p>\n<h2>検証結果</h2>\n<p>ワークログから確認できる範囲の検証結果は次のとおりである。ここでは「実装したはず」ではなく、記録上確認できた項目だけを列挙する。</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">2026-07-20:\n- npm test: 成功\n- npm run build: 成功\n\n2026-07-24:\n- PC向け E2E: 成功\n- モバイル向け E2E: 成功\n\n2026-07-25:\n- tests/monetization.test.ts を追加\n- tests/release-audit.test.ts を追加\n- tests/cron-worker.test.ts を追加</code></pre></div>\n<p>確認できた事実から言えるのは、公開ページ変更、広告設定変更、PC UI 改修、公開前監査の追加が、それぞれテストや build に接続されていることまでである。逆に、Cloudflare への本番 deploy 成功、AdSense 審査通過、実際の広告表示率まではこの週の source material だけでは断定できない。</p>\n<h2>採用理由と代替案</h2>\n<p>今回の判断で比較対象になったのは、公開運用の責務をどこで分けるかだった。たとえば広告設定は、コンポーネントへ env を直接渡す実装も可能だったが、不採用にした。設定ミスの検出が遅れ、preview と production の差異も UI に持ち込みやすいからである。</p>\n<p><code class=\"language-text\">ads.txt</code> についても、静的ファイルを都度更新する運用は代替案としてあり得た。しかし、Cloudflare へ配備するアプリ本体と別経路で管理すると、ビルド済みコードと公開設定のズレが発生しやすい。そこで route handler で配信し、同じ deploy 単位に含める構成を採用した。</p>\n<p>PC 版 UI も完全な別画面化は代替案としてあり得たが、不採用にした。PC とモバイルでロジックまで分離すると、アイテム編集やリスト作成の修正が二重管理になりやすい。今回は状態管理を共通に保ち、表示だけを分岐させるトレードオフを取っている。</p>\n<h2>次に検討した方が良いこと</h2>\n<ul>\n<li><code class=\"language-text\">release-audit</code> に公開ページの構造化データや <code class=\"language-text\">ads.txt</code> 応答確認を追加し、公開前チェックをもう一段自動化する。</li>\n<li><code class=\"language-text\">utm_campaign</code> や公開ページ別 CTA の attribution を、アプリ内行動と結びつけて見られるようにする。</li>\n<li>PC 版 3 カラム UI の部品をサイドバー、メイン、サマリーへ分割し、<code class=\"language-text\">ListDetailClient</code> の責務をさらに整理する。</li>\n<li><code class=\"language-text\">cf:typegen</code> と Cloudflare 向け build をローカルでも回しやすくし、deploy 前に設定差分を検出しやすくする。</li>\n<li>広告設定と公開ページ改善が実際の利用率や共有継続率にどう効いたかを測るため、運用指標を先に定義する。</li>\n</ul>","excerpt":"…","frontmatter":{"date":"2026年07月26日","path":"/2026-07-26-weekly-knowledge","title":"shared-shopping-app を Cloudflare 前提で公開運用しやすい構成へ寄せた実装","thumbnail":"/kenRp-blog/assets/kenrp-blog-replace-wireframe.jpg","metaDescription":"shared-shopping-app で、公開ページ、広告設定、公開前監査、PC UI 改修を分離しながら、Cloudflare 前提で運用しやすい構成へ寄せた実装と判断を整理する。","category":"Knowledge","tags":["cloudflare","nextjs","adsense","testing","weekly-knowledge"]}}},"pageContext":{"slug":"/2026-07-26-weekly-knowledge"}}}