Forms in Hugo - Frontend stuff


That day is upon us folks. It’s finally time to discuss on the Frontend part for creating forms in your Hugo Webpage.

Since we are creating a new HTML element in Hugo, we have to use something called shortcode.

You must be asking…

What is a shortcode?

Keeping inline with my copy-paste nature, here’s a brief description of the same

Hugo loves Markdown because of its simple content format, but there are times when Markdown falls short. Often, content authors are forced to add raw HTML (e.g., video iframe’s) to Markdown content. We think this contradicts the beautiful simplicity of Markdown’s syntax.

Hugo created shortcodes to circumvent these limitations.

A shortcode is a simple snippet inside a content file that Hugo will render using a predefined template. Note that shortcodes will not work in template files. If you need the type of drop-in functionality that shortcodes provide but in a template, you most likely want a partial template instead.

In addition to cleaner Markdown, shortcodes can be updated any time to reflect new classes, techniques, or standards. At the point of site generation, Hugo shortcodes will easily merge in your changes. You avoid a possibly complicated search and replace operation.

Making a shortcode for my form

If you are Certified Lazy™, you can copy below code into your layouts/shortcodes/contact.html. I am skipping detailing what each line does, as it’s pretty self-explanatory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<form id="contact-form" name="contact-form" class="contact-form formdisable" onsubmit="return false">
  <div class="message">
    <textarea id="message" class="form-input" name="message" required="required" aria-describedby="messageHelpBlock">Hi BLZR,&#13;&#10;</textarea> 
    <span id="messageHelpBlock">Enter your message/query (Required)</span> 
  </div>
  <div class="captcha">
    <canvas id="captchaCanvas" height=80>Don't be a dork, Enable JavaScript</canvas>
    <button id="refresh" name="refresh" onclick="generateCaptcha()">&#128472;</button>
    <input id="captchaText" class="form-input" name="CAPTCHA" placeholder="Enter text above" maxlength="7" type="text" required="required" aria-describedby="CAPTCHAHelpBlock"> 
    <span id="CAPTCHAHelpBlock">CAPTCHA Test (Required)</span>
  </div>
  <div class="submit">
    <button id="submit" name="submit" onclick="sendMessage()">Send</button>
  </div>
</form>

CSS-ify that shit

Pretty simple Sass code to beautify my form. I am using a theme for my site, from which I am importing _predefined.scss. You can ignore that if you’re not using the same.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
@import "../scss/_predefined.scss";
$textColor: #0062ff;

.admonition {
    display: none;
}
.monospace{
    font-family: monospace;
}
.prevent{
    cursor: crosshair;
    user-select: none;
    -webkit-user-select: none;
}
.formdisable{
    pointer-events:none;
}
.contact-form {
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: 1fr 1fr auto;
    grid-auto-rows: 1fr;
    gap: 0px 0px;
    grid-auto-flow: row;
    grid-template-areas:
        "message"
        "captcha"
        "submit";
    -webkit-box-shadow: 0px 0px 50px -1px $dark-grey;
    -moz-box-shadow: 0px 0px 50px -1px $dark-grey;
    box-shadow: 0px 0px 50px -1px $dark-grey;
    -webkit-border-radius: 10px;
    -moz-border-radius: 10px;
    border-radius: 10px;

    & * {
        font-family: monospace, monospace;
        font-size: large;
    }

    & input,
    textarea {
        padding: 5px;
        -webkit-box-sizing: border-box;
        -moz-box-sizing: border-box;
        -o-box-sizing: border-box;
        -ms-box-sizing: border-box;
        box-sizing: border-box;
        outline: 0;
        border: none;
        -webkit-border-radius: 5px;
        -moz-border-radius: 5px;
        border-radius: 5px;
        color: $textColor;
        background: $dark-grey;
    }

    & div {
        & span {
            display: none;
        }
    }
}

.message {
    grid-area: message;
    padding: 10px 20px 0 20px;

    & textarea {
        width: 100%;
        resize: vertical;
        min-height: 80px;
        max-height: 400px;
        font-weight: bold;
    }
}

.captcha {
    grid-area: captcha;
    padding: 0 20px;

    >input {
        width: 100%;
    }

    >#captchaCanvas {
        width: 300px;
        height: 80px;
        border: none;
        border-radius: 15px;
        background-color: $midnightblue;
    }

    >#refresh {
        outline: 0;
        border: none;
        cursor: pointer;
        background: transparent;
        margin: 0;
        position: relative;
        top: -25%;
    }
}

.submit {
    text-align: center;
    padding: 20px 0;

    >button {
        width: 95%;
        outline: 0;
        border: 2px solid $theme;
        height: 50px;
        color: $textColor;
        cursor: pointer;
        border-radius: 5px;
        background: linear-gradient(to right, $highlight-grey, $highlight-grey);
        background-repeat: no-repeat;
        background-size: 0 100%;
        transition: background-size 1s 0s;

        &:hover:not([disabled]),
        &:focus:not([disabled]) {
            height: 50px;
            border: 2px solid $textColor;
            background-size: 100% 100%;
            color: $text;
        }

        &:disabled {
            background: $dark-grey;
            color: white;
        }
    }
}

I’m using monospaced font because I am simply not ready to deal with weirdness of fonts and, in my very subjective opinion, it looks cool.

Finally JavaScript

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
const contactForm = document.getElementById("contact-form");
const captchaText = document.getElementById("captchaText");
const messageText = document.getElementById("message");
const submit = document.getElementById("submit");
const admonition = document.getElementsByClassName("admonition")[0];
const errorMessage = document.getElementsByClassName("admonition-content")[0];
const alphaNums = "ABCDEFGHKLMNPQRSTUVWXYZabcdefghkmnpqrstuvwxyz23456789!@#$%^&({[<>]})?";
var generatedCaptcha = '';
var sent = false;
var url = "https://worker_link";
var request = new XMLHttpRequest();
contactForm.classList.remove("formdisable");
function randomColor() {
	let r = Math.floor(Math.random() * 256);
	let g = Math.floor(Math.random() * 256);
	let b = Math.floor(Math.random() * 256);
	return 'rgb(' + r + ',' + g + ',' + b + ')';
}

function generateCaptcha() {
	if (!sent) {
		const captchaCanvas = document.getElementById("captchaCanvas");
		const ctx = captchaCanvas.getContext("2d");
		generatedCaptcha = '';
		captchaText.value = '';
		ctx.font = "25px Roboto";
		ctx.letterSpacing = "20px";
		ctx.clearRect(0, 0, captchaCanvas.width, captchaCanvas.height);
		ctx.beginPath();
		for (let i = 1; i <= 7; i++) {
			var cText = alphaNums.charAt(Math.random() * alphaNums.length);
			generatedCaptcha += cText;
			let sDeg = (Math.random() * 30 * Math.PI) / 180;
			let x = 10 + i * 20;
			let y = 20 + Math.random() * 8;
			ctx.translate(x, y);
			ctx.rotate(sDeg);
			ctx.fillStyle = randomColor();
			ctx.fillText(cText, captchaCanvas.width / 6, captchaCanvas.height / 10);
			ctx.rotate(-sDeg);
			ctx.translate(-x, -y);
		}
		for (let i = 0; i <= 6; i++) {
			ctx.strokeStyle = randomColor();
			ctx.beginPath();
			ctx.moveTo(
				Math.random() * captchaCanvas.width,
				Math.random() * captchaCanvas.height
			);
			ctx.lineTo(
				Math.random() * captchaCanvas.height,
				Math.random() * captchaCanvas.height
			);
			ctx.stroke();
		}
		for (let i = 0; i < 50; i++) {
			ctx.strokeStyle = randomColor();
			ctx.beginPath();
			let x = Math.random() * captchaCanvas.width;
			let y = Math.random() * captchaCanvas.height;
			ctx.moveTo(x, y);
			ctx.lineTo(x + 1, y + 1);
			ctx.stroke();
		}
	}
}
generateCaptcha();

const sendMessage = () => {
	admonition.style.display = "none";
	var message = messageText.value;
	var enteredCaptcha = document.getElementById("captchaText").value;
	if (!sent) {
		if (enteredCaptcha === generatedCaptcha) {
			if (message.length >= 20) {
				sent = true;
				var today = new Date();
				var dateTime = (today.getDate() + '-' + (today.getMonth() + 1) + '-' + today.getFullYear()) + ' ' + (today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds());

				captchaText.disabled = true;
				messageText.disabled = true;
				submit.disabled = true;
				var messageJson = JSON.stringify({
					MSG: `${message}`,
					UA: `${navigator.userAgent}`,
					LANG: `${navigator.language}`,
					DT: `${dateTime}`,
					ZONE: `${Intl.DateTimeFormat().resolvedOptions().timeZone}`
				});
				request.open("POST", url, true);
				request.setRequestHeader("Content-Type", "application/json");
				request.onreadystatechange = function () {
					if (request.readyState === 4 && request.status === 200) {
						console.log(JSON.parse(request.response));
					}
				};
				request.send(messageJson);
				submit.innerHTML = "Sent &#10003;";
			} else {
				admonition.style.display = "block";
				errorMessage.innerHTML = "Message length must be greater than 20 characters";
			}
		} else {
			admonition.style.display = "block";
			errorMessage.innerHTML = "Invalid or wrong Verification Code";
		}
	}
}

This is something I think I need to explain. In the first section, I am taking few variables and one as alphaNums to store alphanumeric string, which shall be used later. Now we also need url, where the message will be sent. In previous article, we have seen how to make backend for the forms. We need that URL for the backend here.

12
contactForm.classList.remove("formdisable");

Here, if JS is loaded, contact Form will be disabled. It is a simple Quality of Life thing for me.

13
14
15
16
17
18
function randomColor() {
	let r = Math.floor(Math.random() * 256);
	let g = Math.floor(Math.random() * 256);
	let b = Math.floor(Math.random() * 256);
	return 'rgb(' + r + ',' + g + ',' + b + ')';
}

This creates a random color in RGB for Captcha, to be used later.

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
function generateCaptcha() {
	if (!sent) {
		const captchaCanvas = document.getElementById("captchaCanvas");
		const ctx = captchaCanvas.getContext("2d");
		generatedCaptcha = '';
		captchaText.value = '';
		ctx.font = "25px Roboto";
		ctx.letterSpacing = "20px";
		ctx.clearRect(0, 0, captchaCanvas.width, captchaCanvas.height);
		ctx.beginPath();
		for (let i = 1; i <= 7; i++) {
			var cText = alphaNums.charAt(Math.random() * alphaNums.length);
			generatedCaptcha += cText;
			let sDeg = (Math.random() * 30 * Math.PI) / 180;
			let x = 10 + i * 20;
			let y = 20 + Math.random() * 8;
			ctx.translate(x, y);
			ctx.rotate(sDeg);
			ctx.fillStyle = randomColor();
			ctx.fillText(cText, captchaCanvas.width / 6, captchaCanvas.height / 10);
			ctx.rotate(-sDeg);
			ctx.translate(-x, -y);
		}
		for (let i = 0; i <= 6; i++) {
			ctx.strokeStyle = randomColor();
			ctx.beginPath();
			ctx.moveTo(
				Math.random() * captchaCanvas.width,
				Math.random() * captchaCanvas.height
			);
			ctx.lineTo(
				Math.random() * captchaCanvas.height,
				Math.random() * captchaCanvas.height
			);
			ctx.stroke();
		}
		for (let i = 0; i < 50; i++) {
			ctx.strokeStyle = randomColor();
			ctx.beginPath();
			let x = Math.random() * captchaCanvas.width;
			let y = Math.random() * captchaCanvas.height;
			ctx.moveTo(x, y);
			ctx.lineTo(x + 1, y + 1);
			ctx.stroke();
		}
	}
}

Here, I am creating a Canvas element (which is already described in HTML).

  • getContext() method returns a drawing context on the canvas.
  • clearRect() method of the Canvas 2D API erases the pixels in a rectangular area by setting them to transparent black.
  • beginPath() method of the Canvas 2D API starts a new path by emptying the list of sub-paths. Call this method when you want to create a new path.

Within the canvas, I am using 3 for loops.

30
31
32
33
34
35
36
37
38
39
40
41
42
for (let i = 1; i <= 7; i++) {
	var cText = alphaNums.charAt(Math.random() * alphaNums.length);
	generatedCaptcha += cText;
	let sDeg = (Math.random() * 30 * Math.PI) / 180;
	let x = 10 + i * 20;
	let y = 20 + Math.random() * 8;
	ctx.translate(x, y);
	ctx.rotate(sDeg);
	ctx.fillStyle = randomColor();
	ctx.fillText(cText, captchaCanvas.width / 6, captchaCanvas.height / 10);
	ctx.rotate(-sDeg);
	ctx.translate(-x, -y);
}

This For loop is used to create 7 alphanumeric string from alphaNums. These characters are placed in random height and random orientation. Last 2 lines are for resetting height and orientation after each character.

43
44
45
46
47
48
49
50
51
52
53
54
55
for (let i = 0; i <= 6; i++) {
	ctx.strokeStyle = randomColor();
	ctx.beginPath();
	ctx.moveTo(
		Math.random() * captchaCanvas.width,
		Math.random() * captchaCanvas.height
	);
	ctx.lineTo(
		Math.random() * captchaCanvas.height,
		Math.random() * captchaCanvas.height
	);
	ctx.stroke();
}

Next for loop is to create 6 random lines in the canvas, just to make sure ChatGPT can’t see these characters clearly.

56
57
58
59
60
61
62
63
64
for (let i = 0; i < 50; i++) {
	ctx.strokeStyle = randomColor();
	ctx.beginPath();
	let x = Math.random() * captchaCanvas.width;
	let y = Math.random() * captchaCanvas.height;
	ctx.moveTo(x, y);
	ctx.lineTo(x + 1, y + 1);
	ctx.stroke();
}

This for loop is used to create speckles thereby attempting to increase complexity further.

77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
var today = new Date();
var dateTime = (today.getDate() + '-' + (today.getMonth() + 1) + '-' + today.getFullYear()) + ' ' + (today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds());
captchaText.disabled = true;
messageText.disabled = true;
submit.disabled = true;
var messageJson = JSON.stringify({
	MSG: `${message}`,
	UA: `${navigator.userAgent}`,
	LANG: `${navigator.language}`,
	DT: `${dateTime}`,
	ZONE: `${Intl.DateTimeFormat().resolvedOptions().timeZone}`
});
request.open("POST", url, true);
request.setRequestHeader("Content-Type", "application/json");
request.onreadystatechange = function () {
	if (request.readyState === 4 && request.status === 200) {
		console.log(JSON.parse(request.response));
	}
};
request.send(messageJson);
submit.innerHTML = "Sent &#10003;";

If captcha is verified and message length is greater than 20 characters, we take current date from system, disable all the input fields and create a JSON document for the message, which also includes browsers'

  • userAgent
  • language
  • timeZone

Just to verify that a human has indeed sent the message. I could have used browser fingerprinting, but it is a 3rd party package and I don’t want to use your browser for advertising.

After that a POST request is sent to Cloudflare worker, and to make this stuff transparent, submit button is updated.

Thus, you have a proper contact form in your Hugo Page. Use it to your heart’s content.


I know this is a bit rushed and could not provide my usual humor. There is much going on in my life. This is the reason why I could not update the blog for such a long time.