Implementing a Contact Form on a Static Site (GAS Integration)
Summarized how to implement a contact form without a server on static sites like GitHub Pages (using Google Apps Script).
Hello! I'm PanKUN.
This portfolio site is a static website hosted on GitHub Pages.
And to implement a contact form, I need a mechanism to send emails. I have been implementing the API on a resident server using Playit.gg for tunnel communication, but I have been in trouble because I haven't been able to create a tunnel for several weeks... (I contacted the official support though...)
So this time, I implemented a completely free email sending function using Google Apps Script (GAS) as a simple backend API.
Why GAS (Google Apps Script)?
There are several ways to implement forms on static sites.
- Use external services like Formspree
- Easy, but there may be limits on the free tier, or redirects to the service page after sending.
- Embed Google Forms
- Easiest, but the design cannot be unified with the site.
- AWS Lambda + API Gateway
- Flexible, but construction is a bit troublesome and may cost money.
- GAS (Google Apps Script)
- Free to use.
- Can write in JavaScript.
- Can easily send emails using
GmailApp. - Can be published as a Web API.
This time, I adopted GAS for reasons such as "I want to control it myself", "I don't want to spend money", and "I want to complete it with JavaScript".
Backend Implementation (GAS)
First is the GAS code that becomes the backend. Write a process to receive data POSTed from the form and send it to your email address.
Points
- Receive POST requests with the
doPost(e)function. - Parse
e.postData.contentsto retrieve data. - Send email with
GmailApp.sendEmail. - Need to return appropriate headers as CORS measures.
The actual code (excerpt) looks like this.
function doPost(e) {
// CORS Headers
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST",
"Access-Control-Allow-Headers": "Content-Type"
};
try {
// Parse Data
let data = {};
if (e.postData && e.postData.contents) {
data = JSON.parse(e.postData.contents);
} else {
data = e.parameter;
}
// Send Email
const recipient = "my-email@example.com"; // Your email address
const subject = "Contact Received";
const body = `Name: ${data.name}\nContent: ${data.message}`;
GmailApp.sendEmail(recipient, subject, body, {
replyTo: data.email
});
// Success Response
const output = ContentService.createTextOutput(JSON.stringify({ result: "success" }));
output.setMimeType(ContentService.MimeType.JSON);
return output;
} catch (error) {
// Error Response
const output = ContentService.createTextOutput(JSON.stringify({ result: "error" }));
output.setMimeType(ContentService.MimeType.JSON);
return output;
}
}By deploying this as a "Web App" and setting the access permission to "Anyone", an API URL that can be hit from the outside is issued.
Frontend Implementation (JavaScript)
Next is the processing on the frontend side that hits this API.
Perform asynchronous transmission using the fetch API.
Tips to avoid CORS errors
If you POST to GAS with application/json, a preflight request (OPTIONS) for CORS (Cross-Origin Resource Sharing) occurs, and GAS may fail to reply successfully, resulting in an error.
To avoid this, I adopted a method of daringly sending data as text/plain.
const GAS_API_URL = "https://script.google.com/macros/s/xxxx/exec";
form.addEventListener("submit", async (e) => {
e.preventDefault();
// Collect Input Data
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
try {
// The trick is to send as text/plain
const response = await fetch(GAS_API_URL, {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "text/plain;charset=utf-8",
},
});
const resJson = await response.json();
if (resJson.result === "success") {
alert("Sent!");
form.reset();
}
} catch (err) {
alert("Failed to send");
}
});On the GAS side, by receiving with JSON.parse(e.postData.contents), it can be handled as JSON without problems.
Design Adjustments
I adjusted not only the function but also the appearance to match the tone and manner of the site.
- Badge display for required items
- Loading animation for the send button
- Ease of input on smartphones (utilizing
input type="email"andtype="tel")
Especially for the send button, I made it display a spinner with CSS animation so that it is visually clear that it is "sending..." after pressing it. It also has the effect of preventing users from hitting it repeatedly.
Summary
Even in a serverless environment, I was able to implement a full-fledged contact form by combining GAS.
- GAS is excellent as a free and easy backend.
- CORS problems can be avoided by devising the
Content-Typeoffetch.
Now I am ready to accept work consultations directly from my portfolio site!
Loading comments...