Skip to content

Commit c73c901

Browse files
nostrapolloclaude
andcommitted
Switch newsletter signup from Buttondown to Cloudflare D1
Stores subscriber emails directly in a D1 SQLite database. No external service dependencies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0795168 commit c73c901

5 files changed

Lines changed: 431 additions & 24 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ jobs:
1818
with:
1919
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
2020
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
21-
command: pages deploy dist --project-name=verifiedsoftware-dev
21+
command: pages deploy dist --project-name=verifiedsoftware-dev --config wrangler.toml

functions/api/subscribe.js

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export async function onRequestPost({ request, env }) {
2-
const corsHeaders = {
2+
const headers = {
3+
'Content-Type': 'application/json',
34
'Access-Control-Allow-Origin': '*',
45
'Access-Control-Allow-Methods': 'POST, OPTIONS',
56
'Access-Control-Allow-Headers': 'Content-Type',
@@ -11,35 +12,22 @@ export async function onRequestPost({ request, env }) {
1112
if (!email || !email.includes('@')) {
1213
return new Response(
1314
JSON.stringify({ success: false, error: 'Valid email required' }),
14-
{ status: 400, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
15+
{ status: 400, headers }
1516
);
1617
}
1718

18-
const res = await fetch('https://api.buttondown.com/v1/subscribers', {
19-
method: 'POST',
20-
headers: {
21-
'Authorization': `Token ${env.BUTTONDOWN_API_KEY}`,
22-
'Content-Type': 'application/json'
23-
},
24-
body: JSON.stringify({ email, tags: ['website'] })
25-
});
19+
await env.DB.prepare(
20+
'INSERT INTO subscribers (email, subscribed_at) VALUES (?, ?) ON CONFLICT(email) DO NOTHING'
21+
).bind(email.toLowerCase().trim(), new Date().toISOString()).run();
2622

27-
if (res.ok) {
28-
return new Response(
29-
JSON.stringify({ success: true }),
30-
{ status: 200, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
31-
);
32-
}
33-
34-
const err = await res.json();
3523
return new Response(
36-
JSON.stringify({ success: false, error: err }),
37-
{ status: 400, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
24+
JSON.stringify({ success: true }),
25+
{ status: 200, headers }
3826
);
3927
} catch (e) {
4028
return new Response(
4129
JSON.stringify({ success: false, error: 'Server error' }),
42-
{ status: 500, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
30+
{ status: 500, headers }
4331
);
4432
}
4533
}

0 commit comments

Comments
 (0)