Learn how to build a professional contact form in Statamic with AWS SES email delivery, including validation, spam protection, and custom templates.
Setting up a reliable contact form is crucial for any website. In this comprehensive guide, I'll walk you through creating a professional contact form in Statamic that sends emails via AWS Simple Email Service (SES), complete with custom templates and proper formatting.
Why AWS SES for Email Delivery?
Before diving into the implementation, let's understand why AWS SES is an excellent choice for transactional emails:
- Reliability: 99.9% uptime SLA with AWS's infrastructure
- Cost-effective: $0.10 per 1,000 emails (significantly cheaper than alternatives)
- Scalability: Handles everything from small sites to enterprise applications
- Deliverability: Built-in reputation management and compliance tools
- Integration: Works seamlessly with other AWS services
Prerequisites
Before starting, ensure you have:
- A Statamic installation (v4.x or higher)
- An AWS account with SES configured
- Verified domain or email addresses in SES
- Laravel's mail configuration access
Step 1: Configure AWS SES
First, set up your AWS SES credentials. Log into the AWS Console and navigate to SES:
- Verify your domain: Go to Verified identities → Create identity → Choose Domain
- Generate SMTP credentials: Account dashboard → SMTP settings → Create SMTP credentials
- Note your SMTP endpoint: Usually
email-smtp.[region].amazonaws.com
- Move out of sandbox: For production, request production access
Step 2: Configure Laravel Mail Settings
Update your .env
file with AWS SES credentials:
# AWS SES SMTP Configuration
MAIL_MAILER=smtp
MAIL_HOST=email-smtp.us-east-1.amazonaws.com
MAIL_PORT=587
MAIL_USERNAME=your-ses-smtp-username
MAIL_PASSWORD=your-ses-smtp-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="noreply@yourdomain.com"
MAIL_FROM_NAME="${APP_NAME}"
MAIL_TO_ADDRESS="contact@yourdomain.com"
Update config/mail.php
to include a custom recipient:
return [
// ... existing configuration
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
// Add custom to address for contact forms
'to_address' => env('MAIL_TO_ADDRESS', 'contact@example.com'),
];
Step 3: Create the Contact Form Blueprint
Create a form YAML configuration at resources/forms/contact.yaml
:
title: 'Contact Form'
handle: contact
honeypot: winnie
fields:
-
handle: name
field:
display: Name
type: text
required: true
validate:
- required
-
handle: email
field:
display: Email
type: text
input_type: email
required: true
validate:
- required
- email
-
handle: subject
field:
display: Subject
type: text
required: true
validate:
- required
-
handle: message
field:
display: Message
type: textarea
required: true
validate:
- required
- min:10
character_limit: 5000
rows: 8
-
handle: phone
field:
display: 'Phone Number (Optional)'
type: text
required: false
store: true
email:
-
to: '{{ config:mail.to_address }}'
from: '{{ config:mail.from_address }}'
reply_to: '{{ email }}'
subject: 'Contact Form Submission: {{ subject }}'
html: emails/contact
text: emails/contact_text
Key features of this configuration:
- Honeypot field:
winnie
acts as spam protection - Field validation: Required fields with appropriate rules
- Email configuration: Dynamic values from config
- Template paths: Separate HTML and text templates
Step 4: Create the Contact Form Template
Create the form view at resources/views/contact.antlers.html
:
<section class="bg-white py-16">
<div class="container max-w-3xl mx-auto px-4">
<h1 class="text-4xl font-bold mb-8">Contact Us</h1>
{{ form:contact }}
{{ if success }}
<div class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded mb-6">
Your message has been sent successfully!
</div>
{{ else }}
{{ if errors }}
<div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded mb-6">
<ul class="list-disc list-inside">
{{ errors }}
<li>{{ value }}</li>
{{ /errors }}
</ul>
</div>
{{ /if }}
<div class="space-y-6">
<div class="grid md:grid-cols-2 gap-6">
<div>
<label for="name" class="block text-sm font-medium mb-2">
Name <span class="text-red-500">*</span>
</label>
<input type="text" name="name" id="name" value="{{ old:name }}"
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<div>
<label for="email" class="block text-sm font-medium mb-2">
Email <span class="text-red-500">*</span>
</label>
<input type="email" name="email" id="email" value="{{ old:email }}"
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
</div>
<div class="grid md:grid-cols-2 gap-6">
<div>
<label for="subject" class="block text-sm font-medium mb-2">
Subject <span class="text-red-500">*</span>
</label>
<input type="text" name="subject" id="subject" value="{{ old:subject }}"
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<div>
<label for="phone" class="block text-sm font-medium mb-2">
Phone Number <span class="text-gray-500">(Optional)</span>
</label>
<input type="tel" name="phone" id="phone" value="{{ old:phone }}"
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div>
<label for="message" class="block text-sm font-medium mb-2">
Message <span class="text-red-500">*</span>
</label>
<textarea name="message" id="message" rows="8"
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required>{{ old:message }}</textarea>
<p class="text-sm text-gray-500 mt-1">Minimum 10 characters</p>
</div>
<!-- Honeypot field (hidden from users) -->
<div class="hidden">
<input type="text" name="{{ honeypot }}" tabindex="-1">
</div>
<button type="submit"
class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors">
Send Message
</button>
</div>
{{ /if }}
{{ /form:contact }}
</div>
</section>
Step 5: Create Email Templates
HTML Email Template
Create resources/views/emails/contact.antlers.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact Form Submission</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: #f9fafb;
border-radius: 8px;
padding: 30px;
}
h1 {
color: #1f2937;
font-size: 24px;
margin-bottom: 20px;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 10px;
}
.field {
margin-bottom: 15px;
}
.label {
font-weight: 600;
color: #6b7280;
display: block;
margin-bottom: 5px;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.value {
color: #1f2937;
background-color: white;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid #e5e7eb;
}
.message-content {
color: #1f2937;
line-height: 1.6;
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
font-size: 12px;
color: #9ca3af;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1>New Contact Form Submission</h1>
{{ fields }}
{{ if value && fieldtype != 'spacer' }}
<div class="field">
<span class="label">{{ display }}</span>
<div class="value">
{{ if handle == 'email' }}
<a href="mailto:{{ value }}">{{ value }}</a>
{{ elseif handle == 'message' }}
<div class="message-content">
{{ value | nl2br | raw }}
</div>
{{ else }}
{{ value }}
{{ /if }}
</div>
</div>
{{ /if }}
{{ /fields }}
<div class="footer">
<p>This email was sent from the contact form on {{ config:app.url }}</p>
<p>Submitted on {{ now format="F j, Y \a\t g:i A" }}</p>
</div>
</div>
</body>
</html>
Plain Text Email Template
Create resources/views/emails/contact_text.antlers.html
:
NEW CONTACT FORM SUBMISSION
============================
{{ fields }}
{{ if value && fieldtype != 'spacer' }}
{{ if handle != 'message' }}{{ display }}: {{ value }}
{{ /if }}
{{ /if }}
{{ /fields }}
Message:
--------
{{ fields }}
{{ if handle == 'message' }}{{ value }}{{ /if }}
{{ /fields }}
============================
This email was sent from the contact form on {{ config:app.url }}
Submitted on {{ now format="F j, Y \a\t g:i A" }}
Step 6: Test Your Configuration
Test your email configuration using Laravel's tinker:
php artisan tinker
Mail::raw('Test email', function($message) {
$message->to(config('mail.to_address'))
->subject('Test Email from Statamic');
});
If successful, you should receive the test email.
Step 7: Advanced Features
Adding File Attachments
To allow file uploads, add to your form YAML:
-
handle: attachment
field:
display: Attachment
type: files
max_files: 1
max_filesize: 5
allowed_extensions:
- pdf
- doc
- docx
And update the email configuration:
email:
-
# ... other config
attachments: true
Rate Limiting
Protect against spam by adding rate limiting in your controller:
use Illuminate\Support\Facades\RateLimiter;
public function submit(Request $request)
{
$key = 'contact-form:' . $request->ip();
if (RateLimiter::tooManyAttempts($key, 3)) {
return back()->with('error', 'Too many submissions. Please try again later.');
}
RateLimiter::hit($key, 3600); // 3 attempts per hour
// Process form...
}
Custom Validation Messages
Customize validation messages in your form:
validate:
- required
- 'min:10'
messages:
required: 'The :attribute field is required.'
min: 'Your message must be at least :min characters.'
Troubleshooting Common Issues
Emails Not Sending
- Check credentials: Verify your AWS SES SMTP credentials
- Check logs: Review
storage/logs/laravel.log
- Test with log driver: Set
MAIL_MAILER=log
temporarily - Verify domain: Ensure your domain is verified in SES
Message Field Formatting Issues
If message paragraphs aren't displaying correctly:
- Use
nl2br
filter:{{ value | nl2br | raw }}
- For plain text: Preserve line breaks naturally
- Consider using a textarea instead of rich text editor
Duplicate Configuration
Clean up .env
file to avoid duplicate mail settings:
# Remove duplicates, keep only one set of MAIL_ variables
MAIL_MAILER=smtp
MAIL_HOST=email-smtp.us-east-1.amazonaws.com
# ... rest of configuration
Security Best Practices
- Never commit credentials: Keep
.env
out of version control - Use environment variables: Don't hardcode credentials
- Implement CSRF protection: Statamic handles this automatically
- Add honeypot fields: Prevents bot submissions
- Rate limit submissions: Prevent abuse
- Validate all inputs: Use Statamic's validation rules
- Sanitize output: Use proper escaping in templates
Conclusion
You now have a fully functional contact form in Statamic that sends professional emails via AWS SES. This setup provides reliability, scalability, and cost-effectiveness for your transactional emails.
The key takeaways:
- AWS SES offers excellent deliverability at low cost
- Statamic's form builder makes configuration straightforward
- Custom email templates enhance professionalism
- Proper validation and security measures are essential
Remember to test thoroughly in development before deploying to production, and always monitor your email delivery rates through the AWS SES console.
Additional Resources
Share this article
David Childs
Consulting Systems Engineer with over 10 years of experience building scalable infrastructure and helping organizations optimize their technology stack.