このディレクトリは Astro の On-demand rendering で Cloudflare Workers ランタイム上で実行される API エンドポイントを置く。サイト全体は静的生成(SSG)だが、ここだけ Worker で動的処理する。
prerender = false を必ず付けるexport const prerender = false;
export const POST: APIRoute = async ({ request }) => { ... };
これを忘れるとビルド時に静的 HTML 化されて Worker で動かない。
cloudflare:workers の env 経由で取得import { env } from 'cloudflare:workers';
const apiKey = env.RESEND_API_KEY;
Astro.locals.runtime.env は Astro 6 / @astrojs/cloudflare v13 で廃止されたので使わない。
ローカル開発: .dev.vars(Wrangler が読む、.gitignore 対象)
本番: npx wrangler secret put <KEY>
public/_headers は静的アセットにしか効かないので、API レスポンスのヘッダーは Worker 内で設定する:
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
},
});
catch (error) {
console.error('Internal error:', error); // Worker ログのみ
return new Response(
JSON.stringify({ error: 'リクエストの処理中にエラーが発生しました' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
スタックトレースをクライアントに返さない。
5 段階で守っている。新しいフォーム系 API を追加するときも同じ層で考える:
website — オフスクリーン + aria-hidden + tabindex="-1"。ボットが埋めたら拒否isValidEmail())sanitizeHeader() で CR/LF を除去Resend で送信する subject / from / reply_to に CR/LF が混入すると追加ヘッダーを注入される。sanitizeHeader() で除去:
function sanitizeHeader(s: string): string {
return s.replace(/[\r\n]/g, '');
}
reply_to に使う email は isValidEmail() の正規表現で改行を弾いている(多重防御)。
public/_headers ではなく ../../../astro.config.mjs の security.csp で生成される CSP が静的ページに適用される。新しい外部サービスを呼び出すフォームを追加するときは:
connect-src を astro.config.mjs で更新外部サービス連携時は API キーを漏らさないため可能な限り Worker 経由を選ぶ。