
Recaptcha integration with Astro components framework
by Rebeca Murillo • 21 December 2023
Introduction
In order to enforce security on the contact form of my website, I decided to integrate Google’s reCaptcha V3, with the server side token verification. After setting up the scripts, I kept encountering a CORS issue when loading the reCaptcha script. In this guide I will highlight the keypoints to ensure a smooth integration to avoid the CORS issue.

This integration of reCaptcha invokes a challenge and validates the user behaviour in the background, providing a smoother user experience.
Step by step guide
The requirements for this guide :
- Nodejs and Astro framework basic knowledge
1. Create Google reCaptcha site
In order to create a Google reCaptcha site configuration, create a new site on the Google reCaptcha admin console.
- Go to Google reCaptcha admin
- Create a new site
- Under type, Select Score based (v3)
- Domains, enter your website domain. For example: “mywebsite.com”
- Retrieve the website key, for your front-end website. Identified as MY_reCAPTCHA_SITE_ID in this tutorial
- Retrieve the secret key, for your backend token validation. Identified as MY_reCAPTCHA_SECRET_KEY

2. Integrate reCaptcha script in your Astro website
The script required to integrate reCaptcha in a website is explained in Google’s documentation for V3 Programmatically invoke the challenge.
If your website is implemented with the Astro framework as a static website (Client Side Rendering), take note the following information.
The reCaptcha script needs to be added in the page <head> element, with the async defer attributes, so that the script is downloaded without blocking the page loading. This will ensure that the Javascript compiled for the Astro component is loaded correctly.
More on async defer attributes in this Javascript documentation
<head>
<script
src="https://www.google.com/recaptcha/api.js?render=MY_reCAPTCHA_SITE_ID" async defer
></script>
</head>
3. Generate a reCaptcha challenge token on form submit button
Create a new Astro component with your form inputs. In the following example, the form consists of a name, email and message inputs. Then on the submit button, the form data is sent to the backend, which is responsible for verifying the token generated by the reCaptcha challenge.
File src/components/Contact.astro
<script>
const successMessageElement = document.getElementById("success-message");
const errorMessageElement = document.getElementById("error-message");
const informationMessageElement = document.getElementById(
"information-message"
);
function onSuccess() {
successMessageElement?.classList.remove("hidden");
errorMessageElement?.classList.add("hidden");
informationMessageElement?.classList.add("hidden");
}
function onError() {
successMessageElement?.classList.add("hidden");
errorMessageElement?.classList.remove("hidden");
informationMessageElement?.classList.add("hidden");
}
function onInformation() {
successMessageElement?.classList.add("hidden");
errorMessageElement?.classList.add("hidden");
informationMessageElement?.classList.remove("hidden");
}
function sendEmail({ name, email, message }) {
grecaptcha.ready(function () {
grecaptcha
.execute("MY_reCAPTCHA_SITE_ID", {
action: "submit",
})
.then(function (recaptchaToken) {
postMessage(
{ name, email, message, recaptchaToken },
onSuccess,
onError
);
});
});
}
async function postMessage(
{ name, email, message, recaptchaToken },
onSuccess,
onError
) {
const url = `https://my.backend.api/message`;
const data = {
from: email,
text: message + `\n` + name,
subject: `Contact message from ${name}`,
recaptchaToken: recaptchaToken,
};
try {
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(data),
});
if (response.ok) {
onSuccess();
} else {
onError();
}
} catch (e) {
onError();
}
}
const btnContactSend = document.getElementById("btn-contact-send");
btnContactSend?.addEventListener("click", function (event) {
event.preventDefault();
const name = document.getElementById("input-name")?.value;
const email = document.getElementById("input-email")?.value;
const message = document.getElementById("input-message")?.value;
if (name?.length > 0 && email?.length > 0 && message?.length > 0) {
sendEmail({ name, email, message });
} else {
onInformation();
}
});
</script>
<div id="contact" class="flex flex-col w-full md:w-8/12 lg:w-6/12 m-auto px-1">
<h2 class="text-center">My contact form</h2>
<div
class="col-span-1 flex flex-col p-5 rounded-xl my-2 lg:m-10 border-solid border shadow-lg border-gray-950 bg-gray-400"
>
<form id="contact-form" class="flex flex-col p-2 lg:p-8 gap-6">
<Input id="input-name" label="Name" type="text" />
<Input id="input-email" label="Email" type="text" />
<Input id="input-message" label="Message" type="textarea" />
<span class="hidden text-red-500" id="error-message">
Error sending the email
</span>
<span class="hidden text-blue-800" id="information-message">
The fields are incorrect
</span>
<span class="hidden text-green-500" id="success-message">
Message was sent successfully
</span>
<Button id="btn-contact-send" label="Send" type="primary" />
</form>
</div>
</div>
You can now call your Contact Astro component in your index.astro page.
<head>
<script
src="https://www.google.com/recaptcha/api.js?render=MY_reCAPTCHA_SITE_ID" async defer
></script>
</head>
<body>
<Contact />
</body>
4. Verify the reCaptcha challenge token in your backend server
To verify the token from your backend server, call the siteverify route. More information about the route and the possible response codes in the documentation reCaptcha guide Verifying the user’s response

This is an example of a backend API route controller, which takes a body containing the form inputs and the reCaptcha challenge. The main function is the handler of a serverless function, more about this in my tutorial CI/CD Send email serverless function in AWS
async function validateCaptcha(recaptchaToken) {
const response = await fetch(
"https://www.google.com/recaptcha/api/siteverify",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
secret: "MY_reCAPTCHA_SECRET_KEY",
response: recaptchaToken,
}),
}
);
const data = await response.json();
console.log("recaptcha response: ", data);
return data;
}
exports.handler = async (event) => {
console.log("event: ", event);
const { recaptchaToken, from, subject, text } = JSON.parse(event.body);
// Step 1: Validate the reCAPTCHA response
const captchaValidationResult = await validateCaptcha(recaptchaToken);
if (!captchaValidationResult.success) {
return response(400, { message: "reCAPTCHA verification failed" });
}
// Step 2... continue
}

