Modulo di contatto con fetch: conferme di invio e gestione degli errori

Crea un modulo di contatto HTML con fetch, conferme chiare ed errori utili. Copia un esempio completo che conserva il messaggio del visitatore se l’invio fallisce.

Di il team mailcontact

Un modulo di contatto deve chiarire una cosa: il messaggio è arrivato? Usa fetch per inviare i campi senza lasciare la pagina, mostrare lo stato d’invio e dichiarare il successo solo dopo la conferma dell’endpoint. In caso di errore, conserva il testo del visitatore e spiega come procedere.

Qui trovi un esempio completo HTML e JavaScript per l’endpoint modulo mailcontact. Non richiede framework frontend. Include etichette visibili, validazione del browser, honeypot, protezione dai clic duplicati e un messaggio di stato in linea. Sostituisci l’endpoint d’esempio con l’URL reale del progetto prima del test.

Perché una fetch risolta non basta

Una richiesta può raggiungere correttamente un server anche se l’invio viene rifiutato. Ad esempio, un’email non valida restituisce HTTP 400 e l’esaurimento della quota mensile HTTP 429. fetch non rifiuta la promise solo perché la risposta ha uno stato di errore HTTP. Controlla response.ok e il corpo della risposta previsto prima di azzerare il modulo. Il comportamento è documentato in Guida Fetch di MDN.

mailcontact restituisce {"ok":true} per le richieste accettate. Un honeypot compilato riceve intenzionalmente la stessa risposta ma viene scartato: controlla anche gli errori di compilazione automatica. Gli invii normali devono lasciare quel campo vuoto. Il riferimento dell’endpoint documenta il formato della risposta e i limiti di convalida.

Un modulo di contatto completo con messaggi di successo ed errore

Per una nuova integrazione, preferisci script MailContact ospitato o SDK personalizzato. Inserisce il controllo nascosto, completa la verifica automatica e conserva le bozze in caso di errore. Le integrazioni con script Free richiedono un link visibile “Processed by mailcontact.app”. L’esempio fetch grezzo sotto mostra le risposte HTTP; include il controllo obbligatorio, ma i messaggi sospetti senza supporto alla verifica restano in Spam per la revisione.

contact.html
<form id="contact-form" action="https://mailcontact.app/api/forms/your-form-id" method="POST">
  <fieldset>
    <legend>Contact us</legend>

    <label for="contact-name">Name (optional)</label>
    <input id="contact-name" name="name" autocomplete="name" maxlength="200" />

    <label for="contact-email">Your email</label>
    <input id="contact-email" type="email" name="email"
      autocomplete="email" maxlength="320" required />

    <label for="contact-message">Message</label>
    <textarea id="contact-message" name="message" maxlength="5000" required></textarea>

    <div class="contact-meta" aria-hidden="true">
      <label for="contact-website">Website</label>
      <input id="contact-website" name="company_website" type="text"
        tabindex="-1" autocomplete="off" />
    </div>

    <button type="submit">Send message</button>
  </fieldset>
  <p id="contact-status" role="status" aria-live="polite" aria-atomic="true"></p>
</form>

<style>
  .contact-meta {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
  }
</style>

<!-- Keep the script after the form so its elements exist. -->
<script>
  const form = document.querySelector("#contact-form");
  const fields = form.querySelector("fieldset");
  const button = form.querySelector('button[type="submit"]');
  const status = document.querySelector("#contact-status");
  let sending = false;

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    if (sending || !form.reportValidity()) return;

    // Collect values before disabling the fieldset.
    const body = new FormData(form);
    if (!String(body.get("message") || "").trim()) {
      status.textContent = "Please write a message before sending.";
      form.elements.namedItem("message").focus();
      return;
    }

    sending = true;
    fields.disabled = true;
    form.setAttribute("aria-busy", "true");
    button.textContent = "Sending…";
    status.textContent = "Sending your message…";
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 15000);

    try {
      const response = await fetch(form.action, {
        method: "POST",
        body,
        signal: controller.signal,
      });
      const data = await response.json().catch(() => null);

      if (!response.ok || data?.ok !== true) {
        let message = "Your message could not be sent. Your text is still here.";
        if (response.status === 400) {
          message = "Check your email and message length, then try again.";
        } else if (response.status === 429) {
          message = data?.code === "MONTHLY_QUOTA_EXCEEDED"
            ? "This form has reached its monthly limit. Please use another contact method on this site."
            : "Too many requests. Please wait before trying again.";
        } else if (response.status === 404) {
          message = "This contact form is unavailable. Please use another contact method on this site.";
        }
        status.textContent = message;
        return;
      }

      form.reset();
      status.textContent = "Thanks! Your message has been received.";
    } catch {
      status.textContent = "We could not confirm delivery. Your text is still here. Check your connection before trying again.";
    } finally {
      clearTimeout(timeout);
      sending = false;
      fields.disabled = false;
      form.removeAttribute("aria-busy");
      button.textContent = "Send message";
    }
  });
</script>

Mantieni gli stili abituali del sito attorno al markup. La piccola regola CSS serve solo all’honeypot, non impone un layout. Senza JavaScript il modulo invia nativamente, ma l’endpoint attuale mostra JSON anziché reindirizzare a una pagina di ringraziamento.

Cosa succede durante l’invio del messaggio?

Il gestore raccoglie i campi prima di disabilitare il fieldset. I controlli disabilitati non entrano in FormData: invertire i passaggi perderebbe il payload. Disabilitarli impedisce anche modifiche alla bozza durante la richiesta che poi verrebbero cancellate da un reset riuscito. Il separato sending protezione che impedisce invii sovrapposti.

L’area di stato esiste già nella pagina e aggiorna il testo quando lo stato cambia. Fornisce riscontro senza finestre di avviso né solo cambi di colore. Il finally ripristina il modulo dopo un successo, un errore del server o di rete, evitando che il pulsante resti disabilitato per sempre.

Conserva il messaggio quando la consegna è incerta

Il timeout di 15 secondi evita attese infinite nel browser. Non annulla un messaggio già accettato dal server. La connessione può fallire dopo il salvataggio ma prima della conferma al browser. Per questo l’errore di rete dice che la consegna non è confermata, senza affermare che nulla è stato inviato.

L’esempio non ritenta automaticamente richieste incerte: un altro tentativo potrebbe duplicare il messaggio. Conserva la bozza perché il visitatore decida. Per garantire assenza di duplicati tra tentativi serve un meccanismo esplicito di idempotenza sul servizio ricevente; disabilitare il pulsante non basta.

Gestisci separatamente i limiti temporanei e le quote mensili

Una risposta 429 può indicare un limite temporaneo o la quota mensile esaurita. Controlla il JSON code: MONTHLY_QUOTA_EXCEEDED identifica il secondo caso. Chiedere al visitatore di riprovare dopo pochi secondi è inutile se il modulo ha raggiunto la quota mensile.

Per i limiti temporanei, l’endpoint fornisce un Retry-After . Puoi usarla per un conto alla rovescia se serve all’interfaccia. Se la quota è esaurita, offri un vero contatto alternativo sul sito. Modulo ed email di ricezione di un progetto Pro condividono la quota dell’account: quell’email non è un’alternativa a quota esaurita. Controlla l’utilizzo e la limiti del piano prima di un lancio importante.

Controlla il modulo prima di pubblicare

  • Invio riuscito: compare la conferma, il modulo si svuota e il messaggio è nella casella del progetto.
  • Dati non validi: un’email non valida o un messaggio vuoto mostrano un errore utile senza cancellare gli altri campi.
  • Browser offline: la bozza resta, la consegna è indicata come incerta e il pulsante torna utilizzabile.
  • Clic ripetuti: mentre lo stato di invio è attivo, viene eseguita una sola richiesta.
  • Consegna delle notifiche: una destinazione Slack o Discord configurata riceve un avviso dopo un messaggio accettato.

Non mostrare il successo solo perché un’animazione termina o viene premuto un pulsante. Collegalo alla risposta dell’endpoint, consenti di recuperare dagli errori e verifica tutto il percorso fino alla casella. Per la ricezione segui la Guida alle notifiche Slack o il Guida Discord.

Collega il modulo a una casella di progetto.

Mantieni il design del modulo e lascia ricevere i messaggi a mailcontact. Crea un account per l’accesso al lancio.

Crea un modulo gratis

Non serve la carta di credito. Free include un progetto e 50 messaggi dal modulo al mese.