MAIS AMOR POR FAVOR

Gatsby Cloud Functions reCAPTCHA: Build a Contact Form

Gatsby Cloud Functions reCAPTCHA

Gatsby Cloud Functions reCAPTCHA: Build a Contact Form

SHARE:

✨ Introducing Gatsby Functions

In this Gatsby Cloud Functions reCAPTCHA project, we learn all about a brand new feature in Gatsby. That new feature is Gatsby Cloud Functions. Gatsby Cloud Functions let you code serverless (also known as lambda) functions right into your static Gatsby site. You might use them for processing form data or creating documents in your database via an API. Whatever you use them for, you will not have to worry about provisioning servers, maintaining them or even ensuring you have enough capacity to handle busy periods. All of that is taken out of your hands.

I got early access to Gatsby functions last month and set about building a demo site straight away. It is a static Gatsby MDX blog which lets users leave comments on blog posts . Using cloud functions, the site automatically triggers a rebuild so as to include fresh comments in the site's static content. All of that functionality was powered by serverless functions.

Today we'll look at something a little less ambitious, though still exciting. We are going to pair up Gatsby Cloud functions with the accessible Google reCAPTCHA. Read on to see how that is different from the annoying Captcha challenges of the past. We will bundle those features into a contact form for a Gatsby MDX blog.

If that sounds exciting, why don't we crack on? Before that though, as promised, we need to learn all about reCAPTCHA.

πŸ˜• How is a reCAPTCHA different to a CAPTCHA?

Gatsby Cloud Functions reCAPTCHA: Introducing reCAPTCHA v3

We have all probably done a CAPTCHA challenge at some point. You fill out a huge form, get to the bottom and have to run through a series of (seemingly) endless challenges! What's worse is that traditional CAPTCHAs can be inaccessible for partially sighted users . With reCAPTCHA, as users interact with the website, Google runs code behind the scenes, employing machine learning algorithms to determine if the interactions are typical of a human user or a bot. On form submission we can ask reCAPTCHA to generate a score from those user interactions giving us a metric to decide on whether we should perform some additional verification.

With that explained, let's build something interesting which you can use on your own site. We will look in more detail at the reCAPTCHA verification process as we go along. Most importantly it will demonstrate Gatsby Cloud functions.

🧱 What are we Building?

Gatsby Cloud Functions reCAPTCHA: Build a Contact Form: What we are Building

We will take a Gatsby v3 MDX starter blog and add a contact form using Formik. The submitted data will them be sent to our serverless function. Subsequently, the function will send us an email using SMTP to let us know someone has a message for us. We use SMTP to increase compatibility with whichever service you are already using, be it Mailtrain, Migadu, Postmark , Sendgrid or some other service. Wes Bos recommends using SMTP over a service's API in case you have to switch service at short notice. If you are using SMTP, it is just a case of swapping out credentials (rather than having to learn another API).

Now we know what we're doing, let's get a wriggle on! The first step will be to log into Google to generate reCAPTCHA API keys.

☁️ Gatsby Cloud Functions reCAPTCHA

Create reCAPTCHA API Keys

Firstly head over to Google to get API keys β€” at www.google.com/u/1/recaptcha/admin/create . We need to get reCAPTCHA v3 credentials. This includes a site key together with a secret key. So once you log in, enter a label for your site. Be sure to choose reCAPTCHA v3 and then enter the domains you need. You might want to add localhost as a domain now and then remove it once you are satisfied things are working. Make a note of the two generated keys, we will need them in the next step.

Gatsby Cloud Functions reCAPTCHA: Build a Contact Form: API Keys

Spin up a new Gatsby MDX Blog Site

Let's spin up a site to add those new keys to. We will use Gatsby Starter Climate which is a shell MDX blog site with a contact page . At the command line type:

gatsby new gatsby-cloud-functions-recaptcha https://github.com/rodneylab/gatsby-starter-climate.git

The site has some environment variables which we need to define (we will add the new reCAPTCHA API keys shortly). Following best practices, these are not stored in the repo. Instead you just need to copy the example file, which is in the repo:

cd gatsby-cloud-functions-recaptcha
npm install
cp .env.EXAMPLE .env.development
cp .env.EXAMPLE .env.production
gatsby develop

The last command spins up the site. You will see, as it stands, the blog is fully functional (open the site by going to localhost:8000 ). Before going on, take a look through the repo in your favourite code editor. Once you are comformable with how it all fits together, we'll carry on.

If you're ready, let's add those environment variables. Edit the .env.development and .env.production files, adding the following two lines to each:

7TELEGRAM_USERNAME="askRodney"
8WIRE_USERNAME="@rodneylab"
9GATSBY_RECAPTCHA_V3_SITE_KEY="your-site-key-from-previous-step"
10RECAPTCHA_V3_SECRET_KEY="your-secret-key-from-previous-step"

Gatsby Cloud Functions reCAPTCHA: Set up the Contact Form

We'll use some boilerplate code to speed things up in creating an accessible form. Let's start by installing the extra packages we will need: formik, lodash.isobject (used in FormikErrorFocus component, below) and axios for posting the form data to the cloud function.

npm install formik lodash.isobject axios

πŸ—³οΈ Poll

Which package do you prefer for adding forms to new projects?
Voting reveals latest results.

Let's create a basic contact form component. Create a component file at src/components/ContactForm.jsx and add the following content:

src/components/ContactForm.jsx β€” click to expand code.
src/components/ContactForm.jsx
jsx
import axios from 'axios';
import { Form, Formik } from 'formik';
import React, { useState } from 'react';
import { H_ELLIPSIS_ENTITY } from '../constants/entities';
import { container, errorText, formButton, successText } from './ContactForm.module.scss';
import FormikErrorFocus from './FormikErrorFocus';
import TextInputField, { TextAreaField } from './InputField';
const ContactForm = () => {
const [serverState, setServerState] = useState({ ok: true, message: '' });
const [showForm, setShowForm] = useState(true);
const recaptchaSiteKey = process.env.GATSBY_RECAPTCHA_V3_SITE_KEY;
const handleSubmit = (values, actions) => {
console.log('values: ', values);
};
const validEmail = (email) =>
/^(([^<>()[]\.,;:[email protected]"]+(.[^<>()[]\.,;:[email protected]"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/i.test(
email.trim(),
);
const validate = (values) => {
const errors = {};
if (values.name.trim() === '') {
errors.name = 'Please enter your name.';
}
if (!values.email.trim()) {
errors.email = 'Please enter your email address.';
} else if (!validEmail(values.email)) {
errors.email = 'Please check your email address.';
}
if (!values.message.trim()) {
errors.message = "Don't forget to write the message!";
} else if (values.message.trim() === '' || values.message.length > 1024) {
errors.message = 'Your message is a little long. Perhaps you could be more succint!';
}
return errors;
};
if (!showForm) {
return (
<div className={successText}>
<p>{serverState.message}</p>
</div>
);
}
return (
<div className={container}>
<Formik
initialValues={{
name: '',
email: '',
message: '',
}}
onSubmit={handleSubmit}
validate={validate}
>
{({ isSubmitting }) => (
<FormikErrorFocus>
<Form id="contact-form" name="contact">
<h2>πŸ–‹ Write me a short message{H_ELLIPSIS_ENTITY}</h2>
<TextInputField
aria-hidden
id="bot-field"
name="bot-field"
placeholder="Bot field"
label="Bot Field"
type="hidden"
/>
<TextInputField
isRequired
id="name-contact"
name="name"
placeholder="Name"
label="name"
title="Name"
type="text"
/>
<TextInputField
isRequired
id="email-contact"
name="email"
placeholder="Email"
label="email"
title="Email"
type="email"
/>
<TextAreaField
isRequired
id="message-contact"
name="message"
placeholder="Write your message here..."
label="message"
title="Message"
type="text"
rows="8"
/>
<div className={formButton}>
<input
type="submit"
aria-disabled={isSubmitting}
disabled={isSubmitting}
value="Submit your message"
/>
{!serverState.ok && (
<small className={errorText}>
Something is not quite right! Please try again later.
</small>
)}
</div>
</Form>
</FormikErrorFocus>
)}
</Formik>
</div>
);
};
export { ContactForm as default };

This won't work yet; we need to add a couple of imports first. I won't go into much detail explaining what we have here, since our main focus is Gatsby Cloud functions. Drop a comment below if you would like to see a separate post on creating accessible forms in Gatsby. Still, I should mention we have included a field just for bots! This a trick recommended by Netlify . The field is not visible in the browser and not announced by screen readers. It is present in the DOM though. It is pretty difficult for a human accidentally to find the field and enter a value. For that reason, whenever there is a value submitted for this field, it is quite safe to assume we are dealing with a bot. More on this when we create our cloud function code.

Next, we define accessible input fields. We do this using reusable components. Create a file for our input component at src/components/InputField.jsx. Give it the following content:

src/components/InputField.jsx β€” click to expand code.
src/components/InputField.jsx
jsx
1import { ErrorMessage, Field, useField } from 'formik';
2import PropTypes from 'prop-types';
3import React from 'react';
4import { isBrowser } from '../utilities/utilities';
5import { container, errorText, field } from './InputField.module.scss';
6
7const TextInputField = ({
8 'aria-hidden': ariaHidden,
9 className,
10 id,
11 innerRef,
12 isRequired,
13 label,
14 name,
15 placeholder,
16 type,
17}) => {
18 const [, meta] = useField(id, name, placeholder, type);
19
20 return (
21 <div className={container}>
22 <label htmlFor={id} className="screen-reader-text">
23 {label}
24 </label>
25 <Field
26 as="input"
27 id={id}
28 aria-hidden={ariaHidden}
29 aria-invalid={meta.error && meta.touched ? 'true' : null}
30 aria-describedby={meta.error && meta.touched ? `${id}-error` : null}
31 aria-required={isRequired ? true : null}
32 className={`${className} ${field}`}
33 name={name}
34 placeholder={placeholder}
35 type={type}
36 innerRef={innerRef}
37 />
38 <ErrorMessage id={`${id}-error`} className={errorText} name={name} component="small" />
39 </div>
40 );
41};
42
43TextInputField.defaultProps = {
44 'aria-hidden': false,
45 innerRef: null,
46 isRequired: null,
47 className: '',
48};
49
50TextInputField.propTypes = {
51 innerRef: isBrowser
52 ? PropTypes.oneOfType([
53 PropTypes.func,
54 PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
55 ])
56 : PropTypes.func,
57 'aria-hidden': PropTypes.bool,
58 className: PropTypes.string,
59 id: PropTypes.string.isRequired,
60 isRequired: PropTypes.bool,
61 label: PropTypes.string.isRequired,
62 name: PropTypes.string.isRequired,
63 placeholder: PropTypes.string.isRequired,
64 type: PropTypes.string.isRequired,
65};
66
67export const TextAreaField = ({
68 className,
69 id,
70 innerRef,
71 isRequired,
72 label,
73 name,
74 placeholder,
75 rows,
76 type,
77}) => {
78 const [, meta] = useField(id, name, placeholder, type);
79
80 return (
81 <div className={container}>
82 <label htmlFor={id} className="screen-reader-text">
83 {label}
84 </label>
85 <Field
86 as="textarea"
87 id={id}
88 aria-invalid={meta.error && meta.touched ? 'true' : null}
89 aria-describedby={meta.error && meta.touched ? `${id}-error` : null}
90 aria-required={isRequired ? true : null}
91 className={`${className} ${field}`}
92 name={name}
93 placeholder={placeholder}
94 rows={rows}
95 type={type}
96 innerRef={innerRef}
97 />
98 <ErrorMessage id={`${id}-error`} className={errorText} name={name} component="small" />
99 </div>
100 );
101};
102
103TextAreaField.defaultProps = {
104 innerRef: null,
105 className: '',
106 isRequired: false,
107 label: '',
108 rows: '5',
109};
110
111TextAreaField.propTypes = {
112 innerRef: isBrowser
113 ? PropTypes.oneOfType([
114 PropTypes.func,
115 PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
116 ])
117 : PropTypes.func,
118 className: PropTypes.string,
119 id: PropTypes.string.isRequired,
120 isRequired: PropTypes.bool,
121 label: PropTypes.string,
122 name: PropTypes.string.isRequired,
123 placeholder: PropTypes.string.isRequired,
124 rows: PropTypes.string,
125 type: PropTypes.string.isRequired,
126};
127
128export default TextInputField;

A Touch of Style

With our input components defined, let's touch them up a little with a spot of CSS. Create a style file at src/components/InputField.module.scss and paste this minimal styling into it:

src/components/InputField.module.scss β€” click to expand code.
src/components/InputField.module.scss
scss
1@import '../styles/styles.scss';
2
3.container {
4 margin-top: $spacing-8;
5
6 h2 {
7 font-size: $font-size-2;
8 }
9}
10
11.success-text {
12 width: 100%;
13 background-color: $color-theme-4;
14 color: $color-theme-3;
15 border-radius: $spacing-2;
16 margin-top: $spacing-8;
17 margin-bottom: $spacing-12;
18 padding: $spacing-2;
19 p {
20 margin-bottom: $spacing-0;
21 text-align: center;
22 }
23}
24
25.form-button {
26 display: flex;
27 flex-direction: column;
28 margin: 0 auto;
29 width: 50%;
30
31 input {
32 margin-top: $spacing-4;
33 margin-left: auto;
34 font-size: $font-size-2;
35 background-color: $color-theme-4;
36 color: $color-theme-3;
37 padding: $spacing-2;
38 border-style: none;
39 border-radius: $spacing-2;
40 }
41
42 input[aria-disabled='true'] {
43 background-color: (lighten($color-theme-4, 40%));
44 }
45
46 input:hover {
47 cursor: pointer;
48 }
49}
50
51.error-text {
52 color: $color-danger;
53}

Let's not leave the contact form component out:

src/components/ContactForm.module.scss β€” click to expand code.
src/components/ContactForm.module.scss
scss
1@import '../styles/styles.scss';
2
3.container {
4 margin-top: $spacing-8;
5
6 h2 {
7 font-size: $font-size-2;
8 }
9}
10
11.success-text {
12 width: 100%;
13 background-color: $color-theme-4;
14 color: $color-theme-3;
15 border-radius: $spacing-2;
16 margin-top: $spacing-8;
17 margin-bottom: $spacing-12;
18 padding: $spacing-2;
19 p {
20 margin-bottom: $spacing-0;
21 text-align: center;
22 }
23}
24
25.form-button {
26 display: flex;
27 flex-direction: column;
28 margin: 0 auto;
29 width: 50%;
30
31 input {
32 margin-top: $spacing-4;
33 margin-left: auto;
34 font-size: $font-size-2;
35 background-color: $color-theme-4;
36 color: $color-theme-3;
37 padding: $spacing-2;
38 border-style: none;
39 border-radius: $spacing-2;
40 }
41
42 input[aria-disabled='true'] {
43 background-color: (lighten($color-theme-4, 40%));
44 }
45
46 input:hover {
47 cursor: pointer;
48 }
49}
50
51.error-text {
52 color: $color-danger;
53}

Finally we add some boilerplate code to improve user experience. This is just to help users out when there are form errors. For example if the user submits the form, forgetting to enter their email, the email field becomes focused and highlighted so they know exactly where the error is.

src/components/FormikErrorFocus.jsx β€” click to expand code.
src/components/FormikErrorFocus.jsx
jsx
1import { useFormikContext } from 'formik';
2import isObject from 'lodash.isobject';
3import { useEffect } from 'react';
4
5const getFirstErrorKey = (object, keys = []) => {
6 const firstErrorKey = Object.keys(object)[0];
7 if (isObject(object[firstErrorKey])) {
8 return getFirstErrorKey(object[firstErrorKey], [...keys, firstErrorKey]);
9 }
10 return [...keys, firstErrorKey].join('.');
11};
12
13const FormikErrorFocus = ({ children }) => {
14 const formik = useFormikContext();
15
16 useEffect(() => {
17 if (!formik.isValid && formik.submitCount > 0) {
18 const firstErrorKey = getFirstErrorKey(formik.errors);
19 if (global.window.document.getElementsByName(firstErrorKey).length) {
20 global.window.document.getElementsByName(firstErrorKey)[0].focus();
21 }
22 }
23 }, [formik.submitCount, formik.isValid, formik.errors]);
24 return children;
25};
26
27export default FormikErrorFocus;

Once again I won't talk much about this, but drop a comment below if you have a question or two!

reCAPTCHA Client API call

We're moving along quite quickly now! Next thing we'll look at is the client side Google Captcha API call. The way reCAPTCHA works, we need to make two calls to the API. The first, on the client side, is to get a token which identifies the user session. That token is valid for two minutes. As soon as we have the token we will send it to the cloud function so it can be used by the second API call, before it expires. We'll build up the code to implement this now. Edit the ContactForm.jsx file:

src/components/ContactForm.jsx β€” click to expand code.
src/components/ContactForm.jsx
jsx
4import { Helmet } from 'react-helmet';
5import { H_ELLIPSIS_ENTITY } from '../constants/entities';
6import { container, errorText, formButton, successText } from './ContactForm.module.scss';
7import FormikErrorFocus from './FormikErrorFocus';
8import TextInputField, { TextAreaField } from './InputField';
9
10const ContactForm = () => {
11 const [serverState, setServerState] = useState({ ok: true, message: '' });
12 const [showForm, setShowForm] = useState(true);
13 const recaptchaSiteKey = process.env.GATSBY_RECAPTCHA_V3_SITE_KEY;
14
15 const submitData = async (values, { setSubmitting, resetForm }, recaptchaToken) => {
16 console.log('values: ', values, recaptchaToken);
17 };
18
19 const handleSubmit = (values, actions) => {
20 window.grecaptcha.ready(() => {
21 window.grecaptcha.execute(recaptchaSiteKey, { action: 'submit' }).then((token) => {
22 submitData(values, actions, token);
23 });
24 });
25 };
26
27 const validEmail = (email) =>
28 /^(([^<>()[]\.,;:[email protected]"]+(.[^<>()[]\.,;:[email protected]"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/i.test(
29 email.trim(),
30 );
31
32 const validate = (values) => {
33 const errors = {};
34 if (values.name.trim() === '') {
35 errors.name = 'Please enter your name.';
36 }
37 if (!values.email.trim()) {
38 errors.email = 'Please enter your email address.';
39 } else if (!validEmail(values.email)) {
40 errors.email = 'Please check your email address.';
41 }
42 if (!values.message.trim()) {
43 errors.message = "Don't forget to write the message!";
44 } else if (values.message.trim() === '' || values.message.length > 1024) {
45 errors.message = 'Your message is a little long. Perhaps you could be more succint!';
46 }
47 return errors;
48 };
49
50 if (!showForm) {
51 return (
52 <div className={successText}>
53 <p>{serverState.message}</p>
54 </div>
55 );
56 }
57
58 return (
59 <div className={container}>
60 <Helmet>
61 <script
62 key="recaptcha"
63 type="text/javascript"
64 src={`https://www.google.com/recaptcha/api.js?render=${recaptchaSiteKey}`}
65 />
66 </Helmet>
67 <Formik
68 initialValues={{
69 name: '',
70 email: '',
71 message: '',
72 }}
73 onSubmit={handleSubmit}
74 validate={validate}
75 >
76 ...
77 </Formik>
78 ...

We've done a few things here so let's go through them one by one. First of all, we mentioned before that reCAPTCHA observes user behaviour up to the point they submit the form. That is done via the script we load in lines 60–66. We use React Helmet to place the script in the page's HTML head section. Next, when the user clicks the submit button, Formik calls handleSubmit (line 73). In the handleSubmit function, we call the grecaptcha function which, in turn, is defined in the script we just added via Helmet. This function contacts Google and fetches us a unique client token. Finally, handleSubmit calls submitData. For now, that final function just prints the form data to the console. Let's change that next!

Submit Form Data

Finally for the client, we need to post the form data to our Gatsby Cloud function. To use Gatsby Cloud functions, we need to create an api folder under src. We will soon see how this is done. For now let's just finish the client side by posting the comment. Update the submitData function:

src/components/ContactForm.jsx β€” click to expand code.
src/components/ContactForm.jsx
jsx
15const submitData = async (values, { setSubmitting, resetForm }, recaptchaToken) => {
16 try {
17 setSubmitting(true);
18 const { 'bot-field': botField, email, message, name } = values;
19 await axios({
20 url: '/api/submit-message',
21 method: 'POST',
22 data: {
23 botField,
24 email,
25 message,
26 name,
27 recaptchaToken,
28 },
29 });
30 setServerState({
31 ok: true,
32 message: 'Thanks for your message! We will try to respond within one working day.',
33 });
34 setSubmitting(false);
35 resetForm();
36 setShowForm(false);
37 } catch (error) {
38 if (error.response) {
39 console.log('Server responded with non 2xx code: ', error.response.data);
40 } else if (error.request) {
41 console.log('No response received: ', error.request);
42 } else {
43 console.log('Error setting up response: ', error.message);
44 }
45 setServerState({ ok: false, message: error.message });
46 }
47 };

This is just a basic API call using axios. There is a fair bit of error handling code here, which is helpful for debugging. This is mostly boilerplate, nonetheless, let me know if you think it needs more explanation. You can also check the axios documentation for help . With the client side wrapped up, let's jump to the highlight of this project; the Gatsby Cloud function.

Gatsby Cloud Functions reCAPTCHA: Set up the Serverless Function

If you are using Gatsby 3.7 or newer there is no additional config to start using Gatsby Cloud functions. Let's actually define our serverless function then. On the client side, we post data to the /api/submit-message route. So we need to create a file containing the cloud function, in our project with the matching path src/api/submit-message.js. Let's do that now:

src/api/submit-message.js
javascript
1const recaptchaValidation = async ({ recaptchaToken }) => {
2 /* coming soon */
3};
4
5const sendEmail = async ({ email, googleCaptchaScore, markedSpam, message, name }) => {
6 /* coming soon */
7};
8
9export default async function handler(req, res) {
10 if (req.method !== 'POST') {
11 res.status(405).send('Method not allowed');
12 } else {
13 const { botField, email, message, name, recaptchaToken } = req.body;
14 const markedSpam = botField != null;
15
16 const recaptchaValidationResult = await recaptchaValidation({ recaptchaToken });
17
18 if (!recaptchaValidationResult.successful) {
19 res.status(400).send(recaptchaValidationResult.message);
20 } else {
21 const googleCaptchaScore = recaptchaValidationResult.message;
22
23 const sendEmailResult = await sendEmail({
24 email,
25 googleCaptchaScore,
26 markedSpam,
27 message,
28 name,
29 });
30 if (!sendEmailResult.successful) {
31 res.status(400).send(sendEmailResult.message);
32 } else {
33 res.status(200).send('All good!');
34 }
35 }
36 }
37}

We probably completed the most technically challenging part of our project when we set up the contact form. In common with Netlify functions , Gatsby functions are fairly easy to implement, though the syntax differs slightly between the two services. One pro for Gatsby cloud functions is that it is easier to test functions locally, on the development server. We'll see this in a moment, but first let's go through the code we have just added.

Gatsby Cloud Function Anatomy

So the main function is the handler. This is what gets called when a user hits the API. In lines 10–11 of our serverless function, we see how we can check the user called our handler with the expected method (that is POST as opposed to GET etc.). Normally, we would want to revalidate all input data, within the cloud function. For the sake of brevity, we won't go into that here, though you can use similar functions to the ones used on the client side. In lines 11, 19, 31 and 33 we see how we can send different response codes back to the client browser. Apart from the response code, we can respond to the user's API call with an HTTP body. The body can be JSON data or a string:

// string response body
res.status(200).send('All is well that ends well.');
// json response body
res
.status(200)
.json({ time: new Date().toISOString(), status: "Doing well!" });

We're going for string responses in our project. We just include JSON for future reference. There's not much left to do now. Next we will flesh out the code for the reCAPTCHA server side response. After that we fill out the SMTP email function and then we are all set for testing!

reCAPTCHA Serverless API call

This is the second call to the reCAPTCHA API. Our Gatsby Cloud functions reCAPTCHA call will send the reCAPTCHA token we got in the client. The Google server will respond with a score. That score is on the scale zero to one, with zero indicating it is very likely the user is a bot.

If that's all clear, let's press on and fill out the function body. Edit src/api/submit-message.js:

src/api/submit-message.js
javascript
1import axios from 'axios';
2
3const recaptchaValidation = async ({ recaptchaToken }) => {
4 const result = await (async () => {
5 try {
6 const response = await axios({
7 url: 'https://www.google.com/recaptcha/api/siteverify',
8 method: 'POST',
9 params: { secret: process.env.RECAPTCHA_V3_SECRET_KEY, response: recaptchaToken },
10 });
11 return { successful: true, message: response.data.score };
12 } catch (error) {
13 let message;
14 if (error.response) {
15 message = `reCAPTCHA server responded with non 2xx code: ${error.response.data}`;
16 } else if (error.request) {
17 message = `No reCAPTCHA response received: ${error.request}`;
18 } else {
19 message = `Error setting up reCAPTCHA response: ${error.message}`;
20 }
21 return { successful: false, message };
22 }
23 })();
24 return result;
25};

In a production application, you would probably want to implement a method for additional user validation if the reCAPTCHA score is below a chosen threshold. Here, we will just pass the score through on the email we send ourselves with the user's message. Talking of email, let's now finish up by writing that final function.

Set up SMTP Email

We're now ready to write out the sendEmail function. We mentioned earlier that we will use SMTP rather than any particular API to future proof our code. We will need SMTP credentials to be able to send email from the Gatsby Cloud function. As before, in line with best practise, we define those credentials in environment variables. Update .dev.development and .dev.production with this additional variables:

9RECAPTCHA_V3_SECRET_KEY="your-secret-key-from-previous-step"
10SMTP_HOST="smtp.examplemailservice.com"
11SMTP_SECRET="password-or-secret-api-key"
12SMTP_SENDER='"Example Sender" <[email protected]>'
13SMTP_USER="smtp-server-username"

The precise values you need will depend on your preferred service. The documentation for Sendgrid and Mailgun is linked. If you don't yet have an SMTP server to test with, get free SMTP test credentials from Ethereal .

Moving on swiftly, we will use nodemailer to send email from the lambda function, so let's install it:

npm i nodemailer

Our final step here is to import nodemailer and to fill out the sendEmail function:

src/api/submit-message.js
javascript
1import axios from 'axios';
2import nodemailer from 'nodemailer';
3...
src/api/submit-message.js
javascript
28const sendEmail = async ({ email, googleCaptchaScore, markedSpam, message, name }) => {
29 const result = await (async () => {
30 try {
31 const text = JSON.stringify(
32 {
33 name,
34 email,
35 message,
36 googleCaptchaScore,
37 markedSpam,
38 },
39 null,
40 2,
41 );
42
43 const transporter = nodemailer.createTransport({
44 host: process.env.SMTP_HOST,
45 port: 465,
46 secure: true,
47 auth: {
48 user: process.env.SMTP_USER,
49 pass: process.env.SMTP_SECRET,
50 },
51 });
52
53 // uncomment for additional help debugging
54 // await transporter.verify();
55
56 const info = await transporter.sendMail({
57 from: process.env.SMTP_SENDER,
58 to: process.env.CONTACT_EMAIL,
59 subject: 'example.com Contact Form Message',
60 text,
61 });
62 return { successful: true, message: info.messageId };
63 } catch (error) {
64 return { successful: false, message: JSON.stringify(error, null, 2) };
65 }
66 })();
67 return result;
68};
69...

The email we send here is a basic, plaintext email containing the bare essentials. If you are setting this up for a client, you might consider using an HTML email and formatting it nicely!

For the sake of clarity, let's have an overview of the message submission process:

  1. The user submits the form on our website.
  2. The information from the form is them sent by this Gatsby Cloud function. The email address which the function sends from will be whatever you chose for the value SMTP_SENDER in the environment variables. You will need to have your service configured to send email from that address for this to work.
  3. Whenever a user submits a message on the contact form, you will receive an email containing the message details. The email is sent to whichever email address you set CONTACT_EMAIL to in your environment variables.

Hope that makes sense, but let me know if that needs more explanation.

Test itΒ out

Well done for making it this far! Let's wrap up by sending a test comment from the front end. You can do this on your local development server, without the need to push your repo to your hosting service. Go to localhost:8000/contact/ and send a test message.

Gatsby Cloud Functions reCAPTCHA: Build a Contact Form: Check your Work

You can see the full code for this Gatsby Cloud Functions reCAPTCHA example on the Rodney Lab GitHub repo.

πŸ™ŒπŸ½ How was it for you?

In this post we learned:

  • how to use Gatsby functions,
  • using reCAPTCHA to check for bots,
  • how to send SMTP email using a cloud function.

I hope you have found this valuable and worth the time you invested. Let me know if there is anything in the post that I can improve on, for any one else creating this project. You can leave a comment below, @ me on twitter or try one of the other contact methods listed below.

πŸ™πŸ½ Gatsby Cloud Functions reCAPTCHA: Feedback

As I say, I hope you enjoyed following along and creating this project as well as learned something new. I also hope you will use this code in your own projects. I would love to hear what you are building with cloud functions as well as reCAPTCHA. Perhaps you are de-googling and will opt for HCaptcha instead of reCAPTCHA. Hopefully you can leverage something from this post in that endeavour. Let me know about your journey as well as the differences in using the two APIs. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter and also askRodney on Telegram . Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as Gatsby JS among other topics. Also subscribe to the newsletter to keep up-to-date with our latest projects.