Formulario de Contacto para Sitio Estático

Dado que este blog es un sitio estático, tuve que buscar alternativas para poder enviar un formulario de contacto sin la necesidad de tener backend o un servidor dedicado para eso.

búsqueda de google

Obviamente al hacer la búsqueda en Google me encuentro con que la opción más usada y famosa es Formspree.

¿Qué es Formspree?

Connect your form to our endpoint and we’ll email you the submissions. No PHP, Javascript or sign up required — perfect for static sites!

<form action="https://formspree.io/[email protected]" method="POST">
  <input type="text" name="name">  
  <input type="email" name="_replyto">
  <input type="submit" value="Send"> 
</form>

Así de sencillo, ya tendría funcionando un formulario de contacto en mi sitio estático, todo bien... pero no.

Resulta que al enviar el formulario con Formspree, nos encontramos con esta maravilla.

captcha

Un Captcha !!!!

Definitivamente descartado, eliminar el anti-spam no es posible en el plan gratuito de formspree, entonces, ¿qué otra alternativa existe?

Bueno, respuestas a esta pregunta hay muchas, servicios que hacen exactamente lo mismo como:

Pero buscando alternativas encontré algo que me llamó mucho la atención; es posible utilizar Google Docs para simular un backend y guardar el registro de mensajes en el formulario de contacto en una planilla, para luego enviarlo a mi correo.

¿Overkill? Totalmente, pero interesante as fuck.

Así que decidí implementarlo en el sitio siguiendo las instrucciones del siguiente repositorio -> https://github.com/dwyl/learn-to-send-email-via-google-script-html-no-server

  1. Primero hay que copiar la siguiente planilla en nuestra cuenta de Google Drive.
  2. Abrimos el Editor de Secuencias de Comando haciendo click en Herramientas > Editor de Secuencias de Comando.
  3. Copiamos el siguiente script y editamos la variable TO_ADDRESS.
var TO_ADDRESS = "[email protected]";

function doPost(e) {
    try {
        Logger.log(e);
        MailApp.sendEmail(TO_ADDRESS, "Formulario de Contacto Enviado", JSON.stringify(e.parameters));
        return ContentService.createTextOutput(
                JSON.stringify({
                    "result": "success",
                    "data": JSON.stringify(e.parameters)
                }))
            .setMimeType(ContentService.MimeType.JSON);
    } catch (error) {
        Logger.log(error);
        return ContentService
            .createTextOutput(JSON.stringify({
                "result": "error",
                "error": e
            }))
            .setMimeType(ContentService.MimeType.JSON);
    }
}
  1. Guardar el script como una nueva versión, para hacerlo, ir a Archivo > Administrar Versiones, ahí escribes un título y guardas la nueva versión.
  2. Publicar el script, para ello, vamos a Publicar > Implementar como aplicación web..., siempre fijarse en que la Versión del Proyecto sea la última.
  3. Apretar Actualizar y aceptar los correspondientes permisos, el resultado será una URI, guardar para el próximo paso.

HTML

Ahora que tenemos el formulario listo para recibir los mensajes, tenemos que implementarlo a un formulario html que debe cumplir con las siguientes condiciones:

  • La clase del <form>debe ser gform.
  • El atributo name de cada <input> debe ser el mismo que su correspondiente columna en la planilla de Google.
  • El action del formulario debe ser la URI obtenida anteriormente.

ejemplo estatico

Con eso tenemos lista la integración, al probarlo obtuve algo como esto:

{"result":"success","data":"{\"correo\":[\"[email protected]\"],\"mensaje\":[\"Hola, este es un mensaje de prueba.\"],\"telefono\":[\"973362531\"],\"nombre\":[\"Juan Latorre\"]}"}

ejemplo mail recibido

Perfecto, ahora solo falta mejorarlo un poco, no quiero que la persona que me contacta vea ese gran mensaje después de enviar el formulario.

Para eso vamos a modificar un poco el script (primera parte, paso 2).

Primero agrego la función formatMailBody() para mejorar un poco el look del correo recibido.

function formatMailBody(obj, order) {
    var result = "";
    if (!order) {
        order = Object.keys(obj);
    }
    for (var idx in order) {
        var key = order[idx];
        result += "<h4 style='text-transform: capitalize; margin-bottom: 0'>" + key + "</h4><div>" + sanitizeInput(obj[key]) + "</div>";
    }
    return result;
}

Pero esta función utiliza a su vez, otra función que limpia o sanitiza el input ingresado por la persona que quiere contartarme; así que agregaremos la función sanitizeInputtambién.

function sanitizeInput(rawInput) {
    var placeholder = HtmlService.createHtmlOutput(" ");
    placeholder.appendUntrusted(rawInput);
    return placeholder.getContent();
}

Y por supuesto teniendo dos nuevas funciones, hay que agregarlas a nuestro loop principal, que ahora queda así:

function doPost(e) {
    try {
        Logger.log(e);
        record_data(e);
        var mailData = e.parameters;
        var orderParameter = e.parameters.formDataNameOrder;
        var dataOrder;
        if (orderParameter) {
            dataOrder = JSON.parse(orderParameter);
        }
        var sendEmailTo = (typeof TO_ADDRESS !== "undefined") ? TO_ADDRESS : mailData.formGoogleSendEmail;
        if (sendEmailTo) {
            MailApp.sendEmail({
                to: String(sendEmailTo),
                subject: "Contact form submitted",
                htmlBody: formatMailBody(mailData, dataOrder)
            });
        }
        return ContentService.createTextOutput(
                JSON.stringify({
                    "result": "success",
                    "data": JSON.stringify(e.parameters)
                }))
            .setMimeType(ContentService.MimeType.JSON);
    } catch (error) {
        Logger.log(error);
        return ContentService
            .createTextOutput(JSON.stringify({
                "result": "error",
                "error": error
            }))
            .setMimeType(ContentService.MimeType.JSON);
    }
}

Sin embargo, nos falta la función para guardar los datos en la planilla creada al comienzo, por lo que la agregamos también.

function record_data(e) {
    var lock = LockService.getDocumentLock();
    lock.waitLock(30000);

    try {
        Logger.log(JSON.stringify(e));
        var doc = SpreadsheetApp.getActiveSpreadsheet();
        var sheetName = e.parameters.formGoogleSheetName || "responses";
        var sheet = doc.getSheetByName(sheetName);

        var oldHeader = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
        var newHeader = oldHeader.slice();
        var fieldsFromForm = getDataColumns(e.parameters);
        var row = [new Date()];

        // loop through the header columns
        for (var i = 1; i < oldHeader.length; i++) {
            var field = oldHeader[i];
            var output = getFieldFromData(field, e.parameters);
            row.push(output);

            var formIndex = fieldsFromForm.indexOf(field);
            if (formIndex > -1) {
                fieldsFromForm.splice(formIndex, 1);
            }
        }

        for (var i = 0; i < fieldsFromForm.length; i++) {
            var field = fieldsFromForm[i];
            var output = getFieldFromData(field, e.parameters);
            row.push(output);
            newHeader.push(field);
        }

        var nextRow = sheet.getLastRow() + 1;
        sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);

        if (newHeader.length > oldHeader.length) {
            sheet.getRange(1, 1, 1, newHeader.length).setValues([newHeader]);
        }
    } catch (error) {
        Logger.log(error);
    } finally {
        lock.releaseLock();
        return;
    }

}

function getDataColumns(data) {
    return Object.keys(data).filter(function(column) {
        return !(column === 'formDataNameOrder' || column === 'formGoogleSheetName' || column === 'formGoogleSendEmail' || column === 'honeypot');
    });
}

function getFieldFromData(field, data) {
    var values = data[field] || '';
    var output = values.join ? values.join(', ') : values;
    return output;
}

y voilá, tenemos un formulario de contacto estático completamente de una forma completamente innecesaria.