Skip to content

Adding a Contact Form to Your VitePress Site

Creating the Form

We're going to create a Vue component for our contact form.

This component should be placed in .vitepress/theme/components/ContactForm.vue:

vue
<script setup>
import { ref, computed } from 'vue'
import { useData } from 'vitepress'

const formData = ref({
  name: '',
  email: '',
  subject: '',
  message: ''
})

const isSubmitting = ref(false)
const isSuccess = ref(false)
const errorMessage = ref('')

const isValidEmail = (email) => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return emailRegex.test(email)
}

const isFormValid = computed(() => {
  return formData.value.name.trim() !== '' &&
    formData.value.email.trim() !== '' &&
    isValidEmail(formData.value.email) &&
    formData.value.subject.trim() !== '' &&
    formData.value.message.trim() !== ''
})

const submitForm = async () => {
  errorMessage.value = ''
  if (!formData.value.name.trim()) {
    errorMessage.value = 'Name is required'
    return
  }
  if (!formData.value.email.trim()) {
    errorMessage.value = 'Email address is required'
    return
  }
  if (!isValidEmail(formData.value.email)) {
    errorMessage.value = 'Email address is invalid'
    return
  }
  if (!formData.value.subject.trim()) {
    errorMessage.value = 'Message subject is required'
    return
  }
  if (!formData.value.message.trim()) {
    errorMessage.value = 'Message is required'
    return
  }
  isSubmitting.value = true
  try {
    const formDataToSend = new FormData()
    formDataToSend.append('name', formData.value.name)
    formDataToSend.append('email', formData.value.email)
    formDataToSend.append('subject', formData.value.subject)
    formDataToSend.append('message', formData.value.message)
    const response = await fetch('/sendmail.php', {
      method: 'POST',
      body: formDataToSend
    })
    const message = await response.text()
    if (response.ok) {
      isSuccess.value = true
      errorMessage.value = ''
    } else {
      errorMessage.value = message || 'An error occurred. Please try again.'
    }
  } catch (error) {
    errorMessage.value = 'Network error'
  } finally {
    isSubmitting.value = false
  }
}
</script>

<template>
  <div class="contact-form">
    <form @submit.prevent="submitForm">
      <div class="form-group">
        <label for="name">Name <span class="required">*</span></label>
        <input
          id="name"
          v-model="formData.name"
          type="text"
          :disabled="isSuccess"
          placeholder="Your name"
        />
      </div>
      <div class="form-group">
        <label for="email">Email <span class="required">*</span></label>
        <input
          id="email"
          v-model="formData.email"
          type="email"
          :disabled="isSuccess"
          placeholder="Your email address"
        />
      </div>
      <div class="form-group">
        <label for="subject">Subject <span class="required">*</span></label>
        <input
          id="subject"
          v-model="formData.subject"
          type="text"
          :disabled="isSuccess"
          placeholder="Message subject"
        />
      </div>
      <div class="form-group">
        <label for="message">Message <span class="required">*</span></label>
        <textarea
          id="message"
          v-model="formData.message"
          :disabled="isSuccess"
          rows="5"
          placeholder="Your message"
        ></textarea>
      </div>
      <!-- Error message area -->
      <div v-if="errorMessage" class="error-message">
        {{ errorMessage }}
      </div>
      <!-- Success message area -->
      <div v-if="isSuccess" class="success-message">
        Thank you! Your message has been sent successfully.
      </div>
      <button
        v-if="!isSuccess"
        type="submit"
        :disabled="!isFormValid || isSubmitting || isSuccess"
        class="submit-button"
      >
        {{ isSubmitting ? 'Sending...' : 'Send message' }}
      </button>
    </form>
  </div>
</template>

<style scoped>
.contact-form {
  max-width: 600px;
  margin: 2rem auto;
}
.form-group {
  margin-bottom: 1.5rem;
}
label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
  color: var(--vp-c-text-1);
}
.required {
  color: #e74c3c;
}
input,
textarea {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid var(--vp-c-divider);
  border-radius: 4px;
  font-size: 1rem;
  font-family: inherit;
  background-color: var(--vp-c-bg);
  color: var(--vp-c-text-1);
  transition: border-color 0.2s;
}
input:focus,
textarea:focus {
  outline: none;
  border-color: var(--vp-c-brand);
}
input:disabled,
textarea:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  background-color: var(--vp-c-bg-soft);
}
textarea {
  resize: vertical;
}
.error-message {
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
  background-color: #fee;
  border-left: 4px solid #e74c3c;
  color: #c0392b;
  border-radius: 4px;
}
.success-message {
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
  background-color: #d4edda;
  border-left: 4px solid #28a745;
  color: #155724;
  border-radius: 4px;
  font-weight: 500;
}
.submit-button {
  width: 100%;
  padding: 0.75rem 1.5rem;
  background-color: var(--vp-c-brand-1);
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.2s;
}
.submit-button:hover:not(:disabled) {
  background-color: var(--vp-c-brand-2);
}
.submit-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
</style>

Integrating the Component on Your Site

Edit .vitepress/theme/index.js to add:

javascript
  enhanceApp({ app }) {
    app.component('ContactForm', ContactForm)
  }

image Note: If you already have enhanceApp in this file, for example in the form enhanceApp({ router }), complete it by integrating the above entry and adding app as follows: enhanceApp({ app, router }).

Now you can integrate the component into any Markdown page like this:

markdown
# Contact

Write to me using the form below:

<ContactForm />

Server Side

For the actual sending of emails, we'll use the PHP script below.

In the header, you need to configure your contact information by modifying CONTACT_INFO:

  • to_name and to_email: your name and email address where you want to receive messages from your visitors,
  • cc_name and cc_email: name and email address for carbon copy of messages (optional),
  • site_email: the address from which your site sends you messages (you can use a non-existent address, such as no-reply@your-domain.tld).

image Note: Your site will send you visitor messages on its behalf, using the site_email that you configured. However, it configures a Reply-To field in emails, which will allow you to respond to messages seamlessly.

This script should be placed in public/sendmail.php:

php
<?php

$CONTACT_INFO['to_email'] = 'me@example.com';
$CONTACT_INFO['to_name'] = 'My name';
$CONTACT_INFO['cc_email'] = '';
$CONTACT_INFO['cc_name'] = '';
$CONTACT_INFO['site_email'] = 'My website <no-reply@example.com>';

header("Content-Type: text/plain");

$from_name = trim($_POST['name']);
$from_email = trim($_POST['email']);
$subject = trim($_POST['subject']);
$message = trim($_POST['message']);

$regex_head = '/[\n\r]/';
$regex_mail = '/^[-+.\w]{1,64}@[-.\w]{1,64}\.[-.\w]{2,6}$/i';

if (empty($from_name)
  || empty($from_email)
  || empty($subject)
  || empty($message)) {
  http_response_code(400);
  echo 'Required fields must be filled';
} else if (!preg_match($regex_mail, $from_email)) {
  http_response_code(400);
  echo 'Invalid email address';
} else if (preg_match($regex_head, $from_name)
  || preg_match($regex_head, $from_email)
  || preg_match($regex_head, $subject)) {
  http_response_code(400);
  echo 'Malformed data';
} else {
  $to = $CONTACT_INFO['to_name']
    .'<'.$CONTACT_INFO['to_email'].'>';
  $headers = 'From: '.$CONTACT_INFO['site_email']."\r\n";
  $headers .= 'Reply-To: '.$from_name
    .' <'.$from_email.'>'."\r\n";
  if (!empty($CONTACT_INFO['cc_email'])) {
    $headers .= 'Cc: '.$CONTACT_INFO['cc_name']
      .' <'.$CONTACT_INFO['cc_email'].'>'."\r\n";
  }
  $headers .= "MIME-Version: 1.0\r\n";
  $headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
  $headers .= "Content-Transfer-Encoding: base64\r\n";

  $contents = vsprintf('
%s

--------
%s <%s>', array(
    $message, $from_name, $from_email
  ));
  $contents = chunk_split(base64_encode($contents));
  if(!mail($to, $subject, $contents, $headers)) {
    http_response_code(500);
    echo 'Unable to send message';
  }
}

?>

You can modify the Vue component as well as the PHP script if needed to add anti-spam protection.

License

All code presented on this page is licensed under CC0 1.0 Universal.

You can copy, modify, distribute and use it, even for commercial purposes, without asking permission.