{"componentChunkName":"component---src-templates-blog-template-js","path":"/2026-06-14-weekly-knowledge","result":{"data":{"site":{"siteMetadata":{"title":"KenRp Blog"}},"markdownRemark":{"html":"<p>2026年6月8日から2026年6月14日のワークログを見直し、この事例では共有買い物リストアプリ <code class=\"language-text\">ShareShopi</code> と、その実装リポジトリ <code class=\"language-text\">shared-shopping-app</code> で進めた安定化作業を一本の知見にまとめた。前半はブログ側という Markdown ベースの技術ブログで日次ワークログの形を整え、後半で <code class=\"language-text\">ShareShopi</code> の UI 改修、Supabase Free の停止対策、Cloudflare への段階移行準備までを実装したので、この記事では「アプリをどう安定運用へ寄せたか」に絞って整理する。</p>\n<h2>この記事で解決すること</h2>\n<p>共有アプリでは、初期表示の遅さや外部サービスの疎通確認を UI 実装の中に混ぜると、原因切り分けが難しくなる。この記事では、次の悩みを解く。</p>\n<ul>\n<li>初期表示の安定化と heartbeat 監視を別々の責務として整理したい。</li>\n<li>Supabase の疎通確認をアプリ本体から切り離す判断を見たい。</li>\n<li>Cloudflare Worker へ小さな運用機能を分離する流れを確認したい。</li>\n</ul>\n<h2>実施した改善</h2>\n<p>この事例の変更は大きく3つある。1つ目は、<code class=\"language-text\">ShareShopi</code> の初回表示で起きていたレイアウトジャンプを減らし、最初の買い物リストへ自然につながる待機 UI に置き換えたこと。2つ目は、Supabase Free 環境が停止したまま放置されるのを避けるため、軽量な heartbeat API をアプリ本体に追加したこと。3つ目は、その heartbeat を Cloudflare Workers 側へ分離し、Vercel 本体と独立して週次起動できる構成を用意したことだ。</p>\n<table>\n<thead>\n<tr>\n<th>変更対象</th>\n<th>何を変えたか</th>\n<th>狙い</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>画面表示</td>\n<td>ルートページ、ローダー、リスト詳細のスケルトン、ログアウト遷移を更新</td>\n<td>初回表示の違和感を減らす</td>\n</tr>\n<tr>\n<td>API / バックエンド</td>\n<td><code class=\"language-text\">GET /api/heartbeat</code> と共通 cron 認証を追加</td>\n<td>Supabase の停止検知と起床を安全に行う</td>\n</tr>\n<tr>\n<td>運用 / デプロイ</td>\n<td>heartbeat 専用 Worker、Wrangler 設定、運用ドキュメントを追加</td>\n<td>本体と監視系処理を分離して移行しやすくする</td>\n</tr>\n</tbody>\n</table>\n<h2>確認できた成果</h2>\n<p>一番大きい成果は、利用者が触る UI 改修と、運用者が触る定期起動基盤を同じ週に整理できたことだった。初回表示では完成後レイアウトに近いスケルトンへ寄せたことで、スマートフォンで「別画面に飛んだ」ように見える切り替わりを抑えやすくなった。運用面では、heartbeat を本体 API と Cloudflare Worker の二段構成にしたことで、今は Vercel 中心でも、後から Cloudflare 側へ責務を移しやすい土台ができた。</p>\n<p>また、実装だけでなくテストと手順も同時に揃えた点が重要だった。Playwright では初回描画の回帰観点を足し、Vitest では cron 認証と heartbeat の失敗パスを明示した。リリースそのものはまだ完了していないが、実装、検証、デプロイ準備、運用ドキュメント更新までを分離せずに進めたことで、次の反映作業がだいぶ軽くなった。</p>\n<h2>判断と進め方</h2>\n<p>週の前半は <code class=\"language-text\">worklog-blogger</code> という Codex の作業ログから草稿を作る自作スキルと、出力先のブログ側を使って、毎日の記録を後から再利用しやすい形へ整えていた。これは今回の主題ではないが、そのおかげで「どのファイルをどう変えたか」「何をまだ実行確認できていないか」が日単位で追いやすくなり、週末に <code class=\"language-text\">ShareShopi</code> の変更をまとめる材料になった。</p>\n<p>その上で週後半は、アプリの見た目と運用の両面を同時に進めた。まず、ルート遷移、リスト詳細の読み込み前、ログアウト遷移という3つの不安定ポイントを洗い出し、ローディング中も完成後のレイアウトを想像しやすい UI にそろえた。次に、Supabase を起こすためだけの軽量 heartbeat を Route Handler として追加し、最後にその呼び出し元を Cloudflare Worker に分離して、Vercel の cron 依存を減らす流れで構成を整理した。</p>\n<h2>使った技術</h2>\n<p>今回の中心技術は、<code class=\"language-text\">ShareShopi</code> のフロントエンドに使っている Next.js App Router と React、運用 API の疎通先である Supabase、定期実行の分離先として使う Cloudflare Workers だった。画面側では <code class=\"language-text\">app/page.tsx</code>、<code class=\"language-text\">app/loading.tsx</code>、<code class=\"language-text\">app/layout.tsx</code>、<code class=\"language-text\">components/list-detail-client.tsx</code>、<code class=\"language-text\">components/nav.tsx</code> を使って、初回表示・テーマ適用・遷移中表示の責務を細かく分けている。データ永続化や初期状態の体感改善には <code class=\"language-text\">sessionStorage</code> とローカルストアが使われている。</p>\n<p>バックエンドでは Next.js Route Handler で <code class=\"language-text\">GET /api/heartbeat</code> を追加し、<code class=\"language-text\">lib/heartbeat.ts</code> に Supabase への軽量クエリを閉じ込めた。認証は <code class=\"language-text\">lib/cron-auth.ts</code> へ切り出し、既存のリマインド API と heartbeat API の両方で同じ判定を共有している。さらに定期起動の外側には <code class=\"language-text\">workers/heartbeat.ts</code> と <code class=\"language-text\">wrangler.heartbeat.jsonc</code> を追加し、Cloudflare 側から本体 API を呼ぶだけの薄い Worker として運用責務を分離した。</p>\n<p>検証では UI 回帰に Playwright E2E テスト、API と Worker の境界に Vitest を使っている。ビルド結果や本番リリース完了まではこの事例のログで確認できていないが、少なくとも「UI は E2E」「認証と heartbeat は単体テスト」「運用反映は専用 deploy コマンド」という分担は作れた。</p>\n<h2>構成</h2>\n<p>今回の構成は、利用者向けの画面層、アプリ本体の運用 API 層、外部からの定期起動層の3段に分けると理解しやすい。画面層では、ルートページが直接コンテンツを持つのではなく、最初のリストへ案内する入口として振る舞う。運用 API 層では、<code class=\"language-text\">GET /api/heartbeat</code> が Supabase の生存確認だけを担当し、業務機能とは切り離されている。定期起動層では、Cloudflare Worker が <code class=\"language-text\">APP_ORIGIN</code> と <code class=\"language-text\">CRON_SECRET</code> を使って本体 API を叩く。</p>\n<div class=\"gatsby-highlight\" data-language=\"mermaid\"><pre class=\"language-mermaid\"><code class=\"language-mermaid\">flowchart LR\n  U[&quot;利用者&quot;] --&gt; R[&quot;Next.js ルート /&quot;]\n  R --&gt; L[&quot;リスト詳細 + スケルトン UI&quot;]\n  C[&quot;Cloudflare heartbeat Worker&quot;] --&gt; H[&quot;GET /api/heartbeat&quot;]\n  H --&gt; S[&quot;Supabase&quot;]\n  H --&gt; A[&quot;cron 認証共通化&quot;]</code></pre></div>\n<p>この分け方の利点は、画面体験の改善と、バックエンド運用の改善を別々に進められることだ。UI の変更は App Router とクライアントコンポーネントの責務整理で完結し、heartbeat は Route Handler と Worker の責務整理で完結する。さらにデプロイ面では、Vercel に残す本体と Cloudflare に置く定期起動を別単位で扱えるので、全面移行前でも小さく切り替えられる。</p>\n<h2>採用理由と代替案</h2>\n<p>初回表示の安定化で「完成後レイアウトに近いスケルトン」を選んだ理由は、単に待機中であることを見せるより、次に何が表示されるかを利用者に予告する方が体感品質に効くからだ。特にカルーセル主体の UI は、通常パネルから後で差し替えるとジャンプ感が強く、CSS の調整だけでは吸収しにくい。そこで DOM 構造自体を似せたスケルトンに寄せている。</p>\n<p>heartbeat を API と Worker に分けた理由は、運用処理を本体へ埋め込まないためだ。Supabase Free の停止対策は「重い仕事をする」より「軽く確実に起こす」ことが重要なので、Route Handler 側は最小限の応答に絞り、定期実行の責務は Cloudflare 側へ逃がした。これなら本体 URL が変わっても <code class=\"language-text\">APP_ORIGIN</code> の差し替えで追従しやすく、Vercel と Cloudflare の併用期間にも耐えやすい。</p>\n<p>テスト戦略を分けたのも妥当だった。初回表示の問題は見た目と遷移の組み合わせなので Playwright が向いている。一方で cron 認証や heartbeat の異常系は、UI を経由せず小さな関数単位で確かめた方が速くて壊れにくい。つまり、UI 回帰は E2E、運用 API と Worker は単体テスト、デプロイは専用コマンドと文書化で支える、という分担を選んでいる。</p>\n<h2>検証結果</h2>\n<p>確認した観点は、初期表示改善と heartbeat 監視を同じ UI 実装へ混ぜず、API と Worker に分けて確認した点にある。確認した主な観点は次の通り。</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token function\">npm</span> <span class=\"token builtin class-name\">test</span>\n<span class=\"token function\">npm</span> run build\n<span class=\"token comment\"># Supabase heartbeat API の疎通確認</span>\n<span class=\"token comment\"># Cloudflare Worker 側の heartbeat 分離</span></code></pre></div>\n<p>初期表示、API 疎通、Worker 分離を別々の責務として扱うことで、障害時の切り分け対象を狭めた。</p>\n<h2>導入手順</h2>\n<ol>\n<li>既存の初回表示フローを確認し、ルート遷移、リスト詳細の解決前、ログアウト中の3箇所でどこに体験上のノイズが出るかを分解する。</li>\n<li><code class=\"language-text\">app/layout.tsx</code> にテーマ初期化を寄せ、<code class=\"language-text\">app/page.tsx</code> と <code class=\"language-text\">app/loading.tsx</code> を全画面ローダーへ寄せ、<code class=\"language-text\">components/list-detail-client.tsx</code> に最終レイアウトへ近いスケルトンを追加する。</li>\n<li><code class=\"language-text\">components/nav.tsx</code> でログアウト中オーバーレイを用意し、遷移中に古い一覧が残らないようにする。</li>\n<li>Playwright の <code class=\"language-text\">tests/e2e/guest-list.spec.ts</code> に初回ローダー、固定配置、カルーセル維持の期待値を追加し、UI 変更の回帰観点を先に固定する。</li>\n<li><code class=\"language-text\">app/api/heartbeat/route.ts</code>、<code class=\"language-text\">lib/heartbeat.ts</code>、<code class=\"language-text\">lib/cron-auth.ts</code> を追加または更新し、Supabase への軽量 heartbeat と共通認証を実装する。</li>\n<li>Vitest の <code class=\"language-text\">tests/heartbeat.test.ts</code> と <code class=\"language-text\">tests/heartbeat-worker.test.ts</code> で、成功系だけでなく認証失敗、設定不足、クエリエラーの戻り方まで確認する。</li>\n<li><code class=\"language-text\">workers/heartbeat.ts</code> と <code class=\"language-text\">wrangler.heartbeat.jsonc</code> を追加し、<code class=\"language-text\">package.json</code> に <code class=\"language-text\">cf:heartbeat:dev</code>、<code class=\"language-text\">cf:heartbeat:deploy</code>、<code class=\"language-text\">cf:heartbeat:secret</code> を定義して、デプロイ単位を本体から分離する。</li>\n<li><code class=\"language-text\">README.md</code>、<code class=\"language-text\">OPERATIONS.md</code>、<code class=\"language-text\">CLOUDFLARE_MIGRATION.md</code>、<code class=\"language-text\">ARCHITECTURE.md</code> を更新し、設定値、切り替え順、未完了の運用確認を文章として残す。</li>\n</ol>\n<p>導入順を表にすると次のようになる。</p>\n<table>\n<thead>\n<tr>\n<th>段階</th>\n<th>実装</th>\n<th>検証</th>\n<th>反映</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>UI 安定化</td>\n<td>ルート、ローダー、スケルトン、ログアウト表示を更新</td>\n<td>Playwright で初回描画を確認</td>\n<td>本番反映は未確認</td>\n</tr>\n<tr>\n<td>heartbeat API</td>\n<td>Route Handler と共通認証を追加</td>\n<td>Vitest で認証と異常系を確認</td>\n<td>本番接続は未確認</td>\n</tr>\n<tr>\n<td>Cloudflare 分離</td>\n<td>Worker、Wrangler 設定、deploy コマンドを追加</td>\n<td>Worker 単体テストを追加</td>\n<td>secret 設定と実デプロイは次段階</td>\n</tr>\n</tbody>\n</table>\n<h2>制約と残課題</h2>\n<p>難しかったのは、UI 改修と運用改修で性質の違う問題を同時に扱うことだった。初回表示では、テーマ適用の遅れ、ローカルストア解決前の空白、ログアウト時の残像が別々に起きるので、1つのローダー追加だけでは解決しない。逆に運用面では、heartbeat 自体は単純でも、認証を雑にすると監視用 API が弱点になってしまう。</p>\n<p>もう1つの課題は、移行途中の責務の持ち方だった。Vercel で本体を動かしつつ、Cloudflare に heartbeat だけ先行移行する期間は、どちらが何を担当するかを曖昧にすると事故になる。そこでこの事例では、<code class=\"language-text\">vercel.json</code> から heartbeat cron を外し、Cloudflare 側へ寄せる方向を明文化した。ただし、実デプロイ完了や secret 設定確認まではログ上で追えていないため、最後の運用反映にはまだ確認作業が残っている。</p>\n<h2>次の改善候補</h2>\n<ul>\n<li>iOS Safari 実機で <code class=\"language-text\">100dvh</code> と safe area を含むローダー表示を確認し、モバイル実機での違和感を潰す。</li>\n<li>heartbeat Worker の失敗通知先を決め、週1回起動しただけで終わらず、失敗時に誰かが気づける運用へ進める。</li>\n<li><code class=\"language-text\">APP_ORIGIN</code> と <code class=\"language-text\">CRON_SECRET</code> の設定手順をチェックリスト化し、Cloudflare Dashboard とコード差分のズレを減らす。</li>\n<li>Supabase を起こすだけでなく、将来的には重要テーブルやメール送信まで含めた段階的ヘルスチェックへ広げる。</li>\n<li>OpenNext、つまり Next.js を Cloudflare 上で動かすための構成の Preview 環境で、ログイン、共有、公開リンクの受け入れ試験を行い、全面移行の残課題を減らす。</li>\n</ul>","excerpt":"2026年6月8日から2026年6月14日のワークログを見直し、この事例では共有買い物リストアプリ  と、その実装リポジトリ  で進めた安定化作業を一本の知見にまとめた。前半はブログ側という Markdown ベースの技術ブログで日次ワークログの形を整え、後半で  の UI 改修、Supabase Free…","frontmatter":{"date":"2026年06月14日","path":"/2026-06-14-weekly-knowledge","title":"ShareShopiの初期表示とheartbeatを分離して運用しやすくする","thumbnail":"/kenRp-blog/assets/image-4.jpg","metaDescription":"共有買い物リストアプリ ShareShopi で、初期表示の安定化、Supabase heartbeat API、Cloudflare Worker 分離をどう進めたか整理する。","category":"Knowledge","tags":["nextjs","cloudflare","weekly-knowledge"]}}},"pageContext":{"slug":"/2026-06-14-weekly-knowledge"}}}