アーキテクチャ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)
/search は Pagefind(astro-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.
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.
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 対象)
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"]
| 種別 | Type | 例 | Example | 生成 | Mode | 島 | Islands |
|---|---|---|---|---|---|---|---|
| トップ / 会社情報 / LP | Top / company / LP | /, /company, /for-owners/, /about/ |
SSG | SSG | 原則なし | None | |
| スポット詳細 | Spot detail | /spot/[id]/ |
SSR | SSR | 地図 (ParkingMapIsland) |
Map (ParkingMapIsland) |
|
| 駅ハブ (★中核) | Station hub (core) | /p/[pref]/[city]/[spotSlug]/[filter]?/ |
SSR | SSR | 地図 + sponsor マーカー | Map + sponsor markers | |
| 都道府県・市区町村ハブ | Pref / city hubs | /p/[pref]/, /p/[pref]/[city]/ |
SSR | SSR | ミニ地図 island | Mini map island | |
| シーン LP | Scene LP | /scene/, /scene/[sceneSlug]/ |
SSG | SSG | なし | None | |
| 検索 | Search | /search |
SSG | SSG | Pagefind を fetch する SearchIsland |
SearchIsland that fetches the Pagefind index |
|
| メディア index | Media index | /media/, /media/story/ |
SSG | SSG | なし | None | |
| メディア記事 / カテゴリ | Media article / category | /media/[...slug]/, /media/category/[slug]/, /media/story/[...slug]/ |
SSR | SSR | なし (記事は Astro レンダリング) | None (articles render in Astro) | |
| 編集者プロフィール | Editor profiles | /about/editors/[slug]/ |
SSG | SSG | なし | 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
- Astro 5 — SSG + Islands アーキテクチャSSG + Islands.
- React 19 —
@parky/uiのコンポーネントを island として埋め込みIslands from@parky/ui. - Tailwind CSS v4 — デザイントークン + Parky ブランド変数Design tokens + Parky brand variables.
- @parky/bff-client — Workers BFF (
/v1/*) を叩く統合 TS クライアント。ビルド時 (Astro) もランタイム (Island) もこれ経由Unified TS client for the Workers BFF (/v1/*); used both at build (Astro) and at runtime (islands). - Mapbox GL JS — 地図 island 用For the map island.
- MDX — メディア記事For media articles.
エラー監視 (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
- ビルドは GitHub Actions の閉じたランナー上で実行し、Supabase 接続は **BFF (Workers) 内に閉じる**。GitHub Actions / Astro ビルド成果物には Supabase / R2 等のシークレットを一切含めない
- Builds run on closed GitHub Actions runners; the Supabase connection lives **only inside the BFF (Workers)**. No Supabase / R2 secrets are exposed to GitHub Actions or bundled into the Astro output.
- 成果物の静的 HTML / Islands JS には機密情報を含めない(公開エンドポイントは BFF (
/v1/*) のみ) - No secrets in the static HTML / Islands JS bundle; the only public surface from the browser is the BFF (
/v1/*). - ランタイムのクライアントサイド検索は Pagefind の静的インデックスを
fetchするため、追加のサーバ通信は発生しない - Runtime client-side search just
fetches the Pagefind static index — no additional server traffic.