/ astro-integrations / How to Integrate Buttondown with Astro: Complete Guide
astro-integrations 6 min read

How to Integrate Buttondown with Astro: Complete Guide

Step-by-step guide to integrating Buttondown with your Astro website. Setup, configuration, and best practices.

How to Integrate Buttondown with Astro: Complete Guide

Buttondown is a minimal, developer-friendly newsletter platform that focuses on writing and subscriber management without the bloat of larger email marketing tools. It supports Markdown for composing emails, offers a clean REST API, and provides features like paid subscriptions, automation, and RSS-to-email. For Astro developers who want a straightforward newsletter system with a good API, Buttondown is a strong choice.

This guide covers connecting Buttondown to an Astro project for subscriber management, building custom signup forms, and displaying newsletter archives.

Prerequisites

Before starting, you will need:

  • Node.js 18+ installed
  • An existing Astro project (any version for basic forms, SSR for API integration)
  • A Buttondown account (free tier for up to 100 subscribers)
  • Your Buttondown API key (found in Settings > API)
  • Your Buttondown username (your newsletter's subdomain)

Installation

Buttondown does not have an official SDK. The REST API is simple enough to use with the built-in fetch API. No additional packages are needed.

For server-side form handling, add an SSR adapter:

npx astro add node

Configuration

Environment Variables

Add your Buttondown credentials to .env:

BUTTONDOWN_API_KEY=your_api_key_here
BUTTONDOWN_USERNAME=your_username

Creating a Buttondown Utility

// src/lib/buttondown.ts
const API_KEY = import.meta.env.BUTTONDOWN_API_KEY;
const BASE_URL = 'https://api.buttondown.email/v1';

export async function addSubscriber(email: string, metadata?: Record<string, string>) {
  const response = await fetch(`${BASE_URL}/subscribers`, {
    method: 'POST',
    headers: {
      Authorization: `Token ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email_address: email,
      metadata: metadata || {},
      type: 'regular',
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.detail || error[0] || 'Subscription failed');
  }

  return response.json();
}

export async function getNewsletterArchive() {
  const response = await fetch(`${BASE_URL}/emails`, {
    headers: {
      Authorization: `Token ${API_KEY}`,
    },
  });

  if (!response.ok) throw new Error('Failed to fetch archive');

  return response.json();
}

Astro Config

// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'hybrid',
  adapter: node({ mode: 'standalone' }),
});

Common Patterns

Server-Side Subscribe Endpoint

Create an API route that handles form submissions:

// src/pages/api/subscribe.ts
import type { APIRoute } from 'astro';
import { addSubscriber } from '../../lib/buttondown';

export const POST: APIRoute = async ({ request }) => {
  const formData = await request.formData();
  const email = formData.get('email') as string;

  if (!email || !email.includes('@')) {
    return new Response(JSON.stringify({ error: 'Valid email is required' }), {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  try {
    await addSubscriber(email, { source: 'website' });

    return new Response(JSON.stringify({ success: true }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    });
  } catch (error: any) {
    const message = error.message.includes('already')
      ? 'You are already subscribed!'
      : 'Subscription failed. Please try again.';

    return new Response(JSON.stringify({ error: message }), {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    });
  }
};

Newsletter Signup Component

---
// src/components/Subscribe.astro
---

<div class="subscribe-box">
  <h3>Subscribe to the newsletter</h3>
  <p>No spam. Unsubscribe anytime.</p>

  <form id="subscribe-form">
    <input type="email" name="email" placeholder="you@example.com" required />
    <button type="submit">Subscribe</button>
  </form>
  <p id="sub-status" style="display: none;"></p>
</div>

<script>
  const form = document.getElementById('subscribe-form') as HTMLFormElement;
  const status = document.getElementById('sub-status') as HTMLParagraphElement;

  form?.addEventListener('submit', async (e) => {
    e.preventDefault();
    const data = new FormData(form);

    try {
      const res = await fetch('/api/subscribe', { method: 'POST', body: data });
      const result = await res.json();

      status.style.display = 'block';
      if (res.ok) {
        status.textContent = 'Thanks! Check your inbox to confirm.';
        status.style.color = 'green';
        form.reset();
      } else {
        status.textContent = result.error;
        status.style.color = 'red';
      }
    } catch {
      status.style.display = 'block';
      status.textContent = 'Something went wrong.';
      status.style.color = 'red';
    }
  });
</script>

Plain HTML Form (No SSR Required)

For static Astro sites, Buttondown provides a direct form action:

<form
  action={`https://buttondown.email/api/emails/embed-subscribe/${import.meta.env.PUBLIC_BUTTONDOWN_USERNAME}`}
  method="post"
  target="popupwindow"
>
  <input type="email" name="email" placeholder="you@example.com" required />
  <button type="submit">Subscribe</button>
</form>

Add the username to your public env:

PUBLIC_BUTTONDOWN_USERNAME=your_username

Displaying a Newsletter Archive

Fetch and display past newsletter issues on your Astro site:

---
// src/pages/newsletter.astro
import { getNewsletterArchive } from '../lib/buttondown';

export const prerender = false;

let emails = [];
try {
  const data = await getNewsletterArchive();
  emails = data.results.filter((e: any) => e.status === 'sent');
} catch {
  // Archive unavailable
}
---

<html>
  <body>
    <h1>Newsletter Archive</h1>

    {emails.length === 0 ? (
      <p>No newsletters yet. Subscribe to be the first to know!</p>
    ) : (
      <ul>
        {emails.map((email: any) => (
          <li>
            <a href={`/newsletter/${email.id}`}>
              <strong>{email.subject}</strong>
            </a>
            <time>{new Date(email.publish_date).toLocaleDateString()}</time>
          </li>
        ))}
      </ul>
    )}
  </body>
</html>

Subscriber Metadata for Segmentation

Pass metadata when subscribing to segment your audience later:

await addSubscriber(email, {
  source: 'blog-post',
  topic: 'astro-tutorials',
  signed_up_page: '/blog/astro-tips',
});

You can use this metadata in Buttondown to create filtered segments and send targeted emails to specific groups.

Troubleshooting

"This email address is already registered" error: Buttondown returns a specific error when a subscriber already exists. Handle this case in your API route and show a friendly message instead of a generic error.

API key returning 401 Unauthorized: Verify the API key is correct and uses the format Token YOUR_KEY in the Authorization header (note: Token, not Bearer). Copy the key directly from Buttondown Settings > API.

Confirmation email not sending: Buttondown uses double opt-in by default. The confirmation email is sent automatically when you add a subscriber via the API. Check the subscriber's spam folder. If they already confirmed previously, the API treats them as already subscribed.

Archive API returning empty results: The /emails endpoint only returns published newsletters. Draft and scheduled emails are not included. Make sure you have sent at least one newsletter from your Buttondown dashboard.

Form submission redirecting to Buttondown: If using the plain HTML form approach, it submits directly to Buttondown's servers and may redirect the user. Add target="popupwindow" to the form or use the API approach for a seamless experience that keeps users on your site.

Rate limiting: Buttondown's API has reasonable rate limits for normal usage. If you are building a high-traffic site, cache the archive results and implement debouncing on the signup form to prevent rapid submissions.

Conclusion

Buttondown is a lightweight newsletter solution that fits well with Astro's developer-focused philosophy. The REST API is straightforward to use without an SDK, and the platform handles subscriber management, double opt-in, and email delivery. Use the API approach for custom form designs that match your site, or the embedded form for quick static site integration. The newsletter archive feature lets you repurpose your email content as web pages, giving your newsletter issues a permanent, searchable home on your Astro site.