アーキテクチャArchitecture

Web 版は Astro 5 の Hybrid SSG + SSR(output: 'server' + ページ別 export const prerender = true 構成です(ADR-0009)。 トップ・LP・/search/scene/*/media/ 等の安定ページはビルド時に静的 HTML として出し、 駐車場系(/p/*, /spot/*, /media/[...slug], /media/category/*, /media/story/[...slug])は Cloudflare Pages Functions 上で 毎リクエスト SSR し、駐車場データの鮮度を edge cache に乗せます。 配信は Cloudflare Pages(dev: parky-home-dev / dev.parky.co.jp、prod: parky-home-prod / parky.co.jp)。 GitHub Actions が wrangler pages deploy で送り込みます。

The web app uses Astro 5 Hybrid SSG + SSR (output: 'server' with per-page export const prerender = true) — see ADR-0009. Stable pages (top / LP / /search / /scene/* / /media/) are pre-rendered; parking-heavy routes (/p/*, /spot/*, /media/[...slug], /media/category/*, /media/story/[...slug]) run as per-request SSR on Cloudflare Pages Functions so live parking data hits the edge cache instead of being baked at build. Hosting is on Cloudflare Pages (dev: parky-home-dev / dev.parky.co.jp, prod: parky-home-prod / parky.co.jp). GitHub Actions runs wrangler pages deploy.

検索は Pagefind で静的化(SSG ページのみ)Search via Pagefind (SSG pages only)

/searchPagefindastro-pagefind)でビルド後の dist/ をスキャンして生成される静的インデックスを、クライアントから fetch して全文検索します。ただし Pagefind は prerender=true な SSG ページしか拾えないため、 SSR 配信される駐車場系ページ(/p/* / /spot/*)は索引対象外です。 SSR ページの導線は sitemap (/sitemap-pages.xml) と内部リンクで担保します。

/search is powered by Pagefind (astro-pagefind): the plugin scans the built dist/ after astro build and emits a static index under dist/pagefind/ that the client fetches at runtime. Pagefind only sees pages with prerender=true — SSR-served parking routes (/p/*, /spot/*) are excluded from this index and rely on sitemap (/sitemap-pages.xml) + internal linking for discovery.

データアクセスは 100% BFF 経由: Data access is 100% via the BFF: Web 版は supabase.from() を直接叩かない。ビルド時の SSG データ取得もランタイム Island の API もすべて Workers BFF (/v1/*) を通す。service_role キーは UI バンドルにも GitHub Actions ワークフローにも含めず、BFF Worker の Secret としてのみ保管する。 The web app never calls supabase.from() directly. Build-time SSG fetches and runtime island APIs all go through the Workers BFF (/v1/*). The service_role key is never embedded in the UI bundle or in GitHub Actions — it lives only as a Worker secret.

Web版のチャネル接続Web app channel view

Web 版だけに関係する接続を抽出した図です。 全体像は 全体アーキテクチャ を参照してください。

The slice of the system the web app touches. For the full picture see the overall architecture.

シェイプをクリックして関連を強調 Click any shape to focus its connections
flowchart LR
  WA["Web App
Astro 5 (Hybrid SSG + SSR)"] subgraph Build["ビルド時 (GitHub Actions)"] GHA["GitHub Actions
ランナー"] BUILD["Astro build
+ astro-pagefind"] end subgraph BFF["BFF (Cloudflare Workers)"] API["/v1/* endpoints
(@parky/bff-client)"] end subgraph Pages["Cloudflare Pages (parky-home-*)"] HTML["静的HTML
(prerender=true)"] FN["_worker.js
(SSR / Pages Functions)"] end subgraph Runtime["ランタイム (ブラウザ)"] ISLAND["React Islands
(Search / Map)"] end subgraph Supabase["Supabase (BFF / SSR からのみ参照)"] DB[("PostgreSQL
+ PostGIS")] end subgraph Storage["Object storage (Cloudflare R2)"] WSB["R2 bucket
parky"] CDN["cdn.parky.co.jp
(public GET)"] end subgraph External["外部"] MB["Mapbox GL JS"] SENTRY["Sentry
(@sentry/astro)"] end %% ビルド: 静的ページのみ事前生成。SSR ページはビルド時 API を叩かない GHA --> BUILD BUILD --> HTML GHA --> Pages %% ランタイム: SSR ページは毎リクエスト BFF 経由で fetch WA --> HTML WA --> FN FN --> API API --> DB %% Island はブラウザから直接 BFF WA --> ISLAND ISLAND --> API ISLAND --> MB %% メディア / 共通 HTML --> CDN FN --> CDN CDN --> WSB ISLAND --> SENTRY

ビルドとデプロイのおおまかな流れBuild & deploy flow (high-level)

sequenceDiagram
  participant GHA as GitHub Actions
  participant Astro as Astro build (@parky/home)
  participant PF as Pagefind (postBuild)
  participant Pages as Cloudflare Pages
parky-home-{dev,prod} participant FN as Pages Functions (SSR) participant API as Workers BFF
{dev-,}api.parky.co.jp participant Supa as Supabase (PostgreSQL) Note over GHA,Astro: 2026-04-19 SSR 化以降、ビルド時の API health check は廃止。
ビルドは API を叩かず純粋にコード→静的アセット変換のみ。 GHA->>Astro: npm run build:home Astro-->>GHA: dist/ (prerender=true の静的HTML + _worker.js + Islands JS) GHA->>PF: astro-pagefind が dist/ を走査 PF-->>GHA: dist/pagefind/ (SSG ページのみの全文検索インデックス) GHA->>Pages: wrangler pages deploy web/home/dist Note over Pages,FN: 配信時:
静的HTML は CDN edge で即返却
SSR ルートは _worker.js が起動 Note over FN,Supa: ランタイム (毎リクエスト) FN->>API: /v1/* (parking-lots / hubs / sponsors / articles 等) API->>Supa: SELECT (Hyperdrive 経由) Supa-->>API: rows API-->>FN: JSON (Layer-First view envelope) FN-->>Pages: SSR HTML (edge cache 対象)
注: Note: 上の図は流れの概要です。どのページがどのテーブルから getStaticPaths を取るかは個別の Astro ページ (web/home/src/pages/**/*.astro) を直接参照してください。 The sequence above is a high-level summary. Which pages pull from which tables in getStaticPaths is best read directly from each page under web/home/src/pages/**/*.astro.

レンダリング戦略(実装ベース)Rendering strategy (as implemented)

振り分けは各ページ先頭の export const prerender = true | false が SSoT。 true はビルド時に静的化、false は Pages Functions の _worker.js 上で毎リクエスト SSR。

The dividing line is export const prerender = true | false at the top of each page (the SSoT). true = baked at build, false = per-request SSR via Pages Functions (_worker.js).

flowchart TB
  start(["Astro page"]) --> q{"export const
prerender = ?"} q -- "true" --> ssg["SSG
ビルド時 dist/ に出力
Pagefind 索引対象"] q -- "false" --> ssr["SSR
_worker.js 上で実行
edge cache + BFF fetch"] ssg --> ex_ssg["例: /, /search, /scene/*, /media/, /about/, /for-owners/, /spot/[id]/ 以外の静的 LP"] ssr --> ex_ssr["例: /spot/[id]/, /p/[pref]/, /p/[pref]/[city]/, /p/[pref]/[city]/[spot]/[filter]?/, /media/[...slug]/, /media/category/[slug]/, /media/story/[...slug]/, /sitemap-pages.xml"]
種別TypeExample生成ModeIslands
トップ / 会社情報 / LPTop / company / LP /, /company, /for-owners/, /about/ SSGSSG 原則なしNone
スポット詳細Spot detail /spot/[id]/ SSRSSR 地図 (ParkingMapIsland) Map (ParkingMapIsland)
駅ハブ (★中核)Station hub (core) /p/[pref]/[city]/[spotSlug]/[filter]?/ SSRSSR 地図 + sponsor マーカー Map + sponsor markers
都道府県・市区町村ハブPref / city hubs /p/[pref]/, /p/[pref]/[city]/ SSRSSR ミニ地図 island Mini map island
シーン LPScene LP /scene/, /scene/[sceneSlug]/ SSGSSG なしNone
検索Search /search SSGSSG Pagefind を fetch する SearchIsland SearchIsland that fetches the Pagefind index
メディア indexMedia index /media/, /media/story/ SSGSSG なしNone
メディア記事 / カテゴリMedia article / category /media/[...slug]/, /media/category/[slug]/, /media/story/[...slug]/ SSRSSR なし (記事は Astro レンダリング) None (articles render in Astro)
編集者プロフィールEditor profiles /about/editors/[slug]/ SSGSSG なしNone

Web 版の API エンドポイントWeb-app API endpoints

Astro の SSR アダプタ (@astrojs/cloudflare) 上で src/pages/api/*.ts に置いた APIRoute が Cloudflare Workers として動作します。BFF (dev-api.parky.co.jp) とは別レイヤで、 公開ポータル固有の軽量ユーティリティを担当します。

With the Astro SSR adapter (@astrojs/cloudflare), files under src/pages/api/*.ts export APIRoute handlers that run as Cloudflare Workers. This is a thin utility layer distinct from the BFF (dev-api.parky.co.jp) and serves portal-specific concerns.

エンドポイントEndpoint 目的Purpose 上流Upstream
GET /api/route-eta GET /api/route-eta クエリ o_lng / o_lat / d_lng / d_lat を受け取り、 2 地点間の車移動時間 (渋滞加味) を秒単位で返す。 /search の「到着予想時刻」ボタンが叩く。 レスポンス: { durationSec, distanceM, source: "mapbox" }。 エラー時 400 / 502。Cloudflare edge で 120 秒キャッシュ (Cache-Control: public, max-age=120, s-maxage=120)。 Takes o_lng / o_lat / d_lng / d_lat query params and returns driving-traffic duration in seconds between the two points. Called by the "Arrival ETA" button on /search. Response: { durationSec, distanceM, source: "mapbox" }. Errors: 400 / 502. Cached on the Cloudflare edge for 120 seconds (Cache-Control: public, max-age=120, s-maxage=120). Mapbox Directions API (driving-traffic プロファイル) Mapbox Directions API (driving-traffic profile)
GET /robots.txt GET /robots.txt prod では sitemap を案内、dev / preview は全 Disallow In prod serves sitemap; in dev / preview returns full Disallow

採用技術Tech stack

エラー監視 (Sentry)Error monitoring (Sentry)

Web 版は @sentry/astro インテグレーションでクライアント Island の未処理例外・ハイドレーション失敗・ナビゲーション Breadcrumb を Sentry に送信します。 DSN は SENTRY_DSN 環境変数でビルド時に注入し、release は git short SHA。 SSG ビルドそのものは GitHub Actions ランナー上で完結するため、ビルド失敗は Actions のジョブログ側で確認します(Sentry にビルドエラーは送らない)。 ランタイム Island からの BFF (/v1/*) 呼出で発生した 5xx は Workers 側の toucan-js で capture されるので、Astro 側からは ブラウザで起きたクライアントエラーのみを担います。 詳細は ops / sentry-setup

The web app uses the @sentry/astro integration to forward client-island unhandled exceptions, hydration errors, and navigation breadcrumbs to Sentry. The DSN comes from the SENTRY_DSN env var at build time; the release identifier is the git short SHA. SSG builds themselves are scoped to the GitHub Actions runner and surface failures via job logs (build errors are not sent to Sentry). Runtime island calls into the BFF (/v1/*) — when those return 5xx, capture happens on Workers via toucan-js, so the Astro side only owns browser-side client errors. See ops / sentry-setup.

セキュリティ境界Security boundaries