Frontend developer focused on inclusive design

Create contact page in Next.js

While working on my Next.js website, I had a need to create a page with a contact form to allow website visitors get it touch with me.

Below you can find instructions, based on my notes, to create a contact page in Next.js, with a help of Tailwind and Postmark.

Important to note, Tailwind CSS is used for styling the Contact page and its components, while Postmark is used as a service to deliver emails from a contact form to the recipient's mailbox.

Prepare

Before beginning to work on this page, make sure you have created a Next.js website. Otherwise, I would recommend to check my notes about how to create a website with Next.js and Tailwind.

Then, create your account at Postmark if you do not have one yet. You will later need to have some information from Postmark in order to be able send emails.

To prepare for developing a contact page:

  1. Open a project with your website in Visual Studio Code, or any other preferable code editor.
  2. Under a root directory of the project, locate the pages directory.

Build

This page allows the site visitors to get in touch with the owner, or the team, behind the site.

To create a contact page in Next.js:

  1. Go to the pages directory.
  2. Add new file to the pages directory, and name it contacts.js.

In addition to the page file, it is also needed to have a server-side file, responsible for sending emails to recipients.

To add the server-side functionality in Next.js:

  1. In the pages directory, go to the api directory. Often, a Next.js installation includes this directory by default. In case it is missing, create the api directory inside pages.
  2. In the api directory, create a new file and name it contacts.js.

By now, you should have two (2) new files in your project: one for the page and another for the server-side functions.

Note, it is not necessary to name the file for server-side functions the same as the file for the page. In my case, I use the same name for both files in order to show connection between these two files.

Page overview

The Contact page is consisted of two main sections:

  • Contact Form component;
  • Page component;

Disclamer

I coded my contact form component inside the Contact page. Often, it is recommended to have a separate file per component. However, I decided to have the component in the page file in terms of showcase simplicity.

I plan to create a separate file for this component when I will practice Dynamic Import functionality, as a part of additional layer of protection against bots, when creating a contact page in Next.js.

Page component

The purpose of this component is to output the current contact page. The structure of the page is pretty similar to other pages on the website, except one thing — it provides a contact form.

Contact Form component

The purpose of this component is to output a functional contact form.

Also, this component performs some verification actions before making a request to send an email. There are additional checks done on a server, via the server-side functionality of this contact form.

Beside checking the contact form inputs, the component also provides “honeypot” to spot spambots.

Honeypot technique

This technique involves adding an invisible field to a form, which can be seen only by spambots. As a result, you can trick them into revealing that they are spambots and not actual website visitors.

Once the email passes all checks, the form sends it using a Postmark service.

Postmark

Postmark is an email provider, and our case it provides an email delivery from a contact form to a recipient.

Think of this service as a postman, who takes a message from the contact form, and brings it to the person who is set as a recipient in the form.

The service also offers a free account with 100 emails per month.

Note, it is absolutely possible to use other email delivery service when building a contact page for a Next.js website.

However, I personally like Postmark because it helped Themes Harbor to resolve email delivery issue, and it provides a great service in general.

The Contact page uses Postmark API to send emails on a Next.js website.

Sender Signature

Postmark API allows to pass an email address that will appear as the from address.

However, it cannot be the email address of the person who actually sends an email using the contact form. It has to be your sender signature that was set up in Postmark previously.

For instance, let's imagine that John Doe ([email protected]) has decided to send me an email using a contact form on my Next.js website.

I thought that I should use his email ([email protected]) as the from address because I am receiving an email specifically "from" that person.

Also, setting this email as the from address would make it easier to reply to it by using default options of a mail app such as Gmail.

However, Postmark does not allow to use your site visitor's email address as the from address. This is an example of error you can get when using such email address:

“ApiInputError: The 'From' address you supplied ([email protected]) is not a Sender Signature on your account. Please add and confirm this address in order to be able to use it in the 'From' field of your messages.”

So, in order to send through Postmark you'll need to have a Sender Signature set up, and use it as the from address when sending emails via Postmark API.

Environment Variables

The Contact page uses environment variables to store:

  • Postmark API key;
  • Sender Signature email address;
  • Recipient email address;

It is done to avoid exposure of these variables in a source code.

Page code

To create the page:

  1. In pages directory, locate contacts.js file that you have created earlier.
  2. Add the page code (see below) to this newly created file.
  3. Read comments in the code snippet for additional information about the page and its functionality.

Here is an example of a final code snippet of the page:

/**
 * React states.
 *
 * `useState`: allows you to have state variables
 * in functional components.
 *
 * `useRef`: allows to directly create a reference
 * to the DOM element in the functional component.
 *
 * @link https://reactjs.org/docs/hooks-reference.html#usestate
 * @link https://reactjs.org/docs/hooks-reference.html#useref
 */
import { useState, useRef } from 'react'

/**
 * Container component.
 *
 * This component holds all main areas of the website,
 * such as `header`, `main`, `footer`.
 *
 * @link https://www.tarascodes.com/create-components-nextjs-tailwind
 */
import Container from '../components/Container';

 /**
  * Header component.
  *
  * This component contains all elements of
  * a top level `header` area.
  *
  * @link https://www.tarascodes.com/create-components-nextjs-tailwind
  */
import Header from '../components/Header';

 /**
  * Footer component.
  *
  * This component contains all elements of
  * a top level `footer` area.
  *
  * @link https://www.tarascodes.com/create-components-nextjs-tailwind
  */
import Footer from '../components/Footer';

/**
 * Page title component.
 *
 * This component outputs markup
 * for the main title of a page.
 *
 * @link https://www.tarascodes.com/create-components-nextjs-tailwind
 */
import PageTitle from '../components/PageTitle';

/**
 * Icon component.
 *
 * This component outputs
 * SVG icon based on set `id` and `size`.
 */
import Icon from '../components/Icon';

/**
 * SEO settings for the page.
 */
const seo = {
    title: 'Get in touch',
    description: 'I love hearing from readers and people from the web community, and I appreciate you taking the time to get in touch.'
}

/**
 * Contact Form component.
 */
const ContactForm = () => {
    // Name of the sender.
    const [name, setName] = useState('');
    // Email address of the sender.
    const [email, setEmail] = useState('');
    // Subject of the email.
    const [subject, setSubject] = useState('');
    // Message of the email.
    const [message, setMessage] = useState('');
    // Sort of a spam "inspector", which checks if honeypot has trapped a bot.
    const [inspector, setInspector] = useState('');
    // Status information about the contact form.
    const [status, setStatus] = useState({
        hasError: false,
        isSubmitted: false,
        message: [],
    });

    // Reference to the DOM element with the sender name.
    const nameInputRef = useRef(null);
    // Reference to the DOM element with the sender email address.
    const emailInputRef = useRef(null);
    // Reference to the DOM element with the email subject.
    const subjectInputRef = useRef(null);
    // Reference to the DOM element with the email message.
    const messageInputRef = useRef(null);

    const displayNotices = () => {
        // Make sure there are message(s) for notices.
        if ( ! status.message.length ) {
            return;
        }

        // Icon
        const noticeIcon = (type) => {
            switch (type) {
                case 'success':
                    return <Icon id='badge-check' size={ 24 } />;
                default:
                    return <Icon id='exclamation-circle' size={ 24 } />;
            }
        }

        // Notice
        const notice = (type, message) => {
            let cssClasses = 'border border-solid p-4 rounded mb-4 text-sm';

            switch (type) {
                case 'success':
                    cssClasses += ' text-green-700 border-green-400 bg-green-50';
                    break;
                default:
                    cssClasses += ' text-red-700 border-red-400 bg-red-50';
            }

            return (
                <div className={cssClasses}>
                    <p className="flex gap-3 items-center">
                        { noticeIcon(type) }
                        <span className="flex-1"><strong>{ message.title }: </strong>{ message.description }.</span>
                    </p>
                </div>
            )
        }

        // Output notice with a success information.
        if ( status.isSubmitted && ! status.hasError ) {
            return(
                notice( 'success', status.message[0] )
            );
        }

        // Output notice with an error information.
        if ( status.hasError ) {
            let jsxErrors = [];

            status.message.map((message, index) => {
                jsxErrors.push( <li key={index}>{notice('error', message)}</li> )
            });

            return <ul>{jsxErrors}</ul>
        }
    }

    // Display error in input field.
    const displayInputError = ( element ) => {
        // Add CSS classes to the element.
        element.classList.add( 'placeholder:text-red-700', 'border-red-400' );
        // Remove CSS classes in the element.
        element.classList.remove( 'border-slate-300' );
    }

    // Clear error in input field.
    const clearInputError = (element) => {
        // Add CSS classes to the element.
        element.classList.add( 'border-slate-300' );
        // Remove CSS classes in the element.
        element.classList.remove('border-red-400');
    }

    // Detect input focus.
    const handleOnBlur = (element) => {
        if ( '' !== element.value ) {
            clearInputError ( element );
        } else {
            displayInputError( element );
        }
    }

    // Detect message submission.
    const handleSubmit = (e) => {
        e.preventDefault();

        // Email data.
        let data = {
            name,
            email,
            subject,
            message,
            inspector
        }

        // Pattern for email address.
        const emailRegex = /\S+@\S+\.\S+/;

        // Check if the user entered a name; otherwise show an error.
        if ( ! name ) {
            displayInputError( nameInputRef.current );
        }

        // Check if the user entered an email; otherwise show an error.
        if ( ! email ) {
            displayInputError( emailInputRef.current );
        } else {
            // Make sure the user entered a valid email; otherwise indicate an error.
            if ( ! emailRegex.test(email) ) {
                displayInputError( emailInputRef.current );
                setStatus({
                    hasError: true,
                    isSubmitted: false,
                    message: [
                        {
                            title: 'Email format is not valid',
                            description: 'please enter an email with a valid format and then try to send your message again',
                        }
                    ],
                });
            }
        }

        // Check if the user entered a subject; otherwise show an error.
        if ( ! subject ) {
            displayInputError( subjectInputRef.current );
        }

        // Check if the user entered a message; otherwise show an error.
        if ( ! message ) {
            displayInputError( messageInputRef.current );
        }

        // Avoid a send action if fields are not filled or email address is not valid.
        if ( ! name || ! email || ! emailRegex.test( email ) || ! subject || ! message ) {
            return;
        }

        // Make a request to send the email using API.
        fetch('/api/contact', {
            method: 'POST',
            headers: {
              'Accept': 'application/json',
              'Content-Type': 'application/json'
            },
            body: JSON.stringify(data)
        }).then((response) => {
            // Display an error message.
            if ( 200 !== response.status ) {
                setStatus({
                    hasError: true,
                    isSubmitted: false,
                    message: [
                        {
                            title: 'Message was not sent',
                            description: 'something went wrong, please try again later',
                        }
                    ],
                });

                return;
            }

            // Display a successful message.
            setStatus({
                hasError: false,
                isSubmitted: true,
                message: [
                    {
                        title: 'Message sent',
                        description: 'thank you for your time writing this message',
                    }
                ],
            });
        });
    }

    return (
        <form className="flex flex-col gap-4">
            { displayNotices() }

            { ! status.isSubmitted && (
            <>
                <p className="flex sm:flex-row flex-col sm:gap-4 gap-2 focus-within:font-bold">
                    <label htmlFor="name" className="w-64">Name:</label>
                    <input
                        id="name"
                        className="
                            rounded w-full shadow-sm border-slate-300
                            focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:placeholder:text-white"
                        type="text"
                        name="fullname"
                        placeholder="Enter your name here"
                        onChange={(e)=>{
                            setName(e.target.value)
                        }}
                        onBlur={() => {
                            handleOnBlur(nameInputRef.current);
                        }}
                        ref={nameInputRef}/>
                </p>

                <p className="flex sm:flex-row flex-col sm:gap-4 gap-2 focus-within:font-bold">
                    <label htmlFor="email" className="w-64">Email:</label>
                    <input
                        id="email"
                        className="
                            rounded w-full shadow-sm border-slate-300
                            focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:placeholder:text-white"
                        type="email"
                        name="email"
                        placeholder="Enter your email address here"
                        onChange={(e)=>{
                            setEmail(e.target.value)
                        }}
                        onBlur={() => {
                            handleOnBlur(emailInputRef.current);
                        }}
                        ref={emailInputRef}/>
                </p>

                <p className="flex sm:flex-row flex-col sm:gap-4 gap-2 focus-within:font-bold">
                    <label htmlFor="subject" className="w-64">Subject:</label>
                    <input
                        id="subject"
                        className="
                            rounded w-full shadow-sm border-slate-300
                            focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:placeholder:text-white"
                        type="text"
                        name="subject"
                        placeholder="Enter a subject of your email here"
                        onChange={(e)=>{
                            setSubject(e.target.value)
                        }}
                        onBlur={() => {
                            handleOnBlur(subjectInputRef.current);
                        }}
                        ref={subjectInputRef}/>
                </p>

                <p className="flex sm:flex-row flex-col sm:gap-4 gap-2 focus-within:font-bold">
                    <label htmlFor="message" className="w-64">Message:</label>
                    <textarea
                        id="message"
                        className="
                            rounded w-full shadow-sm border-slate-300
                            focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:placeholder:text-white"
                        rows="8"
                        name="message"
                        placeholder="Enter your message here"
                        onChange={(e)=>{
                            setMessage(e.target.value)
                        }}
                        onBlur={() => {
                            handleOnBlur(messageInputRef.current);
                        }}
                        ref={messageInputRef}>
                    </textarea>
                </p>

                <p className="absolute -z-10 w-px h-px inset-0 overflow-hidden" aria-hidden="true">
                    <label htmlFor="bot-check" className="flex gap-2 items-center">
                        <input
                            id="bot-check"
                            name="botcheck"
                            type="checkbox"
                            defaultChecked={inspector}
                            onChange={(e) => {
                                setInspector(e.target.value)
                            }}
                            className="rounded" />
                        Are you a human?
                    </label>
                </p>

                <button
                    className="
                        sm:ml-auto shadow-sm shadow-blue-900/50 rounded px-6 py-3 bg-blue-700 text-white font-bold uppercase text-xs tracking-wider
                        hover:bg-blue-800 hover:shadow-none hover:text-blue-50
                        active:bg-blue-900 active:text-blue-200"
                    onClick={(e)=>{handleSubmit(e)}}>
                    Send message
                </button>
            </>
            )}
        </form>
    )
};

/**
 * Component for the current page.
 */
export default function Contacts() {
    return (
        <Container>
            <Header seo={seo} />

            <main className="sm:space-y-8 space-y-4">
                <PageTitle>
                    Get in touch
                </PageTitle>

                <ContactForm />
            </main>

            <Footer />
        </Container>
    )
}

Server code

To create a server-side functionality of the contact form:

  1. In pages directory, locate api directory.
  2. In api directory, locate contacts.js file that you have created earlier.
  3. Add the server code (see below) to this newly created file.
  4. Read comments in the code snippet for additional information about the API route and its functionality.

Here is an example of a final code snippet of the server-side functionality:

/**
 * The Postmark service functionality.
 *
 * @link https://postmarkapp.com/send-email/node
 */
const postmark = require("postmark");

// Client for the Postmark service.
const postmarkApp = new postmark.ServerClient( process.env.POSTMARK_API );

/**
 * Server-side functionality of the Contact page.
 *
 * @link https://nextjs.org/docs/api-routes/introduction
 */
export default async function handler(request, resesponse) {
    // Check the current request method.
    if ( 'POST' !== request.method ) {
        return resesponse.status(405).send(`${request.method} requests are not allowed.`);
    }

    // Data from a contact form.
    const { name, email, subject, message, inspector } = request.body;

    // Prevent spam from bots (Honeypot technique).
    if ( inspector ) {
        return resesponse.status(400).send('Message not sent.');
    }

    // Make sure we have the sender's name.
    if ( ! name || '' === name ) {
        return resesponse.status(400).send('Message not sent.');
    }

    // Make sure email address is added.
    if ( ! email || '' === email ) {
        return resesponse.status(400).send('Message not sent.');
    }

    // Make sure the email has a subject.
    if ( ! subject || '' === subject ) {
        return resesponse.status(400).send('Message not sent.');
    }

    // Make sure email has a content.
    if ( ! message || '' === message ) {
        return resesponse.status(400).send('Message not sent.');
    }

    try {
        // Information about the actual sender, which includes email address and name.
        const messageInfo = `\n---\n>>>from: ${email}\n>>>name: ${name}`;

        // Send email using Postmark API.
        const postmarkResponse = await postmarkApp.sendEmail({
            "From": process.env.POSTMARK_EMAIL_FROM,
            "To": process.env.POSTMARK_EMAIL_TO,
            "Subject": subject,
            "TextBody": message + messageInfo,
            "MessageStream": "outbound"
        });

        // Check if the email was sent without any issues.
        if ( ! postmarkResponse.ErrorCode ) {
            resesponse.status(200).send('Message sent successfully.');
        } else {
            resesponse.status(400).send('Message not sent.');
        }
    } catch (error) {
        resesponse.status(400).send('Message not sent.');
    }
}

Conclusion

You have now learned a way to start building a solid contact form functionality for a website, built with Next.js. Hopefully, this post was useful.