Forms in Hugo - Backend stuff
A little guide on how to implement Forms in Hugo. Part 1
2564 Words | Reading Time: 11 Minutes, 39 Seconds
31-12-2023 07:31 PM UTC
Every self-respecting blog needs a Contact section and a fleeting hope that some audience/reader will use that to contact them; which may as well steer their life into an uncharted territory. It is believed that having a Contact section in blog actually helps if readers cares so much that they will spare a couple of minutes to appreciate or rectify mistakes. In addition to that, a blog with a contact page is thought to do well in terms of SEO; for which I have no studies to cite to.
That being said, and having accepted that the contact form in my blog is as useless as Anne Frank’s drumset, let’s see how I managed to put together a living mess which I call a contact form.
Before, we jump any deeper, we need to know that Hugo generates a static webpage by default, so once a content is served, the server has no fucking clue on what you are doing with it. Sure, you can bring out creativity out of your ass to make it a WordPress clone and add a buttload of dynamic elements. But, if that’s the objective, you are better off using WordPress. Where was I? Yes, Hugo makes static pages, Sever has no clue yada-yada-yada…
This staticness (I learned that today) creates a problem that I cannot add a contact form and have a backend to gather data published to it. My site is generously hosted by free version of GitLab, any hope that I will have a dynamic backend is out of the window (which is ironic because I work as a backend engineer in my day job).
This is how I ended up in this place. Are you ready for a ride?
Step 1. Getting backend ready
One thing I have learned about websites where front-end and backend doesn’t talk to one another, is to make backend first.
And for me that backend is….. 🥁 🥁 🥁 please…. It’s Telegram. Yup, that’s my backend. That is my NoSQL database.
Having a Telegram Bot
I have created a Telegram Bot. You can make your very own bot from BotFather. Keep the BOT_TOKEN
noted down. Next thing is to get your Chat ID. Yes, there is a chatid associated to your Telegram account. If you can read Telegram Bot’s logs, else follow this article.
P.S. You can read bot logs by going to this URL : https://api.telegram.org/bot<BOT_TOKEN>/getUpdates
That is only interaction needed from Telegram as of now.
Making a Cloudflare Worker
Keeping in line with my cheap-ass-ness; and due to the fact that my domain’s DNS is Cloudflare, I will be using Cloudflare Worker. It is the same as AWS Lambda or Azure Functions, as it provides a microservice platform to run a simple script which will listen to the form output and forward it to Telegram bot.
To create a Cloudflare Worker, go to : https://dash.cloudflare.com/<ACCOUNT_ID>/workers-and-pages/create/workers/new
to create a new worker application in your Cloudflare account. You could follow a tutorial, if you are lost in the maze of settings or have zero idea where to start. I found this article quite useful to help a newbie get started.
To get a feel on what the actual heck you’re doing, clone my repo to your local machine and we can follow along.
After cloning, you have to do two things
- Install node.js and npm. After that, you can install the dependencies.
- And you will have to create a file called
wrangler.toml
. Fill it up as follows:
|
|
We shall discuss index.js
and it will be evident why we are using the variables in wrangler.toml
|
|
CORS Headers
Let us see Lines 1 to 5 first, as this is most important, and I had spent most of my debugging time here.
|
|
CORS
or Cross Origin Request Sharing, is a 1“mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources. CORS also relies on a mechanism by which browsers make a “preflight” request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request.”
In other words, this allows me to transfer data from my Hugo site to the worker, and stopping browser from complaining. It is a HTTP Header based system. I am allowing POST
and OPTIONS
methods from ORIGIN_URI
(which we had set up in wrangler.toml) and accepting additional header of Content-Type
to be sent to Worker.
Now, that’s done, we can move to the next part, which will run when we receive a data packet from the internet.
|
|
We are running an asynchronous function, which will trigger every time there is a data packet. At first, we are getting the context, which we shall be denoted by request
. This request
contains everything for us to work with.
Then, we are checking who sent the data to the worker.
|
|
Next, I am checking for three things.
- Does this request arrive in
/submit
path - Does this request have a POST HTTP Header
- Did this request originate from ORIGIN_URI
|
|
If the above conditions are satisfiable, we are transferring control to process the data to UPDATE()
function, and giving browser the response that Worker has received the request (with 200 response code).
|
|
Now, if you have worked with sending form data from one computer to another over a network, have scratched your head on why it is not working and end up hating CORS even more; Yep, Browser sends a Preflight OPTIONS
request to check if it can really send the data or not.
Basic OPTIONS know-how 2
The OPTIONS
request mentioned in the introduction is a preflight request, which is part of the CORS (Cross-Origin Resource Sharing). CORS is a mechanism that provides configuration to configure access to shared resources. CORS applies when a webpage makes a request to another server other than its origin server, this could mean that either the domain, protocol, or port differs.
Using the request, the browser checks with the server whether the request is allowed. Only if the request is allowed, it’ll actually perform it.
We can perform a GET request without the need for a preflight request. However, the restrictions for POST requests are tighter. It means, for example, that we cannot send a JSON request without a preflight.
Thus, to handle OPTIONS, we need another check which invokes handleOptions()
function.
|
|
If none is true, which usually happens when someone decides to send useless requests to the Worker, we invoke the else part. This return a response of 405 with a hash of some text which I forgot. If you are able to decode that, let me know.
|
|
Function Explanation
Now, we should take a look into what each function does in this script. First and foremost, we should check the handleOptions
, as it is the first function to be invoked when sending a form response. Here, I’m doing a basic check if there is a valid Origin point, has proper method and headers. If all three conditions are satisfied, I’m sending the CORS headers, which we have discussed before. Else, I am sending allowed header that I’m accepting only OPTIONS
, which helps in reducing useless spam.
|
|
The next part is when someone decides to spam me with useless requests. It is important that it should be logged so that I could put them in blacklist. I’m creating an activity log of the sender’s request. I am logging the following information:
- IP Address of Request Origin
- Cloudflare Ray ID - it is an identifier given to every request that goes through Cloudflare
- User Agent
- The Closest Cloudflare Datacenter which got this request
- Country from which Request originated
- City from which Request originated
- Approximate geographical location, using latitude & longitude, and using Google Maps for showing me on the map
- ASN of the incoming request
- Postal code of the incoming request
- Timezone from where the request was generated
- ISP used for sending the request
- Any content in the request in JSON format
|
|
Next is UPDATE
function which handles all the legitimate request coming from the form. I’m first converting the request to JSON for easier parsing. From the JSON Object, I am extracting:
- Message itself
- User agent of the browser
- Default language set in browser
- System Date and Time with Timezone
- IP Address
- Edge location
- City, Country, Postal code, region, latitude, longitude of the request origination
- Timezone provided by Cloudflare
- ISP Name
|
|
The most important is TG
function, which actually pushes the contents from Worker to Telegram through our already established Bot. When this function is invoked, it has a BODY
for it to send out. Thus, it starts by fetching TELEGRAMTOKEN
& CHATID
from wrangler.toml and invokes a POST request to api.telegram.org
using CHATID
to determine where to send the payload to. If it encounters an error, it is automatically logged in Worker logs.
|
|
This article is long as it is. We will look into what should you do in Hugo itself to make contact form actually work. That part will be easy, I promise.