Sostituisce Resend con Cloudflare Email Service.
Usa il binding EMAIL per contatti e newsletter, eliminando la dipendenza da Resend e dalle relative API key. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -5,7 +5,6 @@ PUBLIC_CONTACT_TURNSTILE_SITE_KEY=
|
|||||||
PUBLIC_SITE_URL=http://localhost:4321
|
PUBLIC_SITE_URL=http://localhost:4321
|
||||||
|
|
||||||
CONTACT_FORM_MODE=dev
|
CONTACT_FORM_MODE=dev
|
||||||
CONTACT_FORM_RESEND_API_KEY=
|
|
||||||
CONTACT_FORM_FROM_EMAIL=no-reply@nexstudio.ai
|
CONTACT_FORM_FROM_EMAIL=no-reply@nexstudio.ai
|
||||||
CONTACT_FORM_TO_EMAIL=info@nexstudio.ai
|
CONTACT_FORM_TO_EMAIL=info@nexstudio.ai
|
||||||
CONTACT_FORM_TO_EMAIL_INFO=info@nexstudio.ai
|
CONTACT_FORM_TO_EMAIL_INFO=info@nexstudio.ai
|
||||||
@ -13,10 +12,9 @@ CONTACT_FORM_TO_EMAIL_PRIVACY=
|
|||||||
CONTACT_FORM_TO_EMAIL_CAREERS=
|
CONTACT_FORM_TO_EMAIL_CAREERS=
|
||||||
CONTACT_TURNSTILE_SECRET_KEY=
|
CONTACT_TURNSTILE_SECRET_KEY=
|
||||||
|
|
||||||
# Newsletter (custom D1 + Resend, double opt-in)
|
# Newsletter (custom D1 + Cloudflare Email Service, double opt-in)
|
||||||
# In produzione: NEWSLETTER_MODE=live (oppure CONTACT_FORM_MODE=live come fallback)
|
# In produzione: NEWSLETTER_MODE=live (oppure CONTACT_FORM_MODE=live come fallback)
|
||||||
NEWSLETTER_MODE=dev
|
NEWSLETTER_MODE=dev
|
||||||
NEWSLETTER_RESEND_API_KEY=
|
|
||||||
NEWSLETTER_FROM_EMAIL=no-reply@nexstudio.ai
|
NEWSLETTER_FROM_EMAIL=no-reply@nexstudio.ai
|
||||||
NEWSLETTER_NOTIFY_TO_EMAIL=info@nexstudio.ai
|
NEWSLETTER_NOTIFY_TO_EMAIL=info@nexstudio.ai
|
||||||
NEWSLETTER_ADMIN_TOKEN=
|
NEWSLETTER_ADMIN_TOKEN=
|
||||||
|
|||||||
21
README.md
21
README.md
@ -15,7 +15,7 @@ Sito istituzionale NexStudio basato su Astro, con deploy su Cloudflare e form co
|
|||||||
- Adapter Cloudflare (`@astrojs/cloudflare`)
|
- Adapter Cloudflare (`@astrojs/cloudflare`)
|
||||||
- Sitemap (`@astrojs/sitemap`)
|
- Sitemap (`@astrojs/sitemap`)
|
||||||
- Form backend su endpoint `/api/contact`
|
- Form backend su endpoint `/api/contact`
|
||||||
- Turnstile + Resend per anti-spam e invio email
|
- Turnstile + Cloudflare Email Service per anti-spam e invio email
|
||||||
|
|
||||||
## Struttura essenziale
|
## Struttura essenziale
|
||||||
|
|
||||||
@ -29,8 +29,7 @@ Sito istituzionale NexStudio basato su Astro, con deploy su Cloudflare e form co
|
|||||||
|
|
||||||
- Node.js 20+
|
- Node.js 20+
|
||||||
- npm 10+
|
- npm 10+
|
||||||
- Account Cloudflare (Pages/Workers + Turnstile)
|
- Account Cloudflare (Workers + Turnstile + Email Service con dominio onboardato, es. `nexstudio.ai`)
|
||||||
- Account Resend (API key + sender verificato)
|
|
||||||
|
|
||||||
## Avvio locale
|
## Avvio locale
|
||||||
|
|
||||||
@ -63,8 +62,7 @@ Variabili pubbliche (frontend):
|
|||||||
Variabili server (Cloudflare/local):
|
Variabili server (Cloudflare/local):
|
||||||
|
|
||||||
- `CONTACT_FORM_MODE` (`dev` oppure `live`)
|
- `CONTACT_FORM_MODE` (`dev` oppure `live`)
|
||||||
- `CONTACT_FORM_RESEND_API_KEY`
|
- `CONTACT_FORM_FROM_EMAIL` (consigliato: `no-reply@nexstudio.ai`, dominio verificato su Email Service)
|
||||||
- `CONTACT_FORM_FROM_EMAIL` (consigliato: `no-reply@nexstudio.ai`)
|
|
||||||
- `CONTACT_FORM_TO_EMAIL` (default destinatario, es. `info@nexstudio.ai`)
|
- `CONTACT_FORM_TO_EMAIL` (default destinatario, es. `info@nexstudio.ai`)
|
||||||
- `CONTACT_FORM_TO_EMAIL_INFO` (opzionale, override esplicito per area "info")
|
- `CONTACT_FORM_TO_EMAIL_INFO` (opzionale, override esplicito per area "info")
|
||||||
- `CONTACT_FORM_TO_EMAIL_PRIVACY`
|
- `CONTACT_FORM_TO_EMAIL_PRIVACY`
|
||||||
@ -74,7 +72,7 @@ Variabili server (Cloudflare/local):
|
|||||||
Comportamento:
|
Comportamento:
|
||||||
|
|
||||||
- `dev`: accetta submit e scrive payload nei log (senza invio reale)
|
- `dev`: accetta submit e scrive payload nei log (senza invio reale)
|
||||||
- `live`: valida Turnstile e invia email via Resend
|
- `live`: valida Turnstile e invia email via binding `EMAIL` (Cloudflare Email Service)
|
||||||
|
|
||||||
Routing pubblico consigliato (portale vetrina):
|
Routing pubblico consigliato (portale vetrina):
|
||||||
|
|
||||||
@ -91,12 +89,13 @@ Indirizzi consigliati portale:
|
|||||||
|
|
||||||
## Configurazione newsletter
|
## Configurazione newsletter
|
||||||
|
|
||||||
Sistema custom su Cloudflare D1 + Resend (double opt-in).
|
Sistema custom su Cloudflare D1 + Cloudflare Email Service (double opt-in).
|
||||||
|
|
||||||
|
Prerequisito: dominio (es. `nexstudio.ai`) onboardato in **Email Service** con SPF/DKIM, e binding `send_email` → `EMAIL` nel Worker.
|
||||||
|
|
||||||
Variabili server:
|
Variabili server:
|
||||||
|
|
||||||
- `NEWSLETTER_MODE` (`dev` oppure `live`; se assente usa `CONTACT_FORM_MODE`)
|
- `NEWSLETTER_MODE` (`dev` oppure `live`; se assente usa `CONTACT_FORM_MODE`)
|
||||||
- `NEWSLETTER_RESEND_API_KEY` (fallback: `CONTACT_FORM_RESEND_API_KEY`)
|
|
||||||
- `NEWSLETTER_FROM_EMAIL` (fallback: `CONTACT_FORM_FROM_EMAIL`, consigliato `no-reply@nexstudio.ai`)
|
- `NEWSLETTER_FROM_EMAIL` (fallback: `CONTACT_FORM_FROM_EMAIL`, consigliato `no-reply@nexstudio.ai`)
|
||||||
- `NEWSLETTER_NOTIFY_TO_EMAIL` (notifiche admin; fallback: `CONTACT_FORM_TO_EMAIL`)
|
- `NEWSLETTER_NOTIFY_TO_EMAIL` (notifiche admin; fallback: `CONTACT_FORM_TO_EMAIL`)
|
||||||
- `NEWSLETTER_ADMIN_TOKEN` (Bearer token per endpoint admin)
|
- `NEWSLETTER_ADMIN_TOKEN` (Bearer token per endpoint admin)
|
||||||
@ -114,6 +113,8 @@ Endpoint admin (header `Authorization: Bearer <NEWSLETTER_ADMIN_TOKEN>`):
|
|||||||
- `GET /api/newsletter/admin/subscribers?status=active|pending|unsubscribed`
|
- `GET /api/newsletter/admin/subscribers?status=active|pending|unsubscribed`
|
||||||
- `POST /api/newsletter/admin/send` — body JSON `{ "subject", "text", "html?", "dryRun?" }`
|
- `POST /api/newsletter/admin/send` — body JSON `{ "subject", "text", "html?", "dryRun?" }`
|
||||||
|
|
||||||
|
Nota: Email Service è pensato per email **transazionali** (conferme, contatti, notifiche). Le campagne marketing di massa non sono ancora il target ufficiale del servizio.
|
||||||
|
|
||||||
Database:
|
Database:
|
||||||
|
|
||||||
- D1 `nexstudio-newsletter` (binding `NEWSLETTER_DB`)
|
- D1 `nexstudio-newsletter` (binding `NEWSLETTER_DB`)
|
||||||
@ -122,8 +123,8 @@ Database:
|
|||||||
|
|
||||||
Comportamento:
|
Comportamento:
|
||||||
|
|
||||||
- `dev`: accetta submit e logga (senza D1/Resend obbligatori)
|
- `dev`: accetta submit e logga (senza D1/Email obbligatori)
|
||||||
- `live`: scrive su D1, invia email di conferma via Resend, richiede binding D1
|
- `live`: scrive su D1, invia email di conferma via Cloudflare Email Service, richiede binding D1 + EMAIL
|
||||||
|
|
||||||
## Deploy Cloudflare
|
## Deploy Cloudflare
|
||||||
|
|
||||||
|
|||||||
45
src/lib/email.ts
Normal file
45
src/lib/email.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Outbound email via Cloudflare Email Service (`send_email` binding).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type RuntimeEnv = Record<string, unknown>;
|
||||||
|
|
||||||
|
type EmailBinding = {
|
||||||
|
send: (message: {
|
||||||
|
to: string;
|
||||||
|
from: string;
|
||||||
|
subject: string;
|
||||||
|
text?: string;
|
||||||
|
html?: string;
|
||||||
|
replyTo?: string;
|
||||||
|
}) => Promise<{ messageId: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getEmailBinding(runtimeEnv: RuntimeEnv): EmailBinding | null {
|
||||||
|
const binding = runtimeEnv.EMAIL;
|
||||||
|
if (binding && typeof binding === 'object' && 'send' in binding) {
|
||||||
|
return binding as EmailBinding;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendCloudflareEmail(
|
||||||
|
email: EmailBinding,
|
||||||
|
options: {
|
||||||
|
fromEmail: string;
|
||||||
|
toEmail: string;
|
||||||
|
subject: string;
|
||||||
|
text: string;
|
||||||
|
html?: string;
|
||||||
|
replyTo?: string;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
await email.send({
|
||||||
|
from: options.fromEmail,
|
||||||
|
to: options.toEmail,
|
||||||
|
subject: options.subject,
|
||||||
|
text: options.text,
|
||||||
|
...(options.html ? { html: options.html } : {}),
|
||||||
|
...(options.replyTo ? { replyTo: options.replyTo } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -100,34 +100,7 @@ export async function verifyTurnstile(
|
|||||||
return payload.success === true;
|
return payload.success === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendWithResend(options: {
|
export { getEmailBinding, sendCloudflareEmail } from './email';
|
||||||
apiKey: string;
|
|
||||||
fromEmail: string;
|
|
||||||
toEmail: string;
|
|
||||||
subject: string;
|
|
||||||
text: string;
|
|
||||||
html?: string;
|
|
||||||
}): Promise<void> {
|
|
||||||
const response = await fetch('https://api.resend.com/emails', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${options.apiKey}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
from: options.fromEmail,
|
|
||||||
to: [options.toEmail],
|
|
||||||
subject: options.subject,
|
|
||||||
text: options.text,
|
|
||||||
...(options.html ? { html: options.html } : {}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const detail = await response.text();
|
|
||||||
throw new Error(`Resend error ${response.status}: ${detail}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function siteOrigin(requestUrl: URL, runtimeEnv: RuntimeEnv): string {
|
export function siteOrigin(requestUrl: URL, runtimeEnv: RuntimeEnv): string {
|
||||||
const configured = readEnv('PUBLIC_SITE_URL', runtimeEnv) || readEnv('SITE', runtimeEnv);
|
const configured = readEnv('PUBLIC_SITE_URL', runtimeEnv) || readEnv('SITE', runtimeEnv);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
|
import { getEmailBinding, sendCloudflareEmail } from '../../lib/email';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
@ -58,27 +59,6 @@ async function verifyTurnstile(token: string, secret: string, ip?: string | null
|
|||||||
return payload.success === true;
|
return payload.success === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendWithResend(apiKey: string, fromEmail: string, toEmail: string, subject: string, text: string) {
|
|
||||||
const response = await fetch('https://api.resend.com/emails', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
from: fromEmail,
|
|
||||||
to: [toEmail],
|
|
||||||
subject,
|
|
||||||
text,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const detail = await response.text();
|
|
||||||
throw new Error(`Resend error ${response.status}: ${detail}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveDepartmentRecipient(runtimeEnv: RuntimeEnv, defaultRecipient: string, department: string): string {
|
function resolveDepartmentRecipient(runtimeEnv: RuntimeEnv, defaultRecipient: string, department: string): string {
|
||||||
const normalized = department.trim().toLowerCase();
|
const normalized = department.trim().toLowerCase();
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
@ -92,7 +72,6 @@ function resolveDepartmentRecipient(runtimeEnv: RuntimeEnv, defaultRecipient: st
|
|||||||
export const POST: APIRoute = async ({ request, locals, url }) => {
|
export const POST: APIRoute = async ({ request, locals, url }) => {
|
||||||
const runtimeEnv = getRuntimeEnv(locals);
|
const runtimeEnv = getRuntimeEnv(locals);
|
||||||
const mode = readEnv('CONTACT_FORM_MODE', runtimeEnv).toLowerCase() || 'dev';
|
const mode = readEnv('CONTACT_FORM_MODE', runtimeEnv).toLowerCase() || 'dev';
|
||||||
const resendApiKey = readEnv('CONTACT_FORM_RESEND_API_KEY', runtimeEnv);
|
|
||||||
const fromEmail = readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
|
const fromEmail = readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
|
||||||
const toEmail = readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
|
const toEmail = readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
|
||||||
const turnstileSecret = readEnv('CONTACT_TURNSTILE_SECRET_KEY', runtimeEnv);
|
const turnstileSecret = readEnv('CONTACT_TURNSTILE_SECRET_KEY', runtimeEnv);
|
||||||
@ -141,13 +120,23 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
if (mode === 'live') {
|
if (mode === 'live') {
|
||||||
if (!resendApiKey || !fromEmail || !toEmail) {
|
const emailBinding = getEmailBinding(runtimeEnv);
|
||||||
return new Response('Live mode requires CONTACT_FORM_RESEND_API_KEY, CONTACT_FORM_FROM_EMAIL and CONTACT_FORM_TO_EMAIL.', {
|
if (!emailBinding || !fromEmail || !toEmail) {
|
||||||
|
return new Response(
|
||||||
|
'Live mode requires EMAIL binding (Cloudflare Email Service), CONTACT_FORM_FROM_EMAIL and CONTACT_FORM_TO_EMAIL.',
|
||||||
|
{
|
||||||
status: 500,
|
status: 500,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const destination = resolveDepartmentRecipient(runtimeEnv, toEmail, department);
|
const destination = resolveDepartmentRecipient(runtimeEnv, toEmail, department);
|
||||||
await sendWithResend(resendApiKey, fromEmail, destination, subject, text);
|
await sendCloudflareEmail(emailBinding, {
|
||||||
|
fromEmail,
|
||||||
|
toEmail: destination,
|
||||||
|
subject,
|
||||||
|
text,
|
||||||
|
replyTo: email,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.info('[contact:dev] Contact payload received', {
|
console.info('[contact:dev] Contact payload received', {
|
||||||
name,
|
name,
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import {
|
import {
|
||||||
absoluteUrl,
|
absoluteUrl,
|
||||||
|
getEmailBinding,
|
||||||
getNewsletterDb,
|
getNewsletterDb,
|
||||||
getRuntimeEnv,
|
getRuntimeEnv,
|
||||||
readEnv,
|
readEnv,
|
||||||
sendWithResend,
|
sendCloudflareEmail,
|
||||||
siteOrigin,
|
siteOrigin,
|
||||||
type NewsletterSubscriber,
|
type NewsletterSubscriber,
|
||||||
} from '../../../../lib/newsletter';
|
} from '../../../../lib/newsletter';
|
||||||
@ -73,15 +74,13 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const resendApiKey =
|
const emailBinding = getEmailBinding(runtimeEnv);
|
||||||
readEnv('NEWSLETTER_RESEND_API_KEY', runtimeEnv) ||
|
|
||||||
readEnv('CONTACT_FORM_RESEND_API_KEY', runtimeEnv);
|
|
||||||
const fromEmail =
|
const fromEmail =
|
||||||
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
|
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
|
||||||
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
|
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
|
||||||
|
|
||||||
if (!resendApiKey || !fromEmail) {
|
if (!emailBinding || !fromEmail) {
|
||||||
return new Response('Missing Resend API key or from email for newsletter send.', {
|
return new Response('Missing EMAIL binding or from email for newsletter send.', {
|
||||||
status: 500,
|
status: 500,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -129,8 +128,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendWithResend({
|
await sendCloudflareEmail(emailBinding, {
|
||||||
apiKey: resendApiKey,
|
|
||||||
fromEmail,
|
fromEmail,
|
||||||
toEmail: subscriber.email,
|
toEmail: subscriber.email,
|
||||||
subject,
|
subject,
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import {
|
import {
|
||||||
|
getEmailBinding,
|
||||||
getNewsletterDb,
|
getNewsletterDb,
|
||||||
getRuntimeEnv,
|
getRuntimeEnv,
|
||||||
readEnv,
|
readEnv,
|
||||||
required,
|
required,
|
||||||
sendWithResend,
|
sendCloudflareEmail,
|
||||||
type NewsletterSubscriber,
|
type NewsletterSubscriber,
|
||||||
} from '../../../lib/newsletter';
|
} from '../../../lib/newsletter';
|
||||||
|
|
||||||
@ -57,9 +58,7 @@ export const GET: APIRoute = async ({ request, locals, url }) => {
|
|||||||
.bind(row.id)
|
.bind(row.id)
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
const resendApiKey =
|
const emailBinding = getEmailBinding(runtimeEnv);
|
||||||
readEnv('NEWSLETTER_RESEND_API_KEY', runtimeEnv) ||
|
|
||||||
readEnv('CONTACT_FORM_RESEND_API_KEY', runtimeEnv);
|
|
||||||
const fromEmail =
|
const fromEmail =
|
||||||
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
|
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
|
||||||
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
|
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
|
||||||
@ -67,10 +66,9 @@ export const GET: APIRoute = async ({ request, locals, url }) => {
|
|||||||
readEnv('NEWSLETTER_NOTIFY_TO_EMAIL', runtimeEnv) ||
|
readEnv('NEWSLETTER_NOTIFY_TO_EMAIL', runtimeEnv) ||
|
||||||
readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
|
readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
|
||||||
|
|
||||||
if (resendApiKey && fromEmail && notifyTo) {
|
if (emailBinding && fromEmail && notifyTo) {
|
||||||
try {
|
try {
|
||||||
await sendWithResend({
|
await sendCloudflareEmail(emailBinding, {
|
||||||
apiKey: resendApiKey,
|
|
||||||
fromEmail,
|
fromEmail,
|
||||||
toEmail: notifyTo,
|
toEmail: notifyTo,
|
||||||
subject: 'Iscrizione newsletter confermata — NexStudio',
|
subject: 'Iscrizione newsletter confermata — NexStudio',
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { APIRoute } from 'astro';
|
|||||||
import {
|
import {
|
||||||
absoluteUrl,
|
absoluteUrl,
|
||||||
createToken,
|
createToken,
|
||||||
|
getEmailBinding,
|
||||||
getNewsletterDb,
|
getNewsletterDb,
|
||||||
getRuntimeEnv,
|
getRuntimeEnv,
|
||||||
isValidEmail,
|
isValidEmail,
|
||||||
@ -9,7 +10,7 @@ import {
|
|||||||
readEnv,
|
readEnv,
|
||||||
required,
|
required,
|
||||||
sanitizeRedirect,
|
sanitizeRedirect,
|
||||||
sendWithResend,
|
sendCloudflareEmail,
|
||||||
siteOrigin,
|
siteOrigin,
|
||||||
verifyTurnstile,
|
verifyTurnstile,
|
||||||
type NewsletterSubscriber,
|
type NewsletterSubscriber,
|
||||||
@ -118,9 +119,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const resendApiKey =
|
const emailBinding = getEmailBinding(runtimeEnv);
|
||||||
readEnv('NEWSLETTER_RESEND_API_KEY', runtimeEnv) ||
|
|
||||||
readEnv('CONTACT_FORM_RESEND_API_KEY', runtimeEnv);
|
|
||||||
const fromEmail =
|
const fromEmail =
|
||||||
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
|
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
|
||||||
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
|
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
|
||||||
@ -128,9 +127,9 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|||||||
readEnv('NEWSLETTER_NOTIFY_TO_EMAIL', runtimeEnv) ||
|
readEnv('NEWSLETTER_NOTIFY_TO_EMAIL', runtimeEnv) ||
|
||||||
readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
|
readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
|
||||||
|
|
||||||
if (!resendApiKey || !fromEmail) {
|
if (!emailBinding || !fromEmail) {
|
||||||
return new Response(
|
return new Response(
|
||||||
'Live mode requires NEWSLETTER_RESEND_API_KEY (or CONTACT_FORM_RESEND_API_KEY) and NEWSLETTER_FROM_EMAIL (or CONTACT_FORM_FROM_EMAIL).',
|
'Live mode requires EMAIL binding (Cloudflare Email Service) and NEWSLETTER_FROM_EMAIL (or CONTACT_FORM_FROM_EMAIL).',
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -173,8 +172,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|||||||
|
|
||||||
const copy = confirmEmailCopy(locale, confirmUrl);
|
const copy = confirmEmailCopy(locale, confirmUrl);
|
||||||
try {
|
try {
|
||||||
await sendWithResend({
|
await sendCloudflareEmail(emailBinding, {
|
||||||
apiKey: resendApiKey,
|
|
||||||
fromEmail,
|
fromEmail,
|
||||||
toEmail: email,
|
toEmail: email,
|
||||||
subject: copy.subject,
|
subject: copy.subject,
|
||||||
@ -187,8 +185,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|||||||
|
|
||||||
if (notifyTo) {
|
if (notifyTo) {
|
||||||
try {
|
try {
|
||||||
await sendWithResend({
|
await sendCloudflareEmail(emailBinding, {
|
||||||
apiKey: resendApiKey,
|
|
||||||
fromEmail,
|
fromEmail,
|
||||||
toEmail: notifyTo,
|
toEmail: notifyTo,
|
||||||
subject: required(form.get('_subject')) || 'Nuova richiesta newsletter — NexStudio',
|
subject: required(form.get('_subject')) || 'Nuova richiesta newsletter — NexStudio',
|
||||||
|
|||||||
@ -17,5 +17,10 @@
|
|||||||
"database_id": "f2b30e34-6e63-4df8-8f92-f0a0dad08cda",
|
"database_id": "f2b30e34-6e63-4df8-8f92-f0a0dad08cda",
|
||||||
"migrations_dir": "migrations"
|
"migrations_dir": "migrations"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"send_email": [
|
||||||
|
{
|
||||||
|
"name": "EMAIL"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user