Cómo Añadir un Formulario de Contacto en una Web Estática



Los generadores de webs estáticas como Gatsby.js son una buena opción para crear un blog personal o porfolio online. Tendrás todas las mejores prácticas de optimización integradas, además de todas las herramientas de desarrollo modernas que te hacen la vida más fácil como desarrollador.

Pero si necesitas añadir un formulario de contacto en tu web, de momento no hay ningún plugin que te permita hacerlo.

En este artículo te enseñaré como he añadido un formulario de contacto en mi página web personal creada con Gatsby.js y Netlify Forms.

¿Qué Opciones Tenemos Actualmente?

Una de las características principales que necesitaba en mi página web era tener un formulario de contacto, para que cualquier cliente potencial se pueda poner en contacto conmigo.

Pero pensé que crear un servidor back-end para ello, quizás sería demasiado en mi caso.

Entonces empecé a buscar formas de poder gestionar peticiones de formularios en una web estática.

Primero pensé en un usar un servicio externo como Formspree. Te ofrecen un plan gratuito para empezar, pero era un poco limitado, mientras que el plan de pago era demasiado caro para mis necesidades.

Así que finalmente me decidí por usar los formularios de Netlify ya que ahí es tenía hospedada la web también, así que me pareció la mejor opción.

Te ofrecen un buen plan para comenzar con hasta 100 peticiones de formularios al mes —después tendrás que pasar al plan de pago Pro.

Si quieres probar otras herramientas o servicios que van bien para las páginas web estáticas, este repositorio en Github es un buen recurso.

¿Qué Necesitas Para Empezar?

Si tienes tu web hospedada en Netlify, ya puedes empezar a usar su servicio de formularios.

Para hacerlo, solo tienes que añadir un atributo data-netlify="true" o netlify a tu formulario y un atributo name="nombre-del-formulario" —que es el que verás como nombre del formulario en tu panel de usuario de Netlify.

Este solo es el inicio de la etiqueta del mío, el formulario entero es un poco más largo —volveremos a esto más adelante.

<Form
  name="contact"  method="post"
  action={thanksURL}
  data-netlify="true"  data-netlify-honeypot="botfield"
  onSubmit={handleSubmit}
>

Elementos Adicionales Para los Inputs en Formularios JSX

Si estás usando tu formulario en un generador de webs estáticas como Gatsby.js, tendrás que añadir un input oculto con el valor name puesto como form-name y el value puesto como el nombre de tu formulario, en mi caso es contact.

Este paso es importante ya que, si no se indica, el formulario no aparecerá en el panel de usuario de Netlify.

<input type="hidden" name="form-name" aria-hidden="true" value="contact" />

Añadiendo Protección Antispam

Si necesitas activar el filtro de spam —y yo te recomiendo hacerlo, si no es posible que llegues al limite de 100 peticiones al mes bastante rápido— Netlify te ofrece 2 opciones.

La primera es usando un input oculto del tipo “honeypot” el cual solo los bots que intenten rellenar el formulario verán, y si Netlify detecta ese campo como completado, simplemente ignorará la presentación de ese formulario. En mi ejemplo, el mio se llama data-netlify-honeypot="botfield".

data-netlify-honeypot="botfield"

La segunda forma de activar el filtro Spam es usando reCAPTCHA 2. Si necesitas aprender como implementarlo, puedes leer este artículo de Netlify en el que explican como puedes hacerlo.

Así que vamos a empezar a crear el formulario.

Gestionando las Peticiones de Formularios

Por defecto, cuando un usuario te presenta un formulario, Netlify le redirige a una página genérica de agradecimiento. Si te vale esa opción, puedes seguir usando esa opción.

Pero podemos mejorar la experiencia de los usuarios usando una página personalizada o un componente React para cuando el usuario tiene activado el javascript en su navegador.

<StyledForm
  name="contact"
  method="post"
  action={thanksURL}  data-netlify="true"
  data-netlify-honeypot="botfield"
  onSubmit={handleSubmit}>

En mi caso he decidido implementar ambos casos.

1. El Usuario ha Desactivado Javascript en su Navegador

Cuando el usuario tiene javascript deshabilitado en su navegador, será redirigido a una página hecha por mi usando el atributo action={thanksURL} del formulario. De esta manera, puedo usar mi propia página de agradecimiento creada con mis estílos de marca.

Página de confirmación personalizada para los usuarios que tienen javascript deshabilitado
Página de confirmación personalizada para los usuarios que tienen javascript deshabilitado

2. El Usuario Tiene Activado Javascript en su Navegador

Cuando el usuario tiene javascript habilitado en su navegador, el formulario será gestionado por la función onSubmit={handleSubmit}. Teniendo en cuenta de que todo se realiza de forma asíncrona, puedo mejorar la experiencia de usuario y mostrar un indicador del estado de carga mientras la información es enviada a los servidores de Netlify.

Así que vamos a ver como podemos hacerlo.

async function handleSubmit(e) {
  e.preventDefault();

  setShowFormLoading(true);  setFormSubmitted(true);
  const form = e.target;
  fetch("/", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: encode({
      "form-name": form.getAttribute("name"),
      email,
      fullname: fullName,
      message,
      botfield: botField,
    }),
  })
    .then(() => {
      handleFormSent();    })
    .catch((error) => handleFormError(error));
}

En este ejemplo puedes ver que estoy usando la nueva API de Hooks de React, pero se podría conseguir lo mismo usando clases.

Lo primero que hace la función handleSubmit() es cambiar el valor de la variable de estado showFormLoading a true. Después, el indicador de carga spinner se muestre en pantalla.

Acto seguido, cambia el valor de la variable de estado formSubmited de false a true para que el componente <EmailLoading ... /> se muestre en pantalla (más adelante veremos como hacerlo).

Mostrando un Indicador y un Mensaje con el Estado de Carga

Cuando fetch() devuelve una respuesta, se ejecuta la función handleFormSent() que cambiará el valor de la variable de estado formLoading a false y el valor de la variable showFormSuccess a true, si no se ha devuelto ningún error.

function handleFormSent() {
  setShowFormLoading(false);
  setShowFormSuccess(true);
}

Si la respuesta devuelve un error, la función handleFormError() se ejecutará dentro del bloque catch(), el cual cambiará el valor de la variable de estado showFormError a true, para que muestre en pantalla un mensaje de error.

function handleFormSent() {
  let timer = setTimeout(() => {
    setShowFormLoading(false);
    setShowFormSuccess(true);

    clearTimeout(timer);
  }, 800);
}

function handleFormError(error) {
  setShowFormLoading(false);
  setShowFormError(true);
  setFormErrorRes(error);
}

Mientras la información del formulario se envía a los servidores de Netlify, el icóno reemplaza el texto del botón de enviar el formulario.

Entonces, cuando formSubmitted cambia a true, el componente <EmailLoading /> se mostrará en pantalla y mostrará el indicador del estado de carga junto con los mensajes de error o éxito, dependiendo de la respuesta del servidor.

{
  formSubmitted && (
    <EmailLoading
      showFormLoading={showFormLoading}
      showFormSuccess={showFormSuccess}
      showFormError={showFormError}
      formErrorRes={formErrorRes}
      locale={locale}
    />
  );
}

El componente <EmailLoading ... /> simplemente devuelve un indicador del estado de carga de la información cuando isLoading es equivalente a true, y un mensaje de error o éxito cuando recibe la respuesta del servidor.

{
  showFormLoading ? (
    <Spinner />
  ) : (
    <React.Fragment>
      {FORM_SUBMIT_STATUS.cta[locale]}
      <StyledIcon aria-hidden="true">
        <use xlinkHref="#correct" />
      </StyledIcon>
    </React.Fragment>
  );
}

Validando Formularios con CSS

Para validar la información introducida en el formulario he decido usar solo CSS.

Mi solución esta basada en usar los selectores de nivel 3 de CSS como &:valid, :not(), :placeholder-shown o :invalid.

Si te gustaría leer más sobre ellos, puedes leer este articulo de css-tricks en el que lo explican más.

Usando una combinación de estos selectores CSS, puedo tener hasta validación en tiempo real del formulario mientras el usuario esta introduciendo la información.

Por ejemplo, esto es una forma de comprobar si el input no esta activado o tiene focus, si se ha introducido información y si es válido.

/*  Input is:
 *  1. Valid
 *  2. NOT Empty
 *  3. NOT In Focus
 */
&:valid:not(:focus):not(:placeholder-shown) {
  color: ${theme.colors.success600};
  background-color: white !important;
  border: 2px solid ${theme.colors.success400};
  padding-right: ${rem(40)};

  & ~ span {
    display: block !important;
    ${optimizedSuccessSVGDataURI};
    background-size: ${rem(24)};
    background-repeat: no-repeat;
    background-position: 0px 0px;
  }
}

En este caso, también estoy mostrando un icono svg insertado usando un template literal de ES6 ${optimizedSuccessSVGDataURI}; como background-image que se activa con el selector css ~.

Aparte de la validación de la información del formulario, también deberías asegurarte de usar los atributos correctos para cada tipo de input.

En mi ejemplo, utilizo un input tipo email para el que he desactivado las mayúsculas automáticas con autoCapitalize y la autocorección con autoCorrect.

También he añadido el atributo autoComplete para que el navegador autocomplete la información de contacto del usuario.

<StyledInput
  type="email"
  value={email}
  name="email"
  placeholderType="email"
  title={INPUT_EMAIL_ERROR[locale]}
  pattern="^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$"
  autoCapitalize="off"
  autoCorrect="off"
  `autoComplete`="email"
  onChange={(e) => setEmail(e.target.value)}
  required
/>

El regular expression que utilizo en el atributo pattern sirve para comprobar si formato del email introducido es correcto, en concreto la parte que sigue después de la @ que contiene el dominio del email.

El valor guardado en title será el que es mostrado al usuario cuando el formato del email introducido no es válido, pero deberías tener en cuenta que cada navegador muestra el mensaje de forma distinta, así que pruébalo primero.

La variable INPUT_EMAIL_ERROR[locale] simplemente guarda una versión traducida del title ya que el blog lo he traducido al inglés y castellano.

Creando un Formulario de Contacto que Cumple con la RGPD

Ya que actualmente vivo en España, tengo que cumplir con la ley de protección de datos RGPD. Por tanto, faltan algunos campos en el formulario. El campo más importante que me falta es un checkbox que tiene que ser activado por el usuario antes de que me pueda presentar un formulario con su información personal.

Esto también es relativamente fácil hacer con Netlify ya que puedes enviar los valores de cada input y si el usuario ha dado su consentimiento de usar su información, o no, activando el checkbox.

Nota:

Antes de continuar leyendo, deberías tener en cuenta de que solo siguiendo estos pasos no estarás cumpliendo con la RGPD. Faltan muchos otros pasos y más información que no está detallada en este artículo.

Para asegurarte, deberías consultar con tu propio abogado como hice yo.

Desactivando el Botón de Enviar

De acuerdo con las leyes actuales de privacidad, los usuarios no pueden enviarte su información personal si no te han dado un consentimiento expreso previamente.

Por ello, tuve que añadír el checkbox —que no puede estar marcado por defecto— que controla si el formulario puede ser presentado o no.

Fue muy fácil de hacer ya que se puede usar una variable boolean que cambia de true a false cuando el checkbox es activado.

En caso contrario, el botón está desactivado hasta que el checkbox se activa.

{
  !formSubmitted && <SubmitButton disabled={!acceptsConsentCheckbox} />;
}

Depués deberías poder ver en tu panel de usuario de Netlify todas las peticiones de formularios que se han realizado, junto con el consentimiento del usuario.

El Formulario de Contacto con Todas las Opciones

function ContactForm({ locale }) {
  const [email, setEmail] = useState("");
  const [fullName, setFullName] = useState("");
  const [message, setMessage] = useState("");
  const [botField, setBotField] = useState("");
  const [acceptsConsentCheckbox, setAcceptsConsentCheckbox] = useState(false);
  const [checkboxValue, setCheckboxValue] = useState(CONSENT_VALUE[locale].no);
  const [formSubmitted, setFormSubmitted] = useState(false);
  const [showFormLoading, setShowFormLoading] = useState(false);
  const [showFormSuccess, setShowFormSuccess] = useState(false);
  const [showFormError, setShowFormError] = useState(false);
  const [formErrorRes, setFormErrorRes] = useState({});

  function encode(data) {
    return Object.keys(data)
      .map(
        (key) => encodeURIComponent(key) + "=" + encodeURIComponent(data[key]),
      )
      .join("&");
  }

  async function handleSubmit(e) {
    e.preventDefault();

    setShowFormLoading(true);
    setFormSubmitted(true);

    const form = e.target;
    fetch("/", {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: encode({
        "form-name": form.getAttribute("name"),
        email,
        fullname: fullName,
        message,
        datesent: dateSent,
        botfield: botField,
        acceptsconsentcheckbox: acceptsConsentCheckbox,
      }),
    })
      .then(() => {
        handleFormSent();
      })
      .catch((error) => handleFormError(error));
  }

  function handleConsentCheckbox(e) {
    setAcceptsConsentCheckbox(e.target.checked);

    if (e.target.checked) {
      setCheckboxValue(CONSENT_VALUE[locale].yes);
    } else {
      setCheckboxValue(CONSENT_VALUE[locale].no);
    }
  }

  function handleFormSent() {
    setShowFormLoading(false);
    setShowFormSuccess(true);
  }

  function handleFormError(error) {
    setShowFormLoading(false);
    setShowFormError(true);
    setFormErrorRes(error);
  }

  return (
    <FormContainer>
      <StyledForm
        name="contact"
        method="post"
        action="path-to-custom-thank-you-page"
        data-netlify="true"
        data-netlify-honeypot="botfield"
        onSubmit={handleSubmit}
      >
        <input
          type="hidden"
          name="form-name"
          aria-hidden="true"
          value="contact"
        />
        <input
          style={{ display: "none" }}
          aria-hidden="true"
          name="botfield"
          value={botField}
          onChange={(e) => setBotField(e.target.value)}
        />
        <StyledLabel labelType="full name">
          <StyledInput
            type="text"
            value={fullName}
            name="fullname"
            placeholderType="full name"
            autoCorrect="off"
            autoComplete="name"
            onChange={(e) => setFullName(e.target.value)}
            required
          />
          <InputStatusIcon aria-hidden="true" />
        </StyledLabel>
        <StyledLabel labelType="email">
          <StyledInput
            type="email"
            value={email}
            name="email"
            placeholderType="email"
            title={INPUT_EMAIL_ERROR[locale]}
            pattern="^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$"
            autoCapitalize="off"
            autoCorrect="off"
            autoComplete="email"
            onChange={(e) => setEmail(e.target.value)}
            required
          />
          <InputStatusIcon aria-hidden="true" />
        </StyledLabel>
        <StyledLabel labelType="message">
          <StyledTextArea
            rows="8"
            value={message}
            name="message"
            onChange={(e) => setMessage(e.target.value)}
            required
          />
          <InputTextAreaStatusIcon aria-hidden="true" />
        </StyledLabel>
        <PrivacyCheckbox
          type="checkbox"
          name="acceptsconsentcheckbox"
          value={checkboxValue}
          onChange={handleConsentCheckbox}
          locale={locale}
          required
        />

        {formSubmitted && (
          <EmailLoading
            showFormLoading={showFormLoading}
            showFormSuccess={showFormSuccess}
            showFormError={showFormError}
            formErrorRes={formErrorRes}
            locale={locale}
          />
        )}

        {!formSubmitted && <SubmitButton disabled={!acceptsConsentCheckbox} />}
      </StyledForm>
    </FormContainer>
  );
}

Conclusiones

Añadir un formulario de contacto a una página web estática creada con Gatsby.js fue bastante más fácil de lo que pensé.

Usando serivicios como Netlify, es relativamente fácil hacerlo y también puedes cumplir con la RGPD.

Espero que te haya ayudado a aprender como puedes crear tu propio formulario en tu web.

No dudes en dejar tu comentario más abajo si conoces algúna alternativa a usar Netlify. De momento estoy contento con ellos, pero siempre se puede mejorar 🤔.