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. It is written against Astro 6 (current stable: 6.4.2 as of late May 2026) and the Buttondown v1 REST API.
Prerequisites
Before starting, you will need:
- Node.js 22.12.0 or higher. Astro 6 dropped Node 18 and Node 20 support entirely (both reached or are nearing end of life), so check that both your development and deployment environments run Node 22.12.0 or newer.
- An existing Astro project. Plain HTML signup forms work on a fully static site with no adapter. The API approach (server endpoints, archive fetching) needs on-demand rendering, which means an adapter.
- A Buttondown account (the free tier covers up to 100 subscribers)
- Your Buttondown API key (found in your account under Settings, then API, at the top of the API requests page)
- Your Buttondown username (your newsletter's subdomain)
Installation
Buttondown does not publish an official JavaScript SDK. The REST API is simple enough to call with the built-in fetch API, so no Buttondown package is needed.
For server-side form handling and the newsletter archive, you do need an adapter so those routes can render on demand. The Node adapter is the most portable choice:
npx astro add node
This installs @astrojs/node (current version 10.1.2, which requires Astro ^6.3.0) and wires the adapter into astro.config.mjs for you. If you prefer a manual install, run npm install @astrojs/node and add the adapter config yourself (shown below).
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.com/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({
// No `output` line. `static` is the default and you want most pages
// pre-rendered. The adapter enables on-demand rendering only for the
// routes that opt in with `export const prerender = false`.
adapter: node({ mode: 'standalone' }),
});
Note the missing output property. The output: 'hybrid' value was removed in Astro 5. In Astro 5 and 6 the only valid output values are 'static' (the default) and 'server'. The old hybrid behavior is now the default behavior: pages are static unless you opt a route into on-demand rendering. Keep output at its default 'static' and mark individual server routes with export const prerender = false. The Astro docs advise starting with the default static mode until you are sure most of your pages render on demand.
The Node adapter mode option takes 'standalone' (builds a server that starts itself, the simplest deployment) or 'middleware' (produces a handler you mount into an existing Express or Fastify app and serve static files yourself).
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';
// Opt this endpoint into on-demand rendering. Without this it would be
// pre-rendered at build time and could not read the request body.
export const prerender = false;
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 Adapter Required)
For fully static Astro sites with no adapter, Buttondown provides a direct form action that posts straight to its servers:
<form
action={`https://buttondown.com/api/emails/embed-subscribe/${import.meta.env.PUBLIC_BUTTONDOWN_USERNAME}`}
method="post"
target="popupwindow"
>
<label for="bd-email">Email</label>
<input id="bd-email" type="email" name="email" placeholder="you@example.com" required />
<input type="hidden" value="1" name="embed" />
<button type="submit">Subscribe</button>
</form>
Two details that the current Buttondown docs call for. First, the action host is buttondown.com, not the older buttondown.email. Second, the embedded form expects a hidden embed input set to 1. You can attach extra fields by prefixing input names with metadata__ (for example, metadata__first-name) and attach tags with hidden inputs the docs describe.
Add the username to your public env (the PUBLIC_ prefix is required for Astro to expose it to client-rendered templates):
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();
// The /emails endpoint returns every email regardless of status
// (draft, scheduled, sent, and several in-flight states). Keep only
// the ones that actually went out. You can also filter server-side by
// passing ?status=sent to the API call in buttondown.ts.
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.
Common Errors and Fixes
output: 'hybrid' is not a valid configuration: If you copied an older tutorial, your astro.config.mjs may still set output: 'hybrid'. That value was removed in Astro 5 and will error on Astro 5 or 6. Delete the output line entirely (static is the default) and rely on export const prerender = false per route, or use output: 'server' if nearly every page renders on demand.
"Cannot use server-rendered routes without an adapter": An API route or page with export const prerender = false requires an adapter. Run npx astro add node (or another official adapter such as Vercel, Netlify, or Cloudflare). A purely static site with no on-demand routes does not need one, so prefer the plain HTML form in that case.
Peer dependency mismatch on @astrojs/node: Version 10.1.2 of the Node adapter declares a peer dependency on astro@^6.3.0. If you are still on Astro 4 or an early Astro 5, npm will warn or fail. Either upgrade Astro to 6.3.0 or later, or install the adapter major that matches your Astro version.
API key returning 401 Unauthorized: The Authorization header must read Token YOUR_KEY with a single space after Token (the Buttondown docs flag the trailing space specifically). It is Token, not Bearer. Copy the key from the top of your Buttondown API requests page.
Calls hitting the old API host: The current Buttondown API base is https://api.buttondown.com/v1. Older guides used https://api.buttondown.email/v1. Both still respond today, but the documented host is api.buttondown.com, so use it for forward compatibility. The same shift applies to the embedded form action: use https://buttondown.com/api/emails/embed-subscribe/USERNAME.
"This email address is already registered" error: Creating a subscriber that already exists returns an error. Handle it in your API route and show a friendly message. If you instead want to update or merge existing subscribers, the API supports an X-Buttondown-Collision-Behavior request header set to overwrite or add.
Confirmation email not arriving: Buttondown uses double opt-in by default, so adding a subscriber via the API triggers a confirmation email automatically. Check the spam folder. If the address confirmed previously, the API treats them as already subscribed.
Archive returning unexpected items: The /emails endpoint returns every email regardless of status, including drafts, scheduled, paused, throttled, and RSS-managed emails, not just sent ones. Filter to status === 'sent' in your code, or pass ?status=sent as a query parameter so the API does the filtering. For sent emails the publish_date field is the date the email went out.
Hitting the API rate limit (429): Buttondown caps API traffic at 600 requests per minute across all endpoints, with a tighter 100 requests per day specifically on the POST /v1/subscribers (create subscriber) endpoint. Newsletters on paid plans with permanently active status are exempt. Cache the archive response rather than fetching it on every page request, and debounce the signup form so rapid double-clicks do not fire multiple POSTs.
Form submission redirecting to Buttondown: The plain HTML form posts directly to Buttondown's servers and can navigate the user away. Keep target="popupwindow" on the form, or use the API endpoint approach for a flow that stays on your site.
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.
Official Docs and Examples
- Buttondown API authentication: https://docs.buttondown.com/api-authentication
- Buttondown create subscriber: https://docs.buttondown.com/api-subscribers-create
- Buttondown email status reference: https://docs.buttondown.com/api-emails-status
- Buttondown build your subscriber base (embed form): https://docs.buttondown.com/building-your-subscriber-base
- Astro on-demand rendering guide: https://docs.astro.build/en/guides/on-demand-rendering/
- Astro Node adapter guide: https://docs.astro.build/en/guides/integrations-guide/node/
- Example repo (Astro newsletter signup): https://github.com/LuisJimenez19/astro-newsletter
- Walkthrough (Astro plus Buttondown newsletter section): https://amomentineternity.com/blog/how-to-create-a-newsletter-section-using-astro-and-buttondown/
Sources
All versions and facts below were verified on 2026-05-29.
- Astro 6.4.2 current release, from npm registry: https://registry.npmjs.org/astro/latest
@astrojs/node10.1.2 current release and itsastro@^6.3.0peer dependency, from npm registry: https://registry.npmjs.org/@astrojs/node/10.1.2- Valid
outputvalues (static,server) andprerenderexport usage, Astro on-demand rendering docs: https://docs.astro.build/en/guides/on-demand-rendering/ - Node adapter install command and
modeoptions (standalone,middleware), Astro Node adapter docs: https://docs.astro.build/en/guides/integrations-guide/node/ - Astro 6 requires Node 22.12.0 or higher and drops Node 18 and Node 20, Astro v6 upgrade guide: https://docs.astro.build/en/guides/upgrade-to/v6/
- Buttondown
Authorization: Tokenheader (with required trailing space), authentication docs: https://docs.buttondown.com/api-authentication - Buttondown create-subscriber endpoint,
email_addressfield, base URLhttps://api.buttondown.com/v1, andX-Buttondown-Collision-Behaviorheader, create docs: https://docs.buttondown.com/api-subscribers-create - Buttondown email status values and
statusquery filter, email status docs: https://docs.buttondown.com/api-emails-status - Buttondown API rate limits (600 requests/minute general, 100 requests/day on create-subscriber, paid permanently-active exempt), rate limits docs: https://docs.buttondown.com/api-rate-limits
- Buttondown embedded form action
https://buttondown.com/api/emails/embed-subscribe/USERNAME, hiddenembedinput, andmetadata__prefix, subscriber base docs: https://docs.buttondown.com/building-your-subscriber-base - API host reachability confirmed via
curl -sIagainsthttps://api.buttondown.com/v1/subscribersandhttps://buttondown.com/api/emails/embed-subscribe/on 2026-05-29
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.