From 1dbbbba71b4a0f1d6c5f586db3502ac962a0986b Mon Sep 17 00:00:00 2001 From: PrimeBuild Date: Fri, 24 Jul 2026 20:26:20 +0200 Subject: [PATCH 1/2] feat(contact): qualify leads with automatic email flow --- .gitignore | 1 + README.md | 2 +- client/public/llms.txt | 2 +- client/src/components/ContactSection.tsx | 15 +- client/src/lib/schema.ts | 2 +- client/src/pages/About.tsx | 2 +- client/src/pages/Privacy.tsx | 21 +-- docs/email-flow-setup.md | 161 ++++++++++++++++++++ functions/contact.js | 180 ++++++++++++++++------- package.json | 2 +- scripts/check-contact-email.mjs | 59 ++++++++ scripts/prerender-meta.ts | 2 +- wrangler.toml | 10 +- 13 files changed, 382 insertions(+), 77 deletions(-) create mode 100644 docs/email-flow-setup.md create mode 100644 scripts/check-contact-email.mjs diff --git a/.gitignore b/.gitignore index 8321d01..3661d81 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ server/public vite.config.ts.* *.tar.gz .env +.dev.vars *.pem \ No newline at end of file diff --git a/README.md b/README.md index 6768fd4..20c061b 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Questo progetto è sotto licenza MIT. Consulta il file `LICENSE` per maggiori in ## 📞 Contatti -PrimeBuild PC - [primebuild.official@gmail.com](mailto:primebuild.official@gmail.com) +PrimeBuild PC - [preventivi@primebuild.website](mailto:preventivi@primebuild.website) Project Link: [https://github.com/PrimeBuild-pc/webapp](https://github.com/PrimeBuild-pc/webapp) diff --git a/client/public/llms.txt b/client/public/llms.txt index db8fa46..058edca 100644 --- a/client/public/llms.txt +++ b/client/public/llms.txt @@ -22,7 +22,7 @@ - [Chi siamo](https://primebuild.website/chi-siamo/) - Sito ufficiale: https://primebuild.website/ - Sede: Montegrotto Terme, 35036 (PD), Italia -- Email: primebuild.official@gmail.com +- Email: preventivi@primebuild.website - Instagram: https://www.instagram.com/prime_build_/ - GitHub: https://github.com/PrimeBuild-pc - Discord: https://discord.gg/ERUwSxE79q diff --git a/client/src/components/ContactSection.tsx b/client/src/components/ContactSection.tsx index 51293a3..303f46a 100644 --- a/client/src/components/ContactSection.tsx +++ b/client/src/components/ContactSection.tsx @@ -144,7 +144,12 @@ const ContactSection = () => { trackFormSubmit('contact', true); setFormData(initialState); resetTurnstile(); - toast({ title: "Messaggio inviato", description: "Ti contatteremo al piu presto." }); + toast({ + title: "Richiesta ricevuta", + description: json.confirmationSent === false + ? "La conferma automatica non è partita: non inviare di nuovo, abbiamo comunque ricevuto il messaggio." + : "Controlla la tua email per il prossimo passo e il questionario preventivo.", + }); } catch (err) { console.error('Network/contact error', err); trackFormSubmit('contact', false); @@ -157,8 +162,8 @@ const ContactSection = () => { // Obfuscate email to prevent spam harvesters const getEmail = () => { - const user = "primebuild.official"; - const domain = "gmail.com"; + const user = "preventivi"; + const domain = "primebuild.website"; return `${user}@${domain}`; }; @@ -198,7 +203,7 @@ const ContactSection = () => { @@ -289,7 +294,7 @@ const ContactSection = () => { disabled={isSubmitting} className="button-primary disabled:cursor-not-allowed disabled:opacity-50" > - {isSubmitting ? "Invio in corso..." : "Invia Messaggio"} + {isSubmitting ? "Invio in corso..." : "Invia richiesta"}

Inviando questo modulo, dichiari di aver letto la nostra{" "} diff --git a/client/src/lib/schema.ts b/client/src/lib/schema.ts index abaef51..19126a9 100644 --- a/client/src/lib/schema.ts +++ b/client/src/lib/schema.ts @@ -13,7 +13,7 @@ export const buildSchema = (location: string) => { url: `${SITE_URL}/`, logo: { "@type": "ImageObject", url: assetUrl("/logo.png"), width: 302, height: 302 }, image: assetUrl("/og-image.png"), - email: "primebuild.official@gmail.com", + email: "preventivi@primebuild.website", description: "Assemblaggio PC gaming su misura, assistenza tecnica e ottimizzazione a Montegrotto Terme, Padova e in tutta Italia.", address: { "@type": "PostalAddress", diff --git a/client/src/pages/About.tsx b/client/src/pages/About.tsx index e0b2ae9..775d4bf 100644 --- a/client/src/pages/About.tsx +++ b/client/src/pages/About.tsx @@ -47,7 +47,7 @@ const About = () => {

Identità e contatti ufficiali

2.2 Dati raccolti automaticamente (con consenso)

@@ -75,7 +76,8 @@ const Privacy = () => {

3. Finalita del Trattamento

I tuoi dati vengono utilizzati per:

@@ -112,15 +114,16 @@ const Privacy = () => { I tuoi dati non vengono venduti a terzi. Potrebbero essere condivisi solo con:

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..12fcccc --- /dev/null +++ b/docs/email-flow-setup.md @@ -0,0 +1,161 @@ +# 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. + +## Setup manuale Resend e DNS + +È già presente un record `resend._domainkey.primebuild.website`, ma il dominio deve risultare **Verified** nella dashboard Resend. Al momento non risultano il Return-Path `send` né un record DMARC pubblico. + +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. Resend usa normalmente `send.primebuild.website` come Return-Path, separato 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 richiedere un preventivo, completa il questionario: ci permette di valutare budget, utilizzo, giochi o software, risoluzione, componenti già disponibili e tempistiche. Le richieste di preventivo vengono esaminate dopo la compilazione. +> +> **Preventivo Base — gratuito** +> Stima generale dei costi e indicazione della fascia di configurazione più adatta. Non include una distinta completa con modelli e link di acquisto. +> +> **Preventivo Completo — €25** +> Selezione precisa dei componenti con modello esatto, link diretti di acquisto al miglior prezzo trovato al momento della ricerca e checklist completa di compatibilità. È pensato anche per chi vuole acquistare i componenti e assemblare il PC in autonomia. +> +> Se ci hai contattato per assistenza su 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..1f783b0 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,72 @@ 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 richiedere un preventivo, completa il questionario: ${qualificationFormUrl} + +Ci permette di valutare budget, utilizzo, giochi o software, risoluzione, componenti già disponibili e tempistiche. Le richieste di preventivo vengono esaminate dopo la compilazione. + +PREVENTIVO BASE — GRATUITO +Stima generale dei costi e indicazione della fascia di configurazione più adatta. Non include una distinta completa con modelli e link di acquisto. + +PREVENTIVO COMPLETO — €25 +Selezione precisa dei componenti con modello esatto, link diretti di acquisto al miglior prezzo trovato al momento della ricerca e checklist completa di compatibilità. È pensato anche per chi vuole acquistare i componenti e assemblare il PC in autonomia. + +Se ci hai contattato per assistenza su 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 richiedere un preventivo, completa il questionario: ci permette di valutare budget, utilizzo, giochi o software, risoluzione, componenti già disponibili e tempistiche. Le richieste di preventivo vengono esaminate dopo la compilazione.

+

Completa il questionario

+

Preventivo Base — gratuito

+

Stima generale dei costi e indicazione della fascia di configurazione più adatta. Non include una distinta completa con modelli e link di acquisto.

+

Preventivo Completo — €25

+

Selezione precisa dei componenti con modello esatto, link diretti di acquisto al miglior prezzo trovato al momento della ricerca e checklist completa di compatibilità. È pensato anche per chi vuole acquistare i componenti e assemblare il PC in autonomia.

+

Se ci hai contattato per assistenza su 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 +199,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 +220,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..3230809 --- /dev/null +++ b/scripts/check-contact-email.mjs @@ -0,0 +1,59 @@ +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, /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)

PRIME
BUILD

PC gaming progettati intorno a te. Consulenza reale, assemblaggio preciso e prestazioni ottimizzate senza sprechi.

`; if (normalizedPath === "/guides/") return `

Guide PC gaming e ottimizzazione

Approfondimenti su PC gaming, latenza, audio, rete e ottimizzazione.

${guides.map((item) => ``).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. From ff30798075cdcd1c51d2fe4c29d9953a33bc927c Mon Sep 17 00:00:00 2001 From: PrimeBuild Date: Fri, 24 Jul 2026 22:13:54 +0200 Subject: [PATCH 2/2] fix(contact): clarify quote deliverables and pricing --- docs/email-flow-setup.md | 22 ++++++++++++++++------ functions/contact.js | 24 +++++++++++++++--------- scripts/check-contact-email.mjs | 3 +++ 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/docs/email-flow-setup.md b/docs/email-flow-setup.md index 12fcccc..dd6f7cc 100644 --- a/docs/email-flow-setup.md +++ b/docs/email-flow-setup.md @@ -56,13 +56,19 @@ Email Routing risulta già attivo sul dominio: sono presenti gli MX Cloudflare e 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 -È già presente un record `resend._domainkey.primebuild.website`, ma il dominio deve risultare **Verified** nella dashboard Resend. Al momento non risultano il Return-Path `send` né un record DMARC pubblico. +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. Resend usa normalmente `send.primebuild.website` come Return-Path, separato dall'SPF di Email Routing. +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: @@ -133,15 +139,19 @@ Dopo ogni variazione alle impostazioni, avvia un nuovo deploy Cloudflare. > > abbiamo ricevuto il tuo primo contatto. > -> Per richiedere un preventivo, completa il questionario: ci permette di valutare budget, utilizzo, giochi o software, risoluzione, componenti già disponibili e tempistiche. Le richieste di preventivo vengono esaminate dopo la compilazione. +> 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** -> Stima generale dei costi e indicazione della fascia di configurazione più adatta. Non include una distinta completa con modelli e link di acquisto. +> 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** -> Selezione precisa dei componenti con modello esatto, link diretti di acquisto al miglior prezzo trovato al momento della ricerca e checklist completa di compatibilità. È pensato anche per chi vuole acquistare i componenti e assemblare il PC in autonomia. +> 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 su un servizio già in corso, puoi rispondere direttamente a questa email senza compilare il questionario. +> 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 diff --git a/functions/contact.js b/functions/contact.js index 1f783b0..2ccfaae 100644 --- a/functions/contact.js +++ b/functions/contact.js @@ -102,28 +102,34 @@ function buildConfirmation(name, qualificationFormUrl) { abbiamo ricevuto il tuo primo contatto. -Per richiedere un preventivo, completa il questionario: ${qualificationFormUrl} +Per preparare una proposta adatta alle tue esigenze, completa il questionario: ${qualificationFormUrl} -Ci permette di valutare budget, utilizzo, giochi o software, risoluzione, componenti già disponibili e tempistiche. Le richieste di preventivo vengono esaminate dopo la compilazione. +Ci aiuterà a valutare budget, utilizzo, giochi o software, risoluzione, componenti già disponibili, preferenze e tempistiche. PREVENTIVO BASE — GRATUITO -Stima generale dei costi e indicazione della fascia di configurazione più adatta. Non include una distinta completa con modelli e link di acquisto. +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 -Selezione precisa dei componenti con modello esatto, link diretti di acquisto al miglior prezzo trovato al momento della ricerca e checklist completa di compatibilità. È pensato anche per chi vuole acquistare i componenti e assemblare il PC in autonomia. +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 ci hai contattato per assistenza su un servizio già in corso, puoi rispondere direttamente a questa email senza compilare il questionario. +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 richiedere un preventivo, completa il questionario: ci permette di valutare budget, utilizzo, giochi o software, risoluzione, componenti già disponibili e tempistiche. Le richieste di preventivo vengono esaminate dopo la compilazione.

+

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

-

Stima generale dei costi e indicazione della fascia di configurazione più adatta. Non include una distinta completa con modelli e link di acquisto.

+

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

-

Selezione precisa dei componenti con modello esatto, link diretti di acquisto al miglior prezzo trovato al momento della ricerca e checklist completa di compatibilità. È pensato anche per chi vuole acquistare i componenti e assemblare il PC in autonomia.

-

Se ci hai contattato per assistenza su un servizio già in corso, puoi rispondere direttamente a questa email senza compilare il questionario.

+

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 }; diff --git a/scripts/check-contact-email.mjs b/scripts/check-contact-email.mjs index 3230809..c96cc7f 100644 --- a/scripts/check-contact-email.mjs +++ b/scripts/check-contact-email.mjs @@ -54,6 +54,9 @@ 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.');