Set Up Newsletter Subscriptions with Brevo on Cloudflare Pages
A complete guide to implementing newsletter subscriptions using Brevo (formerly Sendinblue) on Cloudflare Pages with serverless functions.
๐ฏ Why We Chose Brevo for Newsletter Management
When building a newsletter subscription system for our Hugo blog, we needed a solution that was:
- Free for small projects with generous limits
- Easy to integrate via API
- Reliable and scalable
- GDPR compliant
- Feature-rich for future growth
Brevo vs Other Solutions
| Feature | Brevo | SendGrid | Mailchimp | EmailOctopus |
|---|---|---|---|---|
| Free Tier | 300 emails/day | 100 emails/day | 500 contacts | 2,500 contacts |
| Contacts Limit | Unlimited | Unlimited | 500 | 2,500 |
| API Access | โ Full | โ Full | โ Limited | โ Full |
| Automation | โ Yes | โ ๏ธ Paid | โ ๏ธ Paid | โ ๏ธ Limited |
| Templates | โ Yes | โ Yes | โ Yes | โ ๏ธ Basic |
| GDPR Compliant | โ Yes | โ Yes | โ Yes | โ Yes |
| Double Opt-in | โ Yes | โ Yes | โ Yes | โ Yes |
Winner: Brevo offers the best balance of features, generous free tier, and ease of integration.
๐๏ธ Architecture Overview
Our implementation uses:
- Hugo - Static site generator
- Cloudflare Pages - Hosting and serverless functions
- Brevo API - Newsletter management
- JavaScript - Serverless function runtime
โโโโโโโโโโโโโโโโโโโ
โ Hugo Site โ
โ (Static HTML) โ
โโโโโโโโโโฌโโโโโโโโโ
โ
โ User subscribes
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Cloudflare Function โ
โ /newsletter/subscribe โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ
โ POST to Brevo API
โ
โผ
โโโโโโโโโโโโโโโโโโโ
โ Brevo API โ
โ (v3/contacts) โ
โโโโโโโโโโโโโโโโโโโ๐ ๏ธ Implementation Steps
1. Set Up Brevo Account
- Sign up at brevo.com (free account)
- Navigate to Settings โ SMTP & API โ API Keys
- Click Generate a new API key
- Copy your API key (format:
xkeysib-xxxxx) - Create a contact list at Contacts โ Lists
- Note your List ID (usually shown in URL)
2. Project Structure
Create this directory structure in your Hugo project:
your-hugo-site/
โโโ functions/
โ โโโ newsletter/
โ โโโ subscribe.js # Cloudflare serverless function
โโโ themes/
โ โโโ your-theme/
โ โโโ layouts/
โ โโโ partials/
โ โโโ sidebar.html # Newsletter form (sidebar)
โ โโโ footer/
โ โโโ newsletter.html # Newsletter form (footer)
โโโ wrangler.toml # Cloudflare configuration (optional)3. Create Serverless Function
Create functions/newsletter/subscribe.js:
// Cloudflare Pages Function for Brevo Newsletter Subscription
export async function onRequestPost(context) {
try {
const { request, env } = context;
// Parse request body
let email;
try {
const body = await request.json();
email = body.email;
} catch (jsonError) {
return new Response(JSON.stringify({
message: 'Invalid request format'
}), {
status: 400,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
// Get environment variables
const BREVO_API_KEY = env.BREVO_API_KEY;
const BREVO_LIST_ID = env.BREVO_LIST_ID || 2;
if (!BREVO_API_KEY) {
return new Response(JSON.stringify({
message: 'Server configuration error'
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
// Validate email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email || !emailRegex.test(email)) {
return new Response(JSON.stringify({
message: 'Invalid email address'
}), {
status: 400,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
// Subscribe to Brevo
const brevoResponse = await fetch('https://api.brevo.com/v3/contacts', {
method: 'POST',
headers: {
'accept': 'application/json',
'api-key': BREVO_API_KEY,
'content-type': 'application/json'
},
body: JSON.stringify({
email: email,
listIds: [parseInt(BREVO_LIST_ID)],
updateEnabled: true,
attributes: {
SUBSCRIBED_FROM: 'Website',
SUBSCRIPTION_DATE: new Date().toISOString()
}
})
});
// Parse response safely
const responseText = await brevoResponse.text();
let data = {};
if (responseText) {
try {
data = JSON.parse(responseText);
} catch (parseError) {
console.error('Failed to parse Brevo response');
}
}
// Handle success
if (brevoResponse.ok || brevoResponse.status === 201) {
return new Response(JSON.stringify({
message: 'Successfully subscribed! Please check your email.',
success: true
}), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
// Handle duplicate email
if (brevoResponse.status === 400) {
if (data.code === 'duplicate_parameter' ||
(responseText && responseText.includes('already exist'))) {
return new Response(JSON.stringify({
message: 'This email is already subscribed.',
success: true
}), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
}
// Other errors
return new Response(JSON.stringify({
message: 'Subscription failed. Please try again later.',
error: data.message || 'Unknown error'
}), {
status: brevoResponse.status || 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
} catch (error) {
console.error('Subscription error:', error);
return new Response(JSON.stringify({
message: 'An error occurred. Please try again later.',
error: error.message
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
}
// Handle CORS preflight
export async function onRequestOptions() {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
}
});
}4. Frontend Newsletter Form
Add this to your theme’s sidebar or footer:
<form id="newsletter-form" class="newsletter-form">
<input
type="email"
name="email"
placeholder="Your email address"
required
id="newsletter-email">
<button type="submit">Subscribe</button>
<div id="newsletter-message" style="display: none;"></div>
</form>
<script>
(function() {
const form = document.getElementById('newsletter-form');
const emailInput = document.getElementById('newsletter-email');
const messageEl = document.getElementById('newsletter-message');
form.addEventListener('submit', async function(e) {
e.preventDefault();
const email = emailInput.value.trim();
// Validate email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email || !emailRegex.test(email)) {
showMessage('Please enter a valid email address', 'error');
return;
}
try {
const response = await fetch('/newsletter/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: email })
});
const data = await response.json();
if (response.ok || data.success) {
showMessage(data.message, 'success');
emailInput.value = '';
localStorage.setItem('newsletter_subscribed', 'true');
} else {
showMessage(data.message, 'error');
}
} catch (error) {
showMessage('Unable to connect. Please try again later.', 'error');
}
});
function showMessage(text, type) {
messageEl.textContent = text;
messageEl.className = 'message-' + type;
messageEl.style.display = 'block';
if (type === 'success') {
setTimeout(() => {
messageEl.style.display = 'none';
}, 5000);
}
}
})();
</script>5. Deploy to Cloudflare Pages
Option A: GitHub Integration (Recommended)
- Push to GitHub:
git add .
git commit -m "Add newsletter integration with Brevo"
git push origin mainConnect to Cloudflare:
- Go to Cloudflare Dashboard
- Navigate to Workers & Pages โ Create application โ Pages
- Click Connect to Git
- Select your repository
Configure Build Settings:
- Build command:
hugo --gc --minify - Build output directory:
public - Root directory:
/(default)
- Build command:
Add Environment Variables:
- Go to Settings โ Environment variables
- Add for Production:
BREVO_API_KEY=xkeysib-your-key-hereBREVO_LIST_ID=2(your list ID)
Deploy!
Option B: Direct Deploy via Wrangler CLI
# Install Wrangler
npm install -g wrangler
# Login to Cloudflare
wrangler login
# Build Hugo site
hugo --gc --minify
# Deploy
wrangler pages deploy public --project-name=your-site
# Add environment variables
wrangler pages secret put BREVO_API_KEY --project-name=your-site
wrangler pages secret put BREVO_LIST_ID --project-name=your-site6. Test Your Integration
After deployment:
- Visit your site and try subscribing
- Check Brevo dashboard at Contacts โ Lists to see new subscriber
- Test duplicate subscription with same email
- Monitor logs in Cloudflare Dashboard โ Functions
Test with curl:
curl -X POST https://yoursite.com/newsletter/subscribe \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com"}'Expected response:
{
"message": "Successfully subscribed! Please check your email.",
"success": true
}๐ Advanced Features
1. Enable Double Opt-in (GDPR)
In Brevo dashboard:
- Go to Contacts โ Settings
- Enable Double opt-in
- Customize confirmation email template
2. Create Welcome Email
- Go to Campaigns โ Templates
- Create new email template
- Set up automation at Automation โ Workflows
- Trigger: “Contact added to list”
3. Add Spam Protection
Add honeypot fields to your form:
<!-- Hidden fields that bots will fill -->
<input type="text" name="bot-field"
style="position: absolute; left: -5000px;"
aria-hidden="true"
tabindex="-1">Check in your serverless function:
const botField = formData.get('bot-field');
if (botField) {
return; // Silent fail for bots
}4. Track Subscription Source
Add UTM parameters to track where subscribers come from:
attributes: {
SUBSCRIBED_FROM: 'Website',
SOURCE_PAGE: window.location.pathname,
UTM_SOURCE: new URLSearchParams(window.location.search).get('utm_source') || 'direct'
}๐ป Local Development
For testing locally, create a Go server that mimics the Cloudflare function:
// server.go
package main
import (
"bytes"
"encoding/json"
"net/http"
"os"
)
func main() {
http.HandleFunc("/api/newsletter/subscribe", handleSubscribe)
http.Handle("/", http.FileServer(http.Dir("./public")))
http.ListenAndServe(":8080", nil)
}
func handleSubscribe(w http.ResponseWriter, r *http.Request) {
// Same logic as JavaScript function
// Uses os.Getenv("BREVO_API_KEY")
}Run with:
hugo --gc --minify
go run server.go๐ Monitoring & Analytics
View Metrics in Brevo
- Dashboard - Overall statistics
- Contacts โ Lists - List growth
- Reports - Email performance
Cloudflare Analytics
- Workers & Pages โ Your project โ Analytics
- View function invocations, errors, duration
- Monitor API costs (usually free under limits)
๐ฐ Cost Analysis
Brevo Free Tier
- โ 300 emails per day
- โ Unlimited contacts
- โ Full API access
- โ Email templates & automation
Cloudflare Pages
- โ Unlimited bandwidth
- โ 500 builds/month
- โ 100,000 function requests/day
- โ No credit card required
Total Monthly Cost: $0 ๐
๐ Security Best Practices
- Never commit API keys - Use environment variables only
- Enable CORS properly - Set appropriate origins in production
- Rate limiting - Consider implementing on high-traffic sites
- GDPR compliance - Enable double opt-in, provide unsubscribe
- Privacy Policy - Update to mention Brevo as data processor
๐ง Troubleshooting
“Server configuration error”
- Verify
BREVO_API_KEYis set in Cloudflare environment variables - Check API key is active in Brevo dashboard
“Unexpected end of JSON input”
- Usually occurs with duplicate emails
- Fixed by parsing response as text first, then JSON
Function not found (404)
- Ensure file is at
functions/newsletter/subscribe.js - Check Cloudflare deployment logs
- Verify function was built and deployed
CORS errors
- Add proper CORS headers to all responses
- Implement
onRequestOptionshandler
โ Conclusion
Brevo + Cloudflare Pages provides a powerful, free, and scalable newsletter solution for static sites. Key benefits:
- โ Zero cost for most sites
- โ Serverless architecture - no servers to manage
- โ Global edge deployment - fast worldwide
- โ GDPR compliant out of the box
- โ Scalable - handles traffic spikes automatically
The combination of Hugo’s static generation, Cloudflare’s edge functions, and Brevo’s email infrastructure creates a modern, performant newsletter system.
๐ Resources
Questions? Leave a comment below. Happy coding!!!
