Skip to content

Commit bbebce9

Browse files
committed
Switch contact flow to Workers and Turnstile
1 parent be6b310 commit bbebce9

46 files changed

Lines changed: 1024 additions & 149 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Contact Form
2-
RESEND_API_KEY=
32
CONTACT_TO_EMAIL=
43
CONTACT_FROM_EMAIL=
5-
GOOGLE_RECAPTCHA_SECRET_KEY=
4+
TURNSTILE_SECRET_KEY=
5+
PUBLIC_TURNSTILE_SITE_KEY=
66

77
# Qiita Settings
88
QIITA_API_ENDPOINT=https://qiita.com/api/v2
@@ -17,7 +17,3 @@ ZENN_USERNAME=
1717
# Google Analytics
1818
# 計測に使用するMeasurement ID(例: G-XXXXXXXXXX)
1919
PUBLIC_GOOGLE_ANALYTICS_ID=
20-
21-
# Google reCAPTCHA
22-
# reCAPTCHA v2のサイトキー(公開可能なキー)
23-
PUBLIC_GOOGLE_RECAPTCHA_SITE_KEY=

.prettierignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
src/content/blog/*
22
src/content/profile/*
3-
environment.md
3+
environment.md
4+
migrations/*.sql
5+
pnpm-lock.yaml

astro.config.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { defineConfig, fontProviders } from 'astro/config';
2+
import cloudflare from '@astrojs/cloudflare';
23
import mdx from '@astrojs/mdx';
34
import UnoCSS from 'unocss/astro';
45
import sitemap from '@astrojs/sitemap';
@@ -48,7 +49,11 @@ const codeFilenameMetaTransformer = {
4849
export default defineConfig({
4950
site: 'https://saku-space.com',
5051
cacheDir: './.astro-cache',
51-
output: 'static',
52+
output: 'server',
53+
adapter: cloudflare({
54+
imageService: 'compile',
55+
prerenderEnvironment: 'node',
56+
}),
5257
fonts: [
5358
{
5459
name: 'Zen Kaku Gothic New',

docs/public-release-checklist.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
# 公開リポジトリ移行チェックリスト
22

3-
パブリック化に伴う環境変数の再投入と動作確認フローをまとめています。Cloudflare Pages や Vercel など、デプロイ環境に合わせて調整してください
3+
パブリック化に伴う環境変数の再投入と動作確認フローをまとめています。Cloudflare Workers Static Assets へのデプロイを前提にしています
44

55
## 1. ローカル環境の `.env`
66

77
1. `.env.example` をコピーして `.env` を再作成する。
88
2. 以下の値を最新の本番向けクレデンシャルに置き換える。
9-
- `PUBLIC_CONTACT_FORM_ENDPOINT`
109
- `PUBLIC_GOOGLE_ANALYTICS_ID`
11-
- `PUBLIC_GOOGLE_RECAPTCHA_SITE_KEY`(reCAPTCHA v2 checkbox のサイトキー)
10+
- `PUBLIC_TURNSTILE_SITE_KEY`(Cloudflare Turnstile のサイトキー)
11+
- `TURNSTILE_SECRET_KEY`
12+
- `CONTACT_TO_EMAIL`
13+
- `CONTACT_FROM_EMAIL`
1214
- Qiita / Zenn のユーザー名(任意)
13-
3. reCAPTCHA 管理画面でサイトキーの許可ドメインに `saku-space.com` `localhost` が含まれることを確認する
15+
3. ローカル確認では Cloudflare Turnstile の公式テストキーを使える。本番キーを `localhost` に許可しなくてもよい
1416
4. `pnpm run dev` で起動し、フォーム送信やダークモード切替など主要機能を確認する。
1517

1618
## 2. デプロイ環境のシークレット設定
1719

18-
- Cloudflare Pages を利用する場合、プロジェクト設定の環境変数セクションに上記キーを登録する。
20+
- `TURNSTILE_SECRET_KEY` は Wrangler secret として登録する。
21+
- `CONTACT_TO_EMAIL`, `CONTACT_FROM_EMAIL`, `PUBLIC_TURNSTILE_SITE_KEY` は公開されても問題ない値として `wrangler.jsonc` の vars か Cloudflare 側の変数で管理する。
22+
- `CONTACT_DB` の D1 binding と `EMAIL` の Email Sending binding を `wrangler.jsonc` と Cloudflare 側で一致させる。
23+
- D1 は `saku-space-contact` / `7cfb9a52-694f-4c2b-aa06-0052e6080186` を使用する。schema は `migrations/0001_contact_submissions.sql` と一致させる。
24+
- Email Sending は `saku-space.com` を sending domain とし、`contact@saku-space.com` から送信する。Cloudflare 側で `cf-bounce.saku-space.com` の MX/SPF/DKIM と `_dmarc.saku-space.com` の DNS status が `ready` であることを確認する。
25+
- Turnstile 本番 widget は `saku-space-contact` を作成済み。公開サイトキーは `wrangler.jsonc``PUBLIC_TURNSTILE_SITE_KEY` に反映済み。
1926
- Google Analytics など第三者サービスでローテーションを実施した際は、同じ手順で最新値へ更新する。
2027
- 値の変更後は `pnpm run build``pnpm run preview` をローカルで実行し、エラーがないことを確認してからデプロイする。
2128

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const compat = new FlatCompat({
2323

2424
export default [
2525
{
26-
ignores: ['dist/**/*', 'node_modules/**/*', '.astro/**/*', 'videos/**/*'],
26+
ignores: ['dist/**/*', 'node_modules/**/*', '.astro/**/*', '.wrangler/**/*', 'videos/**/*'],
2727
},
2828
{
2929
files: ['**/*.css'],
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
CREATE TABLE IF NOT EXISTS contact_submissions (
2+
id TEXT PRIMARY KEY,
3+
name TEXT NOT NULL,
4+
email TEXT NOT NULL,
5+
subject TEXT NOT NULL,
6+
message TEXT NOT NULL,
7+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
8+
delivery_status TEXT NOT NULL DEFAULT 'pending',
9+
provider TEXT NOT NULL DEFAULT 'cloudflare-email',
10+
provider_message_id TEXT
11+
);
12+
13+
CREATE INDEX IF NOT EXISTS idx_contact_submissions_created_at
14+
ON contact_submissions (created_at DESC);
15+
16+
CREATE INDEX IF NOT EXISTS idx_contact_submissions_delivery_status
17+
ON contact_submissions (delivery_status);

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
"generate-ogp": "node scripts/build-ogp.js",
2929
"build:with-ogp": "pnpm run generate-ogp && pnpm run build",
3030
"build:cloudflare": "pnpm exec playwright install chromium chromium-headless-shell && pnpm run build:with-ogp",
31-
"analyze": "ANALYZE=true astro build"
31+
"analyze": "ANALYZE=true astro build",
32+
"db:migrate:local": "wrangler d1 migrations apply CONTACT_DB --local",
33+
"db:migrate:remote": "wrangler d1 migrations apply CONTACT_DB --remote"
3234
},
3335
"dependencies": {
3436
"@astrojs/cloudflare": "^13.5.5",

public/.assetsignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
_worker.js
2+
_routes.json
3+
.DS_Store
4+
**/.DS_Store

src/actions/index.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { defineAction } from 'astro:actions';
2+
import { env } from 'cloudflare:workers';
3+
import { createContactActionHandler, extractContactFormValues } from '../lib/contact/contactAction';
4+
import { submitContactSubmission } from '../lib/contact/contactService';
5+
import { createCloudflareEmailNotifier } from '../lib/contact/cloudflareEmailNotifier';
6+
import { createD1ContactSubmissionRepository } from '../lib/contact/d1Repository';
7+
import { createTurnstileVerifier } from '../lib/contact/turnstile';
8+
import type { ContactRuntimeEnv } from '../lib/contact/types';
9+
10+
const isContactRuntimeEnv = (
11+
runtimeEnv: Partial<ContactRuntimeEnv>
12+
): runtimeEnv is ContactRuntimeEnv =>
13+
Boolean(
14+
runtimeEnv.CONTACT_DB &&
15+
runtimeEnv.EMAIL &&
16+
runtimeEnv.CONTACT_TO_EMAIL &&
17+
runtimeEnv.CONTACT_FROM_EMAIL &&
18+
runtimeEnv.TURNSTILE_SECRET_KEY
19+
);
20+
21+
export const server = {
22+
contact: defineAction({
23+
accept: 'form',
24+
handler: async (formData, context) => {
25+
const runtimeEnv = env as Partial<ContactRuntimeEnv>;
26+
27+
if (!isContactRuntimeEnv(runtimeEnv)) {
28+
return {
29+
ok: false as const,
30+
values: extractContactFormValues(formData),
31+
fieldErrors: {},
32+
message: 'お問い合わせ機能の設定が不足しています。',
33+
};
34+
}
35+
36+
const actionHandler = createContactActionHandler({
37+
submitContactSubmission: (input) =>
38+
submitContactSubmission(input, {
39+
verifyTurnstile: createTurnstileVerifier({
40+
secretKey: runtimeEnv.TURNSTILE_SECRET_KEY,
41+
}),
42+
repository: createD1ContactSubmissionRepository(runtimeEnv.CONTACT_DB),
43+
notifier: createCloudflareEmailNotifier({
44+
email: runtimeEnv.EMAIL,
45+
toEmail: runtimeEnv.CONTACT_TO_EMAIL,
46+
fromEmail: runtimeEnv.CONTACT_FROM_EMAIL,
47+
}),
48+
}),
49+
});
50+
51+
return actionHandler(formData, { remoteIp: context.clientAddress });
52+
},
53+
}),
54+
};

src/cloudflare-env.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module 'cloudflare:workers' {
2+
export const env: import('./lib/contact/types').ContactRuntimeEnv;
3+
}

0 commit comments

Comments
 (0)