Skip to content

Securing a Newsletter Subscription Form on a Static MkDocs Site

Adding a newsletter subscription form to a static site seems straightforward—until you consider security implications. In this post, I'll share how I implemented a secure newsletter subscription for my MkDocs-based blog using Cloudflare Turnstile and a serverless API.

For the Finfluencers Trade blog, built with MkDocs (a static site generator), I wanted a simple way for readers to subscribe to a newsletter for updates. Static sites can't run server-side code directly, so handling form submissions requires a different approach than dynamic sites (like WordPress or Node.js apps). I needed a solution that was secure, reliable, respected user privacy, and didn't require me to manage a backend just for email signups.

System Architecture Overview

First, let's understand the components involved:

  • MkDocs: A static site generator that creates our blog and documentation site
  • Newsletter Form: A form in the site footer where users can submit their email address to receive newsletters
  • ConvertKit (Kit): Email marketing platform used to manage subscribers and send newsletters
  • Cloudflare Turnstile: Bot protection service that verifies human users
  • Serverless API: Backend service that securely connects the form to ConvertKit

The flow works as follows:

  1. User enters their email in the form
  2. Cloudflare Turnstile verifies the user is human (invisibly)
  3. Our serverless API receives the form submission with the verification token
  4. The API validates the token and forwards the email to ConvertKit
  5. ConvertKit adds the subscriber to our newsletter list

The Challenges

Static sites like those built with MkDocs face three major challenges when implementing newsletter subscriptions:

  1. API Key Security: You can't securely store API keys in client-side code
  2. Bot Protection: Without proper safeguards, bots can flood your subscription list with fake emails
  3. CORS Configuration: Supporting both production and development environments securely

Let's explore how I solved these problems with a clean, secure implementation.

Challenge 1: Keeping API Keys Secure

The initial temptation is to make API calls directly from the browser using your ConvertKit API key:

// DON'T DO THIS - Insecure approach
fetch(`https://api.convertkit.com/v3/forms/${FORM_ID}/subscribe`, {
  method: 'POST',
  body: JSON.stringify({
    api_key: 'your_api_key_exposed_to_all_users', // 🚨 SECURITY RISK!
    email: userEmail
  })
});

This exposes your API key to anyone who inspects your site's JavaScript, potentially allowing malicious users to abuse your ConvertKit account.

Challenge 2: Bot Protection with Cloudflare Turnstile

Even with server-side API calls, you still need to prevent automated form submissions. Traditional CAPTCHAs frustrate users, while basic honeypots can be bypassed by sophisticated bots.

Why Cloudflare Turnstile?

Cloudflare Turnstile is a CAPTCHA alternative hosted by Cloudflare that offers several advantages:

Pros:

  • Invisible verification: No puzzles for legitimate users to solve
  • Advanced bot detection: Uses multiple signals to identify bots
  • Privacy-focused: Doesn't track users across sites like reCAPTCHA
  • Free tier: Generous free allowance (up to 500K challenges per month)
  • Easy integration: Simple JavaScript API and verification endpoint

Cons:

  • Dependency on Cloudflare: Adds an external service dependency
  • Limited customization: Fewer visual customization options than some alternatives

For my needs, the invisible verification and privacy benefits made Turnstile the ideal choice.

Challenge 3: CORS Configuration for Multiple Environments

Properly configuring CORS is essential when your frontend and API live on different domains or when you need to support both development and production environments.

The Solution: Serverless API + Cloudflare Turnstile

I built a solution combining:

  1. A serverless API endpoint that keeps credentials secure
  2. Cloudflare Turnstile for invisible bot protection
  3. Proper CORS configuration to handle multiple environments

Step 1: Setting Up Cloudflare Turnstile

First, I created a Turnstile widget and obtained site and secret keys:

# In mkdocs.yml - Only the site key is public
extra:
  turnstile_site_key: your_site_key_here

And added the widget to my subscription form:

<!-- In footer.html -->
<div class="cf-turnstile" 
     data-sitekey="{{ config.extra.turnstile_site_key }}" 
     data-theme="dark">
</div>

Step 2: Creating a Serverless API

I implemented a secure API endpoint that handles:

  • Turnstile token validation
  • CORS for multiple environments
  • Authenticated requests to ConvertKit
// api/subscribe.js (simplified)
export default async function handler(req, res) {
  // Handle CORS for multiple environments
  const allowedOrigins = (process.env.ALLOWED_ORIGIN || 'https://finfluencers.trade').split(',');
  const requestOrigin = req.headers.origin;

  if (allowedOrigins.includes(requestOrigin)) {
    res.setHeader('Access-Control-Allow-Origin', requestOrigin);
    res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  }

  // Handle preflight
  if (req.method === 'OPTIONS') {
    return res.status(200).end();
  }

  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { email, token } = req.body;

  // Verify Turnstile token
  const turnstileResponse = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      secret: process.env.TURNSTILE_SECRET_KEY,
      response: token
    })
  });

  const turnstileData = await turnstileResponse.json();
  if (!turnstileData.success) {
    return res.status(400).json({ error: 'Bot verification failed' });
  }

  // Make request to ConvertKit API
  const response = await fetch(`https://api.convertkit.com/v3/forms/${process.env.CONVERTKIT_FORM_ID}/subscribe`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      api_key: process.env.CONVERTKIT_API_KEY,
      email,
      tags: process.env.CONVERTKIT_TAG_ID ? [process.env.CONVERTKIT_TAG_ID] : []
    })
  });

  const responseData = await response.json();
  return res.status(response.ok ? 200 : 500).json(responseData);
}

Step 3: Frontend JavaScript for the Subscription Form

On the client side, I implemented JavaScript to collect the email, get the Turnstile token, and make the API request:

// docs/javascripts/subscription.js (simplified)
async function subscribeEmail(email) {
  // Get the Turnstile token
  const token = await turnstile.getResponse();

  // Submit to our secure API endpoint
  const response = await fetch('/api/subscribe', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email, token })
  });

  const data = await response.json();

  if (response.ok) {
    // Store email in localStorage to hide form on future visits
    localStorage.setItem('finfluencers_subscribed_email', email);
    showNotification('Thanks for subscribing!');
    hideFooter();
  } else {
    showNotification(data.error || 'Subscription failed');
    turnstile.reset();
  }
}

Building Secure Systems: Reflections and Results

This implementation provides significant advantages:

  1. Enhanced Security: API keys remain secure on the server, never exposed in client-side code (as demonstrated in the serverless API implementation)
  2. Effective Bot Protection: Cloudflare Turnstile prevents spam without frustrating legitimate users (shown in our token verification process)
  3. Development Flexibility: The CORS configuration works seamlessly across environments (implemented in our API handler)
  4. User-Centric Design: Invisible verification maintains a clean, frictionless experience (achieved through Turnstile's invisible verification)

Key Lessons:

  1. Security by Design: Building security into the system architecture from the beginning is much more effective than adding it later
  2. User Experience Balance: It's possible to implement strong security measures without sacrificing user experience
  3. Environment Consistency: A good solution should work identically in both development and production
  4. Third-Party Integration: Leveraging specialized services like Cloudflare Turnstile often provides better results than building custom solutions

This solution demonstrates that static sites can implement secure, user-friendly newsletter subscriptions with the right approach. By carefully selecting tools and prioritizing both security and user experience, we created a robust implementation that protects our ConvertKit account while providing a seamless experience for our visitors.

Sources