Using StaticForms with Nuxt.js: A Complete Guide
Are you building a Nuxt.js application and need a simple way to handle form submissions without setting up a backend server? StaticForms.xyz is the perfect solution for collecting form data from your static sites and receiving submissions directly in your inbox.
In this guide, we'll explore how to integrate the StaticForms service with your Nuxt.js application, including both basic implementation and enhanced security with reCAPTCHA. If you're using Next.js instead, check out our Next.js integration guide.
What is StaticForms?
StaticForms is a form-to-email service designed specifically for static websites. It allows you to receive form submissions via email without writing any server-side code. This is particularly useful for:
- Contact forms
- Feedback forms
- Newsletter signups
- Simple data collection
The service is free to use (with some limitations) and provides a straightforward API for integrating forms into your static website.
Getting Started with StaticForms
Before implementing forms in your Nuxt.js app, you'll need to:
- Visit staticforms.xyz
- Register for a free account
- Obtain your unique API key
This API key will be used to connect your forms to your StaticForms account, ensuring submissions are forwarded to your email address.
Basic Integration in Nuxt.js
Let's start by implementing a simple contact form without reCAPTCHA. This approach is quick to set up but offers less protection against spam.
Create a Client-Side Component for Your Form
<!-- components/ContactForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const formData = ref({
name: '',
email: '',
message: '',
subject: 'Contact Form Submission', // Default subject
honeypot: '', // Anti-spam field
replyTo: '@', // This will set replyTo to the email provided
apiKey: 'YOUR_API_KEY' // Replace with your actual apiKey
})
const response = ref({
type: '',
message: ''
})
const isSubmitting = ref(false)
const handleSubmit = async () => {
isSubmitting.value = true
try {
const res = await fetch('https://api.staticforms.xyz/submit', {
method: 'POST',
body: JSON.stringify(formData.value),
headers: { 'Content-Type': 'application/json' }
})
const json = await res.json()
if (json.success) {
response.value = {
type: 'success',
message: 'Thank you for your message! We\'ll get back to you soon.'
}
// Reset form fields
formData.value.name = ''
formData.value.email = ''
formData.value.message = ''
} else {
response.value = {
type: 'error',
message: json.message || 'An error occurred. Please try again.'
}
}
} catch (error) {
console.error('Error submitting form:', error)
response.value = {
type: 'error',
message: 'An error occurred while submitting the form. Please try again.'
}
} finally {
isSubmitting.value = false
}
}
</script>
<template>
<div class="w-full max-w-md mx-auto">
<div v-if="response.type === 'success'" class="p-4 mb-4 text-sm rounded-md bg-green-50 text-green-700">
{{ response.message }}
</div>
<div v-else-if="response.type === 'error'" class="p-4 mb-4 text-sm rounded-md bg-red-50 text-red-700">
{{ response.message }}
</div>
<form @submit.prevent="handleSubmit" class="space-y-4">
<!-- Honeypot field to prevent spam -->
<input
type="text"
v-model="formData.honeypot"
style="display: none"
/>
<div>
<label for="name" class="block text-sm font-medium mb-1">Name</label>
<input
id="name"
v-model="formData.name"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<label for="email" class="block text-sm font-medium mb-1">Email</label>
<input
id="email"
v-model="formData.email"
type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<label for="message" class="block text-sm font-medium mb-1">Message</label>
<textarea
id="message"
v-model="formData.message"
rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded-md"
required
></textarea>
</div>
<button
type="submit"
:disabled="isSubmitting"
class="w-full px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{{ isSubmitting ? 'Sending...' : 'Send Message' }}
</button>
</form>
</div>
</template>
Use the Form Component in Your Page
<!-- pages/contact.vue -->
<script setup lang="ts">
definePageMeta({
title: 'Contact Us',
description: 'Get in touch with our team'
})
</script>
<template>
<div class="container mx-auto py-12">
<h1 class="text-3xl font-bold mb-8 text-center">Contact Us</h1>
<ContactForm />
</div>
</template>
Enhanced Integration with reCAPTCHA
For better protection against spam and bot submissions, we can add Google reCAPTCHA to our form. First, we need to install the Vue reCAPTCHA package:
npm install vue-recaptcha@latest
Contact Form with reCAPTCHA
<!-- components/ContactFormWithRecaptcha.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import VueRecaptcha from 'vue-recaptcha'
// Add reCAPTCHA script to the document
onMounted(() => {
if (!document.getElementById('recaptcha-script')) {
const script = document.createElement('script')
script.id = 'recaptcha-script'
script.src = 'https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoaded&render=explicit'
script.async = true
script.defer = true
document.head.appendChild(script)
}
})
const formData = ref({
name: '',
email: '',
message: '',
subject: 'Contact Form Submission',
honeypot: '',
replyTo: '@',
apiKey: 'YOUR_API_KEY', // Replace with your actual apiKey
'g-recaptcha-response': '' // Will be filled by the reCAPTCHA
})
const response = ref({
type: '',
message: ''
})
const isSubmitting = ref(false)
const recaptchaInstance = ref(null)
const onRecaptchaVerified = (token: string) => {
formData.value['g-recaptcha-response'] = token
}
const onRecaptchaExpired = () => {
formData.value['g-recaptcha-response'] = ''
}
const resetRecaptcha = () => {
if (recaptchaInstance.value) {
recaptchaInstance.value.reset()
}
}
const handleSubmit = async () => {
if (!formData.value['g-recaptcha-response']) {
response.value = {
type: 'error',
message: 'Please complete the reCAPTCHA verification.'
}
return
}
isSubmitting.value = true
try {
const res = await fetch('https://api.staticforms.xyz/submit', {
method: 'POST',
body: JSON.stringify(formData.value),
headers: { 'Content-Type': 'application/json' }
})
const json = await res.json()
if (json.success) {
response.value = {
type: 'success',
message: 'Thank you for your message! We\'ll get back to you soon.'
}
// Reset form fields
formData.value.name = ''
formData.value.email = ''
formData.value.message = ''
formData.value['g-recaptcha-response'] = ''
resetRecaptcha()
} else {
response.value = {
type: 'error',
message: json.message || 'An error occurred. Please try again.'
}
resetRecaptcha()
}
} catch (error) {
console.error('Error submitting form:', error)
response.value = {
type: 'error',
message: 'An error occurred while submitting the form. Please try again.'
}
resetRecaptcha()
} finally {
isSubmitting.value = false
}
}
</script>
<template>
<div class="w-full max-w-md mx-auto">
<div v-if="response.type === 'success'" class="p-4 mb-4 text-sm rounded-md bg-green-50 text-green-700">
{{ response.message }}
</div>
<div v-else-if="response.type === 'error'" class="p-4 mb-4 text-sm rounded-md bg-red-50 text-red-700">
{{ response.message }}
</div>
<form @submit.prevent="handleSubmit" class="space-y-4">
<!-- Honeypot field to prevent spam -->
<input
type="text"
v-model="formData.honeypot"
style="display: none"
/>
<div>
<label for="name" class="block text-sm font-medium mb-1">Name</label>
<input
id="name"
v-model="formData.name"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<label for="email" class="block text-sm font-medium mb-1">Email</label>
<input
id="email"
v-model="formData.email"
type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<label for="message" class="block text-sm font-medium mb-1">Message</label>
<textarea
id="message"
v-model="formData.message"
rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded-md"
required
></textarea>
</div>
<div class="mb-4">
<vue-recaptcha
ref="recaptchaInstance"
:sitekey="'YOUR_RECAPTCHA_SITE_KEY'"
@verify="onRecaptchaVerified"
@expired="onRecaptchaExpired"
/>
</div>
<button
type="submit"
:disabled="isSubmitting"
class="w-full px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{{ isSubmitting ? 'Sending...' : 'Send Message' }}
</button>
</form>
</div>
</template>
Using the reCAPTCHA Form Component
<!-- pages/contact-secure.vue -->
<script setup lang="ts">
definePageMeta({
title: 'Secure Contact Form',
description: 'Contact us using our secure form with reCAPTCHA protection'
})
</script>
<template>
<div class="container mx-auto py-12">
<h1 class="text-3xl font-bold mb-8 text-center">Secure Contact Form</h1>
<p class="text-center mb-8">This form is protected by reCAPTCHA to prevent spam submissions.</p>
<ContactFormWithRecaptcha />
</div>
</template>
Using Nuxt.js Composition API for Form Reusability
For larger applications, you might want to extract the form logic into a composable function for better reusability:
// composables/useStaticForm.ts
import { ref } from 'vue'
export default function useStaticForm(initialData: any = {}) {
const formData = ref({
name: '',
email: '',
message: '',
subject: 'Contact Form Submission',
honeypot: '',
replyTo: '@',
apiKey: 'YOUR_API_KEY', // Replace with your actual apiKey
...initialData
})
const response = ref({
type: '',
message: ''
})
const isSubmitting = ref(false)
const submitForm = async () => {
isSubmitting.value = true
try {
const res = await fetch('https://api.staticforms.xyz/submit', {
method: 'POST',
body: JSON.stringify(formData.value),
headers: { 'Content-Type': 'application/json' }
})
const json = await res.json()
if (json.success) {
response.value = {
type: 'success',
message: 'Thank you for your message! We\'ll get back to you soon.'
}
// Reset certain fields but keep configuration
const { apiKey, subject, replyTo, honeypot } = formData.value
formData.value = {
name: '',
email: '',
message: '',
subject,
honeypot,
replyTo,
apiKey
}
return true
} else {
response.value = {
type: 'error',
message: json.message || 'An error occurred. Please try again.'
}
return false
}
} catch (error) {
console.error('Error submitting form:', error)
response.value = {
type: 'error',
message: 'An error occurred while submitting the form. Please try again.'
}
return false
} finally {
isSubmitting.value = false
}
}
const resetForm = () => {
const { apiKey, subject, replyTo, honeypot } = formData.value
formData.value = {
name: '',
email: '',
message: '',
subject,
honeypot,
replyTo,
apiKey
}
response.value = { type: '', message: '' }
}
return {
formData,
response,
isSubmitting,
submitForm,
resetForm
}
}
You can then use this composable in any component:
<!-- components/NewsletterSignup.vue -->
<script setup lang="ts">
import useStaticForm from '~/composables/useStaticForm'
const { formData, response, isSubmitting, submitForm } = useStaticForm({
subject: 'Newsletter Signup',
apiKey: 'YOUR_API_KEY' // Replace with your actual apiKey
})
const handleSubmit = async () => {
await submitForm()
}
</script>
<template>
<div class="newsletter-form">
<div v-if="response.type === 'success'" class="p-4 mb-4 text-sm rounded-md bg-green-50 text-green-700">
{{ response.message }}
</div>
<div v-else-if="response.type === 'error'" class="p-4 mb-4 text-sm rounded-md bg-red-50 text-red-700">
{{ response.message }}
</div>
<form @submit.prevent="handleSubmit" class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium mb-1">Name</label>
<input
id="name"
v-model="formData.name"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<label for="email" class="block text-sm font-medium mb-1">Email</label>
<input
id="email"
v-model="formData.email"
type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<button
type="submit"
:disabled="isSubmitting"
class="w-full px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{{ isSubmitting ? 'Submitting...' : 'Subscribe to Newsletter' }}
</button>
</form>
</div>
</template>
Environment Variables in Nuxt.js
For better security, you should store your StaticForms API key as an environment variable:
- Create a
.env
file in your project root:
NUXT_PUBLIC_STATICFORMS_KEY=your_api_key_here
- Access it in your components or composables:
const apiKey = process.env.NUXT_PUBLIC_STATICFORMS_KEY
Conclusion
Integrating StaticForms with Nuxt.js provides a simple, effective solution for handling form submissions in static sites without setting up a backend server. Whether you use the basic implementation or enhance it with reCAPTCHA for extra security, StaticForms makes the process straightforward.
For more information about StaticForms, check out our getting started guide or explore other integration examples for different frameworks, such as our Next.js integration tutorial.
Remember that the free tier has some limitations, but it's perfect for most personal projects and small business websites. For larger projects with higher submission volumes, consider upgrading to a paid plan.
Happy coding with Nuxt.js and StaticForms!