- Ultimo aggiornamento: 7 Dicembre 2025
+ Ultimo aggiornamento: 24 luglio 2026
@@ -39,7 +39,7 @@ const Privacy = () => {
Il titolare del trattamento dei dati personali e Prime Build, con sede a Padova, Italia.
- Email di contatto: primebuild.official@gmail.com
+ Email di contatto: preventivi@primebuild.website
@@ -53,6 +53,7 @@ const Privacy = () => {
Nome
Indirizzo email
Oggetto e contenuto del messaggio
+
Informazioni tecniche e di budget fornite nel questionario preventivo
2.2 Dati raccolti automaticamente (con consenso)
@@ -75,7 +76,8 @@ const Privacy = () => {
3. Finalita del Trattamento
I tuoi dati vengono utilizzati per:
- Rispondere alle richieste inviate tramite il modulo di contatto
+ Rispondere e inviare conferme operative per le richieste trasmesse tramite il modulo
+ Qualificare le richieste di preventivo e preparare la stima scelta dall'utente
Migliorare il sito web analizzando statistiche anonime di utilizzo
Garantire la sicurezza del sito prevenendo abusi e spam
@@ -112,15 +114,16 @@ const Privacy = () => {
I tuoi dati non vengono venduti a terzi. Potrebbero essere condivisi solo con:
- Google LLC - Per l'elaborazione delle statistiche Analytics (soggetto alle loro policy privacy)
- Cloudflare Inc. - Per l'hosting e la protezione del sito
+ Google LLC - Per Google Analytics e, se compilato, per il questionario Google Forms
+ Cloudflare Inc. - Per hosting, protezione anti-spam e gestione tecnica delle richieste
+ Resend - Per l'invio delle email operative e delle conferme automatiche
6. Conservazione dei Dati
- I messaggi inviati tramite il form di contatto vengono conservati per il tempo necessario a evadere la richiesta
+ I messaggi e le risposte al questionario vengono conservati per il tempo necessario a valutare ed evadere la richiesta
I dati di Google Analytics vengono conservati per 14 mesi, poi automaticamente eliminati
@@ -138,8 +141,8 @@ const Privacy = () => {
Per esercitare questi diritti, contattaci a{" "}
-
- primebuild.official@gmail.com
+
+ preventivi@primebuild.website
@@ -169,7 +172,7 @@ const Privacy = () => {
puoi contattarci a:
diff --git a/docs/email-flow-setup.md b/docs/email-flow-setup.md
new file mode 100644
index 0000000..dd6f7cc
--- /dev/null
+++ b/docs/email-flow-setup.md
@@ -0,0 +1,171 @@
+# Flusso email professionale Prime Build
+
+## Architettura scelta
+
+```text
+Modulo sito + Turnstile
+ |
+ v
+Cloudflare Pages Function /contact
+ |-- Resend --> [PRIMO CONTATTO] alla casella interna
+ `-- Resend --> conferma al cliente + link Google Form
+ |
+ v
+ Google Form di qualificazione
+ |
+ v
+ notifica/risposta qualificata
+```
+
+- **Indirizzo pubblico e Reply-To:** `preventivi@primebuild.website`.
+- **Destinazione reale:** la casella Gmail esistente, nascosta dietro Cloudflare Email Routing.
+- **Mittente automatico:** `Prime Build Preventivi
`.
+- Non usare `no-reply@`: impedisce una risposta naturale e comunica distanza proprio quando il cliente deve fidarsi.
+- `info@` è generico e riceve più spam; `supporto@` va riservato in futuro ai clienti con un servizio già attivo.
+
+Il primo messaggio resta disponibile, ma arriva con oggetto `[PRIMO CONTATTO]`: in Gmail può essere archiviato automaticamente sotto l'etichetta `Da qualificare`. La posta prioritaria sarà quindi la notifica di una risposta completa al Google Form.
+
+## Perché il Google Form va nella prima risposta automatica
+
+Il link inviato per email aggiunge un piccolo impegno e verifica che l'indirizzo sia raggiungibile. Chi compila il secondo passaggio ha già letto differenze e prezzo dei due preventivi. Non mostrare subito il link sul sito: annullerebbe questo filtro.
+
+Il form dovrebbe richiedere almeno:
+
+1. email usata nel primo contatto;
+2. servizio scelto: Base gratuito oppure Completo €25;
+3. budget massimo e indicazione se include monitor/periferiche;
+4. giochi, software e utilizzo principale;
+5. risoluzione e refresh rate desiderati;
+6. componenti già posseduti;
+7. preferenze o vincoli (dimensioni, rumore, Wi-Fi, estetica);
+8. tempistiche;
+9. acquisto/assemblaggio autonomo oppure servizio Prime Build;
+10. conferma di aver compreso cosa include il livello scelto.
+
+Evita upload di file e domande non necessarie. Non richiedere l'accesso Google: l'apertura del link dalla casella email è già un filtro sufficiente e il login farebbe perdere lead validi.
+
+## Setup manuale Cloudflare Email Routing
+
+Email Routing risulta già attivo sul dominio: sono presenti gli MX Cloudflare e l'SPF `include:_spf.mx.cloudflare.net`. Non eliminarli.
+
+1. Apri **Cloudflare → Email → Email Routing → Destination addresses**.
+2. Verifica la casella Gmail di destinazione, se non è già verificata.
+3. In **Routing rules → Custom addresses**, crea:
+ - `preventivi@primebuild.website` → Gmail esistente;
+ - facoltativo: `dmarc@primebuild.website` → Gmail esistente, per ricevere i report DMARC.
+4. Invia un messaggio esterno a `preventivi@primebuild.website` e verifica ricezione e risposta.
+5. Non attivare un catch-all: aumenta lo spam senza migliorare il servizio.
+
+### Regola legacy `no-reply@`
+
+`no-reply@primebuild.website` (con trattino) non compare nel codice né negli altri repository Prime Build. La versione precedente del sito usava soltanto `noreply@primebuild.website` (senza trattino) come mittente in uscita, e questa PR la sostituisce con `preventivi@primebuild.website`. Una regola Email Routing non è necessaria per spedire: serve solo a ricevere risposte. Si può quindi disattivare la regola `no-reply@`, attendere 7–14 giorni e poi eliminarla se non emergono messaggi inattesi.
+
+Non eliminare invece i record DNS MX/TXT di `noreply.primebuild.website`: sono il Return-Path tecnico verificato di Resend e non rappresentano una casella o una regola di inoltro.
+
+## Setup manuale Resend e DNS
+
+Sono già presenti il DKIM `resend._domainkey.primebuild.website` e il Return-Path tecnico `noreply.primebuild.website` con SPF/MX Amazon SES. Devono restare invariati e il dominio deve risultare **Verified** nella dashboard Resend. La creazione dell'indirizzo inoltrato `dmarc@primebuild.website` non crea il record DNS DMARC: sono due configurazioni distinte.
+
+1. Apri **Resend → Domains** e aggiungi o seleziona `primebuild.website`.
+2. Copia in **Cloudflare → DNS** esattamente i record DKIM, SPF e MX mostrati da Resend.
+3. Non sostituire gli MX Cloudflare all'apice e non creare un secondo SPF all'apice. In questa configurazione Resend usa `noreply.primebuild.website` come Return-Path, separato dall'indirizzo `no-reply@primebuild.website` e dall'SPF di Email Routing.
+4. Lascia i record email in modalità **DNS only** quando applicabile.
+5. Premi **Verify DNS Records** in Resend e attendi lo stato `Verified`.
+6. Aggiungi il record iniziale:
+
+ ```text
+ Type: TXT
+ Name: _dmarc
+ Value: v=DMARC1; p=none; rua=mailto:dmarc@primebuild.website; adkim=r; aspf=r; pct=100
+ ```
+
+ Dopo almeno 2–4 settimane di report senza mittenti legittimi non allineati, valuta `p=quarantine`. Non partire direttamente con `reject`.
+7. In **Resend → API Keys**, crea una chiave con sola autorizzazione di invio. Copiala una volta e non inserirla nel repository.
+8. Lascia disattivato il tracking di aperture/click salvo reale necessità : riduce trattamento dati e complessità privacy.
+
+## Variabili Cloudflare Pages
+
+Apri **Cloudflare → Workers & Pages → website → Settings → Variables and Secrets** e configura Production (e Preview solo per i test):
+
+| Nome | Tipo | Valore |
+|---|---|---|
+| `RESEND_API_KEY` | Secret | `re_...` |
+| `QUALIFICATION_FORM_URL` | Variable | URL HTTPS completo del Google Form |
+| `TURNSTILE_SECRET_KEY` | Secret | chiave Turnstile già in uso |
+| `MAIL_TO` | Variable/Secret | casella Gmail interna |
+| `MAIL_FROM` | Variable | `preventivi@primebuild.website` |
+| `MAIL_REPLY_TO` | Variable | `preventivi@primebuild.website` |
+| `EMAIL_STRICT` | Variable | `1` |
+| `ALLOWED_ORIGINS` | Variable | `https://primebuild.website` |
+
+La chiave Resend deve esistere soltanto lato Cloudflare. Non usare mai un nome `VITE_RESEND_API_KEY`: le variabili `VITE_*` finiscono nel JavaScript pubblico.
+
+Alternativa CLI per i secret:
+
+```bash
+npx wrangler pages secret put RESEND_API_KEY --project-name website
+npx wrangler pages secret put TURNSTILE_SECRET_KEY --project-name website
+```
+
+Per sviluppo locale crea `.dev.vars` (ignorato da Git):
+
+```dotenv
+RESEND_API_KEY=re_xxx
+TURNSTILE_SECRET_KEY=xxx
+QUALIFICATION_FORM_URL=https://docs.google.com/forms/d/e/xxx/viewform
+MAIL_TO=casella-interna@example.com
+MAIL_FROM=preventivi@primebuild.website
+MAIL_REPLY_TO=preventivi@primebuild.website
+```
+
+Dopo ogni variazione alle impostazioni, avvia un nuovo deploy Cloudflare.
+
+## Configurazione Google Form e Gmail
+
+1. Collega il Google Form a un Google Sheet.
+2. Attiva **Ricevi notifiche email per le nuove risposte** dal menu delle risposte/Sheet.
+3. Nel testo finale del Form usa:
+
+ > Grazie, la richiesta è completa. Il team Prime Build potrà ora valutarla. L'invio del questionario non comporta acquisti automatici; eventuali passaggi successivi saranno comunicati via email.
+
+4. In Gmail crea un filtro per gli oggetti che contengono `[PRIMO CONTATTO]`: applica l'etichetta `Da qualificare` e, se desiderato, ignora la Posta in arrivo.
+5. Lascia in Posta in arrivo le notifiche del Google Form: saranno i lead che hanno completato il percorso.
+
+## Copy della conferma automatica
+
+**Oggetto:** `Abbiamo ricevuto la tua richiesta | Prime Build`
+
+> Ciao {{nome}},
+>
+> abbiamo ricevuto il tuo primo contatto.
+>
+> Per preparare una proposta adatta alle tue esigenze, completa il questionario: ci aiuterà a valutare budget, utilizzo, giochi o software, risoluzione, componenti già disponibili, preferenze e tempistiche.
+>
+> **Preventivo Base — gratuito**
+> Include l'analisi delle tue esigenze, una stima del costo complessivo e l'indicazione della fascia di configurazione più adatta. È sufficiente per capire che tipo di PC e quale livello di prestazioni potresti ricevere affidando a Prime Build la realizzazione e l'assemblaggio.
+>
+> **Preventivo Completo — €25**
+> Include la stessa analisi, con in più il modello esatto di ogni componente, i link di acquisto, i prezzi consigliati rilevati al momento della ricerca e una checklist completa di compatibilità . È pensato per chi vuole poter acquistare e assemblare la build in autonomia.
+>
+> Se dopo aver acquistato il Preventivo Completo decidi di affidare comunque a Prime Build l'assemblaggio, i €25 già pagati verranno sottratti dal costo della manodopera.
+>
+> I preventivi riflettono prezzi e disponibilità presenti nel momento in cui vengono preparati. Il mercato dei componenti può cambiare rapidamente e Prime Build non vende direttamente i singoli componenti. Se rileviamo un prezzo particolarmente alto, te lo comunichiamo con trasparenza e, quando ci sono indicazioni concrete di un possibile calo a breve, possiamo consigliarti di attendere.
+>
+> Se ci hai contattato per assistenza relativa a un servizio già in corso, puoi rispondere direttamente a questa email senza compilare il questionario.
+>
+> Team Prime Build
+
+Il codice in `functions/contact.js` genera sia HTML sia testo semplice e inserisce il pulsante verso `QUALIFICATION_FORM_URL`.
+
+## Test prima del merge
+
+1. Configura routing, DNS, API key e URL del Form.
+2. Invia il modulo con una casella diversa da quella interna.
+3. Verifica:
+ - Turnstile blocca invii privi di token;
+ - arriva la notifica interna con Reply-To del cliente;
+ - il cliente riceve HTML e testo semplice;
+ - il pulsante apre il Form corretto;
+ - rispondendo alla conferma si raggiunge `preventivi@primebuild.website`;
+ - SPF, DKIM e DMARC risultano `PASS` nelle intestazioni del messaggio.
+4. In Cloudflare crea facoltativamente una regola di rate limiting su `POST /contact` (per esempio 5 richieste ogni 10 minuti per IP), senza sostituire Turnstile.
diff --git a/functions/contact.js b/functions/contact.js
index e577212..2ccfaae 100644
--- a/functions/contact.js
+++ b/functions/contact.js
@@ -5,6 +5,7 @@
const DEFAULT_ALLOWED_ORIGIN = 'https://primebuild.website';
const DEFAULT_TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
+const RESEND_EMAILS_URL = 'https://api.resend.com/emails';
function parseAllowedOrigins(env) {
const configured = (env.ALLOWED_ORIGINS || DEFAULT_ALLOWED_ORIGIN)
@@ -60,17 +61,78 @@ function jsonResponse(payload, status, allowOrigin = null) {
});
}
-function sanitize(value, max) {
- if (!value) return '';
+function clean(value, max) {
+ return value ? String(value).trim().slice(0, max) : '';
+}
- return String(value)
+function escapeHtml(value) {
+ return value
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
- .replace(/'/g, ''')
- .trim()
- .slice(0, max);
+ .replace(/'/g, ''');
+}
+
+function getHttpsUrl(value) {
+ try {
+ const url = new URL(value);
+ return url.protocol === 'https:' ? url.toString() : '';
+ } catch {
+ return '';
+ }
+}
+
+async function sendEmail(apiKey, payload) {
+ return fetch(RESEND_EMAILS_URL, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${apiKey}`,
+ },
+ body: JSON.stringify(payload),
+ });
+}
+
+function buildConfirmation(name, qualificationFormUrl) {
+ const safeName = escapeHtml(name);
+ const safeUrl = escapeHtml(qualificationFormUrl);
+ const subject = 'Abbiamo ricevuto la tua richiesta | Prime Build';
+ const text = `Ciao ${name},
+
+abbiamo ricevuto il tuo primo contatto.
+
+Per preparare una proposta adatta alle tue esigenze, completa il questionario: ${qualificationFormUrl}
+
+Ci aiuterà a valutare budget, utilizzo, giochi o software, risoluzione, componenti già disponibili, preferenze e tempistiche.
+
+PREVENTIVO BASE — GRATUITO
+Include l'analisi delle tue esigenze, una stima del costo complessivo e l'indicazione della fascia di configurazione più adatta. È sufficiente per capire che tipo di PC e quale livello di prestazioni potresti ricevere affidando a Prime Build la realizzazione e l'assemblaggio.
+
+PREVENTIVO COMPLETO — €25
+Include la stessa analisi, con in più il modello esatto di ogni componente, i link di acquisto, i prezzi consigliati rilevati al momento della ricerca e una checklist completa di compatibilità . È pensato per chi vuole poter acquistare e assemblare la build in autonomia.
+
+Se dopo aver acquistato il Preventivo Completo decidi di affidare comunque a Prime Build l'assemblaggio, i €25 già pagati verranno sottratti dal costo della manodopera.
+
+I preventivi riflettono prezzi e disponibilità presenti nel momento in cui vengono preparati. Il mercato dei componenti può cambiare rapidamente e Prime Build non vende direttamente i singoli componenti. Se rileviamo un prezzo particolarmente alto, te lo comunichiamo con trasparenza e, quando ci sono indicazioni concrete di un possibile calo a breve, possiamo consigliarti di attendere.
+
+Se ci hai contattato per assistenza relativa a un servizio già in corso, puoi rispondere direttamente a questa email senza compilare il questionario.
+
+Team Prime Build`;
+ const html = `Ciao ${safeName},
+abbiamo ricevuto il tuo primo contatto.
+Per preparare una proposta adatta alle tue esigenze, completa il questionario: ci aiuterà a valutare budget, utilizzo, giochi o software, risoluzione, componenti già disponibili, preferenze e tempistiche.
+Completa il questionario
+Preventivo Base — gratuito
+Include l'analisi delle tue esigenze, una stima del costo complessivo e l'indicazione della fascia di configurazione più adatta. È sufficiente per capire che tipo di PC e quale livello di prestazioni potresti ricevere affidando a Prime Build la realizzazione e l'assemblaggio.
+Preventivo Completo — €25
+Include la stessa analisi, con in più il modello esatto di ogni componente, i link di acquisto, i prezzi consigliati rilevati al momento della ricerca e una checklist completa di compatibilità . È pensato per chi vuole poter acquistare e assemblare la build in autonomia.
+Se dopo aver acquistato il Preventivo Completo decidi di affidare comunque a Prime Build l'assemblaggio, i €25 già pagati verranno sottratti dal costo della manodopera.
+I preventivi riflettono prezzi e disponibilità presenti nel momento in cui vengono preparati. Il mercato dei componenti può cambiare rapidamente e Prime Build non vende direttamente i singoli componenti. Se rileviamo un prezzo particolarmente alto, te lo comunichiamo con trasparenza e, quando ci sono indicazioni concrete di un possibile calo a breve, possiamo consigliarti di attendere.
+Se ci hai contattato per assistenza relativa a un servizio già in corso, puoi rispondere direttamente a questa email senza compilare il questionario.
+Team Prime Build
`;
+
+ return { subject, text, html };
}
async function verifyTurnstileToken(request, env, token, debugEnabled) {
@@ -143,11 +205,11 @@ export const onRequestPost = async (context) => {
return jsonResponse({ success: true, skipped: true }, 200, allowOrigin);
}
- const name = sanitize(data.name, 100);
- const email = sanitize(data.email, 254);
- const subject = sanitize(data.subject, 150);
- const message = sanitize(data.message, 5000);
- const turnstileToken = sanitize(data.turnstileToken, 4096);
+ const name = clean(data.name, 100);
+ const email = clean(data.email, 254);
+ const subject = clean(data.subject, 150);
+ const message = clean(data.message, 5000);
+ const turnstileToken = clean(data.turnstileToken, 4096);
if (!name || !email || !subject || !message || !turnstileToken) {
return jsonResponse({ success: false, error: 'Missing fields' }, 400, allowOrigin);
@@ -164,72 +226,90 @@ export const onRequestPost = async (context) => {
const mailTo = env.MAIL_TO || 'primebuild.official@gmail.com';
const resendApiKey = env.RESEND_API_KEY;
- if (!mailTo || !resendApiKey) {
- console.error('Email service not configured');
+ const qualificationFormUrl = getHttpsUrl(env.QUALIFICATION_FORM_URL);
+ if (!mailTo || !resendApiKey || !qualificationFormUrl) {
+ console.error('Email service or qualification form not configured');
return jsonResponse({ success: false, error: 'Email service misconfigured' }, 503, allowOrigin);
}
- const fromEmail = env.MAIL_FROM || 'noreply@primebuild.website';
- const prefix = env.MAIL_SUBJECT_PREFIX ? `${env.MAIL_SUBJECT_PREFIX.trim()} ` : '';
+ const fromEmail = env.MAIL_FROM || 'preventivi@primebuild.website';
+ const replyTo = env.MAIL_REPLY_TO || fromEmail;
+ const prefix = env.MAIL_SUBJECT_PREFIX ? `${env.MAIL_SUBJECT_PREFIX.trim()} ` : '[PRIMO CONTATTO] ';
const html =
- `Nome: ${name}
- Email: ${email}
- Oggetto: ${subject}
- Messaggio: ${message.replace(/\n/g, ' ')}
`;
+ `Nome: ${escapeHtml(name)}
+ Email: ${escapeHtml(email)}
+ Oggetto: ${escapeHtml(subject)}
+ Messaggio: ${escapeHtml(message).replace(/\n/g, ' ')}
`;
const plain =
`Nome: ${name}\nEmail: ${email}\nOggetto: ${subject}\nMessaggio:\n${message}`;
+ let notificationSent = false;
try {
- const mailResp = await fetch('https://api.resend.com/emails', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${resendApiKey}`,
- },
- body: JSON.stringify({
- from: `Modulo Contatti <${fromEmail}>`,
- to: [mailTo],
- reply_to: email,
- subject: `${prefix}${subject}`,
- html,
- text: plain,
- }),
+ const notificationResponse = await sendEmail(resendApiKey, {
+ from: `Sito Prime Build <${fromEmail}>`,
+ to: [mailTo],
+ reply_to: email,
+ subject: `${prefix}${subject}`,
+ html,
+ text: plain,
});
- if (!mailResp.ok) {
- const errTxt = await mailResp.text().catch(() => '');
- console.error('Resend send failed', mailResp.status, errTxt);
- const payload = {
- success: strictEmail ? false : true,
- warning: 'Email send failed',
- degraded: !strictEmail,
- };
+ if (!notificationResponse.ok) {
+ const errorText = await notificationResponse.text().catch(() => '');
+ console.error('Resend notification failed', notificationResponse.status, errorText);
+ return jsonResponse(
+ {
+ success: !strictEmail,
+ warning: 'Email send failed',
+ degraded: !strictEmail,
+ },
+ strictEmail ? 502 : 200,
+ allowOrigin
+ );
+ }
- if (debugEnabled) {
- console.debug('Resend error body:', errTxt.slice(0, 400));
- }
+ notificationSent = true;
+ const confirmation = buildConfirmation(name, qualificationFormUrl);
+ const confirmationResponse = await sendEmail(resendApiKey, {
+ from: `Prime Build Preventivi <${fromEmail}>`,
+ to: [email],
+ reply_to: replyTo,
+ ...confirmation,
+ });
- return jsonResponse(payload, strictEmail ? 502 : 200, allowOrigin);
+ if (!confirmationResponse.ok) {
+ const errorText = await confirmationResponse.text().catch(() => '');
+ console.error('Resend confirmation failed', confirmationResponse.status, errorText);
+ return jsonResponse(
+ {
+ success: true,
+ confirmationSent: false,
+ warning: 'Confirmation email failed',
+ ms: Date.now() - start,
+ },
+ 200,
+ allowOrigin
+ );
}
} catch (error) {
console.error('Resend exception', error);
- const payload = {
- success: strictEmail ? false : true,
- warning: 'Email send exception',
- degraded: !strictEmail,
- };
-
if (debugEnabled) {
console.debug('Resend exception message:', (error && error.message) || String(error));
}
-
- return jsonResponse(payload, strictEmail ? 502 : 200, allowOrigin);
+ return jsonResponse(
+ notificationSent
+ ? { success: true, confirmationSent: false, warning: 'Confirmation email failed' }
+ : { success: false, error: 'Email send exception' },
+ notificationSent ? 200 : 502,
+ allowOrigin
+ );
}
return jsonResponse(
{
success: true,
+ confirmationSent: true,
message: 'Message received successfully!',
ms: Date.now() - start,
},
diff --git a/package.json b/package.json
index ce153fa..5afc491 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"dev": "vite",
"build": "vite build && tsx scripts/prerender-meta.ts && tsx scripts/check-seo.ts",
"preview": "vite preview",
- "check": "tsc",
+ "check": "tsc && node scripts/check-contact-email.mjs",
"knip": "knip"
},
"dependencies": {
diff --git a/scripts/check-contact-email.mjs b/scripts/check-contact-email.mjs
new file mode 100644
index 0000000..c96cc7f
--- /dev/null
+++ b/scripts/check-contact-email.mjs
@@ -0,0 +1,62 @@
+import assert from 'node:assert/strict';
+import { onRequestPost } from '../functions/contact.js';
+
+const resendPayloads = [];
+globalThis.fetch = async (url, options) => {
+ if (String(url).includes('siteverify')) {
+ return Response.json({ success: true });
+ }
+
+ if (url === 'https://api.resend.com/emails') {
+ resendPayloads.push(JSON.parse(options.body));
+ return Response.json({ id: `email-${resendPayloads.length}` });
+ }
+
+ throw new Error(`Unexpected request: ${url}`);
+};
+
+const env = {
+ ALLOWED_ORIGINS: 'https://primebuild.website',
+ TURNSTILE_SECRET_KEY: 'test-secret',
+ RESEND_API_KEY: 're_test',
+ QUALIFICATION_FORM_URL: 'https://docs.google.com/forms/d/e/test/viewform',
+ MAIL_TO: 'internal@example.com',
+ MAIL_FROM: 'preventivi@primebuild.website',
+ MAIL_REPLY_TO: 'preventivi@primebuild.website',
+ EMAIL_STRICT: '1',
+};
+
+const request = new Request('https://primebuild.website/contact', {
+ method: 'POST',
+ headers: {
+ Origin: 'https://primebuild.website',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ name: 'Lorenzo & Co',
+ email: 'cliente@example.com',
+ subject: 'Nuovo PC',
+ message: 'Budget 1500 euro',
+ turnstileToken: 'valid-token',
+ }),
+});
+
+const response = await onRequestPost({ request, env });
+const result = await response.json();
+assert.equal(response.status, 200);
+assert.equal(result.confirmationSent, true);
+assert.equal(resendPayloads.length, 2);
+assert.deepEqual(resendPayloads[0].to, ['internal@example.com']);
+assert.equal(resendPayloads[0].reply_to, 'cliente@example.com');
+assert.match(resendPayloads[0].subject, /^\[PRIMO CONTATTO\]/);
+assert.deepEqual(resendPayloads[1].to, ['cliente@example.com']);
+assert.equal(resendPayloads[1].reply_to, 'preventivi@primebuild.website');
+assert.match(resendPayloads[1].html, /Ciao Lorenzo & Co/);
+assert.match(resendPayloads[1].html, /Completa il questionario/);
+assert.match(resendPayloads[1].text, /PREVENTIVO COMPLETO — €25/);
+assert.match(resendPayloads[1].text, /Include la stessa analisi/);
+assert.match(resendPayloads[1].text, /sottratti dal costo della manodopera/);
+assert.match(resendPayloads[1].text, /Il mercato dei componenti può cambiare rapidamente/);
+assert.match(resendPayloads[1].text, /https:\/\/docs\.google\.com\/forms/);
+
+console.log('Contact email flow check passed.');
diff --git a/scripts/prerender-meta.ts b/scripts/prerender-meta.ts
index a3b9434..33a8f45 100644
--- a/scripts/prerender-meta.ts
+++ b/scripts/prerender-meta.ts
@@ -146,7 +146,7 @@ const shellFor = (path: string) => {
if (normalizedPath === "/") return `Montegrotto Terme · 35036 (PD)
PRIMEBUILD PC gaming progettati intorno a te. Consulenza reale, assemblaggio preciso e prestazioni ottimizzate senza sprechi.
PC gaming su misura · Assistenza PC · Ottimizzazione `;
if (normalizedPath === "/guides/") return `Guide PC gaming e ottimizzazione Approfondimenti su PC gaming, latenza, audio, rete e ottimizzazione.
${guides.map((item) => `${escapeHtml(item.description)}
`).join("")} `;
- if (normalizedPath === "/chi-siamo/") return `PC gaming progettati intorno alle persone Prime Build progetta e assembla PC gaming su misura, offre assistenza tecnica e ottimizzazione da Montegrotto Terme, Padova, con servizi disponibili in tutta Italia.
Il metodo Prime Build Budget e obiettivi guidano ogni proposta. Temperature, stabilità e prestazioni vengono misurate prima e dopo gli interventi. Componenti, modifiche e limiti vengono spiegati con chiarezza.
Il team tecnico Prime Build Le guide e le configurazioni pubblicate sul sito sono curate dal team tecnico Prime Build e riportano date e fonti quando disponibili.
Contatto: primebuild.official@gmail.com
`;
+ if (normalizedPath === "/chi-siamo/") return `PC gaming progettati intorno alle persone Prime Build progetta e assembla PC gaming su misura, offre assistenza tecnica e ottimizzazione da Montegrotto Terme, Padova, con servizi disponibili in tutta Italia.
Il metodo Prime Build Budget e obiettivi guidano ogni proposta. Temperature, stabilità e prestazioni vengono misurate prima e dopo gli interventi. Componenti, modifiche e limiti vengono spiegati con chiarezza.
Il team tecnico Prime Build Le guide e le configurazioni pubblicate sul sito sono curate dal team tecnico Prime Build e riportano date e fonti quando disponibili.
Contatto: preventivi@primebuild.website
`;
if (normalizedPath === "/privacy/") return `Privacy e Cookie Policy Informativa sul trattamento dei dati personali e sull'uso dei cookie del sito Prime Build.
`;
return "";
};
diff --git a/wrangler.toml b/wrangler.toml
index a293008..c1fcc32 100644
--- a/wrangler.toml
+++ b/wrangler.toml
@@ -4,7 +4,8 @@ compatibility_date = "2024-01-01"
[vars]
MAIL_TO = "primebuild.official@gmail.com"
-MAIL_FROM = "noreply@primebuild.website"
+MAIL_FROM = "preventivi@primebuild.website"
+MAIL_REPLY_TO = "preventivi@primebuild.website"
ALLOWED_ORIGINS = "https://primebuild.website"
EMAIL_STRICT = "1"
TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
@@ -12,6 +13,7 @@ DEBUG = "0"
# IMPORTANT: keep secrets out of repo.
# Configure via dashboard or CLI:
-# wrangler secret put RESEND_API_KEY
-# wrangler secret put TURNSTILE_SECRET_KEY
-# wrangler secret put GITHUB_TOKEN
+# npx wrangler pages secret put RESEND_API_KEY --project-name website
+# npx wrangler pages secret put TURNSTILE_SECRET_KEY --project-name website
+# Set QUALIFICATION_FORM_URL in Cloudflare Pages environment variables.
+# GITHUB_TOKEN is only needed by the release-download function.