How to set up a simple contact form on your Ghost blog using Basin

Five telephone booths next to each other at night.
"She got out." - "It doesn't matter." - "The informant is real."

I was looking for the simplest way to set up a contact form on my Ghost blog and I'm happy to report that I found one.

If you're looking for a sneak peek on what we're going to build, take a look at my contact page. Feel free to send me a message to see the contact form in action.

You can also play with the contact form on CodePen. Just paste your form endpoint into the form's action attribute after obtaining it.

Create a Basin account

We can do most everything in Ghost, but we need a provider that can process the contact form data and email it to us. For this, I chose Basin. They offer a free plan (100 submission/month) and it does everything we need.

  1. Sign up for Basin on their website.
  2. Click link in email to confirm account.

Configure Basin project

Let's set a name and timezone for our project. After clicking the email confirmation link, you should be on the My Forms page. If not, click on Forms in the top navigation.

  1. To the right of My First Form, click on cogwheel to open settings.
  2. Enter Name, like your website domain, to help identify this project.
  3. Select your Timezone so that timestamps in messages are accurate.
  4. Click on Save Changes button.

Configure Basin form

Next we're going to give the form a recognizable name, enable Ajax, and configure a custom honeypot. Basically some housekeeping to prepare our form.

  1. Click on Forms in top navigation.
  2. Click on My First Form underneath project name you chose.

Settings > General

  1. Click on Settings in form menu (far right of form header).
  2. Enter Name, like Contact Form, to help identify this form.
  3. Click Enable AJAX switch toward right to enable it.
  4. Click Save Changes button.

When you submit a contact form, the browser will normally follow the URL defined in the form's action attribute. In our case, that would be a thank you page on Basin's website.

To improve the user experience, we're going to use Ajax and custom JavaScript to display a thank you message right on our contact page, which is why we enabled that. More on that later.

Settings > Spam

  1. Click on Spam in settings menu (left side under form header).
  2. Scroll down to Custom Honeypot section.
  3. Enter Hidden field name. I entered organization in mine.
  4. Optionally, under Accepted Languages, choose languages you can read.
  5. Click Save Changes button.

As you can see, there are a number of ways to protect you from spam, including Google reCAPTCHA and hCAPTCHA. I'm not a fan of either, so I'll rely on Basin's already-enabled Duplicate Filter and set up a Custom Honeypot.

A honeypot for forms is simply an additional input field that we expect to remain empty when the contact form is submitted. Should Basin see a value for organization, it will automatically mark it as spam and not deliver it to your inbox.

I'm only going to be asking the visitor for name, email, and their message, so I chose a field called organization. It's somewhat realistic, because many forms do ask for something like company, institution, or organization.

To tie it all together, this organization field will be hidden to human visitors, so they won't see it and therefore not fill it out. Robots, on the other hand, look at the HTML markup and should see this field. Since robots generally attempt to fill out as many fields as possible, this is what makes the honeypot work.

Emails > Notification

  1. Click Emails in form menu.
  2. Click Notifications in emails menu.
  3. Enter From name to set email sender. I entered my website domain.
  4. Enter Subject line for emails. I entered New message from {{name}}.
  5. Optionally, you can also hide the dashboard button in the email. I did.

We can use a feature in Basin called merge tags. It allows you to use a form field's value in your contact form email. Since we will have a field called name, we can use it in the subject line to see who sent us a message before opening the email.

It'll display something like this: "New message from Claudia Pumpernickel".

Submissions

  1. Click Submissions in form menu.
  2. Look at Form Setup box on right.
  3. Note form endpoint, e.g. https://usebasin.com/f/xxxxxxxxxxxx

Since we're already in Basin, I wanted to point out the form endpoint. Keep this URL available for the next section.

Create contact form page

Now we're going to set up everything we need in Ghost.

  1. Click on Pages and then New Page button in Ghost.
  2. Click (+) button within body and add HTML component.
  3. Copy and paste following code into HTML component:
<form action="" method="POST" id="contact-form">
  <div>
    <label for="name">First and Last Name</label>
    <input type="text" id="name" name="name" autofocus required>
  </div>
  <div>
    <label for="email">Email Address</label>
    <input type="email" id="email" name="email" required>
  </div>
  <div class="poof">
    <label for="organization">Organization</label>
    <input type="text" id="organization" name="organization">
  </div>
  <div>
    <label for="message">What's on your mind?</label>
    <textarea name="message" id="message" rows="10" required></textarea>
  </div>
  <div id="response-container"></div>
  <div>
    <div id="button-container">
      <button type="submit" id="send-button">
        Send Message
      </button>
      <div id="send-indicator" class="hide">
        <div></div>
        <div></div>
        <div></div>
      </div>
    </div>
  </div>
</form>

Once you've done that, paste your form endpoint from earlier into the form's action attribute. It should look something like this:

<form action="https://usebasin.com/f/xxxxxxxxxxxx" method="POST" id="contact-form">

The contact form asks for first and last name, email address, and a message:

<div>
  <label for="name">First and Last Name</label>
  <input type="text" id="name" name="name" autofocus required>
</div>
<div>
  <label for="email">Email Address</label>
  <input type="email" id="email" name="email" required>
</div>
<div>
  <label for="message">What's on your mind?</label>
  <textarea name="message" id="message" rows="10" required></textarea>
</div>

Feel free to adjust the values between the labels:

  • First and Last Name
  • Email Address
  • What's on your mind?

Between email and message, we also have our honeypot field:

<div class="poof">
  <label for="organization">Organization</label>
  <input type="text" id="organization" name="organization">
</div>

The poof class will ensure it's not visible to visitors.

We also have a container to output a success or error message from Basin, which we'll retrieve using Ajax:

<div id="response-container"></div>

Last, there is a button to send the message:

<button type="submit" id="send-button">
  Send Message
</button>

And an indicator that will display while the messaging is sending:

<div id="send-indicator" class="hide">
  <div></div>
  <div></div>
  <div></div>
</div>

If you decide to change any of the HTML IDs:

  • contact-form
  • response-container
  • button-container
  • send-button
  • send-indicator

You'll have to update them in the CSS and JavaScript, too.

Inject CSS in page header

Let's make the contact form look a little better.

I'm using Ghost's Casper theme, which doesn't include form styles. My aim is to provide you with a comprehensive solution, but if you have a custom theme or know your way around CSS, by all means, customize the look and feel to your heart's content.

  1. On your contact page, open Settings sidebar.
  2. Click Code injection toward bottom.
  3. Copy and paste following code into Page header:
<style type="text/css">
  #contact-form > div {
    margin: 20px 0;
  }
  #contact-form .poof {
    display: none;
  }
  #contact-form label {
    color: #4c566a;
    display: block;
    margin-bottom: 10px;
  }
  #contact-form input,
  #contact-form textarea {
    border: 1px solid #d8dee9;
    border-radius: 5px;
    box-shadow: inset 1px 1px 2px #e5e9f0;
    padding: 12px;
    width: 500px;
  }
  .dark-mode #contact-form label {
    color: inherit;
  }
  .dark-mode textarea {
    color: #303a3e;
  }
  @media (prefers-color-scheme: dark) {
    #contact-form label {
        color: inherit;
    }
    #contact-form textarea {
        color: #303a3e;
    }
  }
  @media only screen and (max-width: 600px) {
    #contact-form input,
    #contact-form textarea {
      width: 100%;
    }
  }
  #contact-form input:focus,
  #contact-form textarea:focus,
  #contact-form button:focus {
    outline: 2px solid #5e81ac;
    outline-offset: 2px;
  }
  #response-container {
    font-weight: bold;
  }
  #response-container.success {
    color: #a3be8c;
  }
  #response-container.error {
    color: #bf616a;
  }
  #button-container {
    background-color: #2e3440;
    border: none;
    border-radius: 5px;
    color: #fff;
    cursor: pointer;
    display: inline-block;
    position: relative;
  }
  #send-button.hide,
  #send-indicator.hide {
    visibility: hidden;
  }
  #send-button {
    background: none;
    border: none;
    border-radius: 5px;
    color: #fff;
    cursor: pointer;
    padding: 12px 20px;
  }
  #send-button:hover {
    background-color: #3b4252;
  }
  #send-indicator {
    display: flex;
    justify-content: center;
    position: absolute;
    top: 20%;
    left: 0;
    right: 0;
  }
  #send-indicator div {
    animation: 0.9s bounce infinite alternate;
    background: #fff;
    border-radius: 50%;
    margin: 0 3px;
    height: 7px;
    width: 7px;
  }
  #send-indicator div:nth-child(2) {
    animation-delay: 0.3s;
  }
  #send-indicator div:nth-child(3) {
    animation-delay: 0.6s;
  }
  @keyframes bounce {
    to {
      opacity: 0.3;
      transform: translate3d(0, 10px, 0);
    }
  }
</style>
💡
Feb 18, 2023: Added couple .dark-mode properties to fix color of label and textarea when Casper is switched to dark color scheme.
💡
Mar 1, 2023: Added prefers-color-scheme: dark to fix label and textarea when Casper is set to auto color scheme and it's currently dark.

Last, let's make the contact form functional.

You could test and submit the contact form now, but you'd be greeted with a page that looks something like this:

{"success":true,"given_params":{"name":"Claudia Pumpernickel","email":"[email protected]","message":"In the pursuit of making our future simpler, we tend to sacrifice our time overcomplicating the present."}}

As previously mentioned, if you go to Basin and disable Ajax, you'd be directed to their thank you page, however, we'd like to keep the visitor on our website and display a success or error message right in our form.

  1. On your contact page, open Settings sidebar.
  2. Click Code injection toward bottom.
  3. Copy and paste following code into Page footer:
<script type="text/javascript">

  // Send contact form in Ghost to Basin
  document.addEventListener("DOMContentLoaded", sendContactFormToBasin);
  function sendContactFormToBasin() {
    const contactForm = document.getElementById("contact-form");
    if (contactForm === null) {
      return;
    }
    contactForm.addEventListener("submit", whenContactFormSubmitted);
  }

  // Run when user submits contact form
  function whenContactFormSubmitted(event) {
    event.preventDefault();
    const contactForm = event.target;
    const formData = new FormData(contactForm);
    const xhr = new XMLHttpRequest();
    toggleSendButton();
    xhr.open("POST", contactForm.action, true);
    xhr.send(formData);
    xhr.addEventListener("load", whenContactFormSent);
  }

  // Run when contact form is sent to Basin
  function whenContactFormSent(event) {
    const xhr = event.target;
    const contactForm = document.getElementById("contact-form");
    const responseContainer = document.getElementById("response-container");
    if (responseContainer === null) {
      return;
    }
    if (xhr.status === 200) {
      responseContainer.classList.add("success");
      responseContainer.innerHTML = "Message successfully sent.";
      contactForm.reset();
      toggleSendButton();
      setTimeout(resetResponseContainer, 5000, responseContainer);
    } else {
      let response = JSON.parse(xhr.response);
      responseContainer.classList.add("error");
      responseContainer.innerHTML = "Error: " + response.error;
      toggleSendButton();
    }
  }

  // Disable submit button while sending; renable when done
  function toggleSendButton() {
    const sendButton = document.getElementById("send-button");
    const sendIndicator = document.getElementById("send-indicator");
    sendButton.classList.toggle("hide");
    sendIndicator.classList.toggle("hide");
  }

  // Erase success/error message after submitting contact form
  function resetResponseContainer(responseContainer) {
    responseContainer.innerHTML = "";
    responseContainer.className = "";
  }
</script>

The script looks for a form with a contact-form ID. If it exists, it listens for when that form is submitted.

function sendContactFormToBasin() {
  const contactForm = document.getElementById("contact-form");
  if (contactForm === null) {
    return;
  }
  contactForm.addEventListener("submit", whenContactFormSubmitted);
}

When a form is submitted, the visitor is normally redirected to the URL within the form's action attribute, however the script prevents this behavior with event.preventDefault().

The script now shows the sending indictor by calling toggleSendButton(), sends the form data to Basin using XMLHttpRequest, and listens for when it's complete.

function whenContactFormSubmitted(event) {
  event.preventDefault();
  const contactForm = event.target;
  const formData = new FormData(contactForm);
  const xhr = new XMLHttpRequest();
  toggleSendButton();
  xhr.open("POST", contactForm.action, true);
  xhr.send(formData);
  xhr.addEventListener("load", whenContactFormSent);
}

If the form was successfully submitted, the script displays a success message, resets the form, displays the send message button again, and, after 5 seconds, removes the success message.

If the form was not successfully submitted, the script displays an error message and shows the send message button again so that the visitor can try again.

function whenContactFormSent(event) {
  const xhr = event.target;
  const contactForm = document.getElementById("contact-form");
  const responseContainer = document.getElementById("response-container");
  if (responseContainer === null) {
    return;
  }
  if (xhr.status === 200) {
    responseContainer.classList.add("success");
    responseContainer.innerHTML = "Message successfully sent.";
    contactForm.reset();
    toggleSendButton();
    setTimeout(resetResponseContainer, 5000, responseContainer);
  } else {
    let response = JSON.parse(xhr.response);
    responseContainer.classList.add("error");
    responseContainer.innerHTML = "Error: " + response.error;
    toggleSendButton();
  }
}

You now have a fully functional contact form on your Ghost blog.

If you have any questions or need help making tweaks, leave a comment below.

Featured image by Marko Pekić.