Send your Hugo blog as a newsletter with Emit — from absolute scratch
You write in Hugo, run hugo, and push static files. There's no database, no
login, no server to babysit — and no obvious way to email readers when you publish. This
guide fixes that. We'll start with nothing and end with a working loop: every time you ship
a post, Emit picks it up from your RSS feed and emails your confirmed subscribers, from your
own domain. You pay $0.80 per 1,000 emails — no monthly fee, no
per-subscriber tier.
What we'll do
Before you start
You'll need three things:
-
A Hugo site you can publish. It can be brand new — even
hugo new site myblogwith a single post is enough to follow along. It does need to be reachable on the public internet (GitHub Pages, Netlify, Cloudflare Pages, a VPS — anywhere) so Emit can fetch the RSS feed. -
A domain you control the DNS for. You'll add a couple of DNS records so
email sends from your own domain (e.g.
posts@yourblog.com) instead of a shared address. This is what keeps you out of spam folders. - About $10. Emit is prepaid and has no free tier; the minimum top-up is $10, which buys 12,500 emails. There's nothing to cancel later — credits just sit there until used.
Everything below can be done from the dashboard with no code, or
from the API with curl. We'll show both. Set a shell variable for the API base
so the commands copy-paste cleanly:
export EMIT=https://api.rssemit.com
Step 1Create your Emit account
The fastest path is the dashboard. Open rssemit.com/app.html, choose Create account, and enter your name and email. You'll be handed an API key — copy it now, because it's shown exactly once and stored only as a hash on our side. If you lose it, you rotate it; you can't retrieve it.
Prefer the API? It's a single call:
curl -sX POST $EMIT/v1/accounts \
-H "Content-Type: application/json" \
-d '{"name":"My Hugo Blog","email":"you@yourblog.com"}'
The response includes your key once:
{
"id": "acct_3pQ…",
"object": "account",
"email": "you@yourblog.com",
"public_token": "pk_live_8f2…",
"api_key": "sk_live_a91c…", <-- copy this now, shown once
"credit_balance": 0
}
Stash the key and your public token in environment variables — we'll use both:
export EMIT_KEY=sk_live_a91c…
export EMIT_PK=pk_live_8f2…
Emit will email you a verification link for the account address — click it to confirm you own the inbox. That's separate from verifying your sending domain, which is next.
Authentication, in one line. Every authenticated request carries
Authorization: Bearer $EMIT_KEY. The pk_… public token is
different — it's safe to ship in your site's HTML and can only create pending
(opt-in) subscribers, nothing else.
Step 2Verify your sending domain
To send from posts@yourblog.com you have to prove you own
yourblog.com and let the mail provider sign your messages (DKIM). Register the
domain with Emit to get the exact DNS records to add:
curl -sX POST $EMIT/v1/domains \
-H "Authorization: Bearer $EMIT_KEY" \
-H "Content-Type: application/json" \
-d '{"domain":"yourblog.com"}'
The response (and the dashboard's Domains section) lists the records to create at your DNS host — typically:
-
A TXT ownership challenge like
emit-verify=…on the domain itself. - A set of DKIM / SPF records (CNAME or TXT) that let the provider sign mail on your behalf.
Add them in your registrar / DNS dashboard (Cloudflare, Namecheap, Route 53, etc.). DNS can take a few minutes to propagate, then ask Emit to verify:
curl -sX POST $EMIT/v1/domains/{domain_id}/verify \
-H "Authorization: Bearer $EMIT_KEY"
When it reports verified, any from address on that domain is yours to use. If
verification fails, it's almost always propagation — wait a few minutes and retry.
Step 3Fund your account
Emit bills per email from a prepaid balance — zero balance means nothing sends, so you load credits before connecting a feed. Start a top-up (minimum $10):
curl -sX POST $EMIT/v1/billing/topup \
-H "Authorization: Bearer $EMIT_KEY" \
-H "Content-Type: application/json" \
-d '{"amount_usd":10}'
You get back a Stripe Checkout URL:
{
"checkout_url": "https://checkout.stripe.com/c/pay/…",
"amount_usd": 10,
"credits": 12500
}
Open that URL, pay with a card, and the Stripe webhook credits your balance — 12,500 emails for $10. From the dashboard this is just the Billing → Add credits button; same Checkout page, no copy-paste. You can confirm the balance landed any time:
curl -s $EMIT/v1/account -H "Authorization: Bearer $EMIT_KEY"
How far does $10 go? Credits are charged per email sent, not per subscriber stored. If you have 500 confirmed readers and publish 4 posts a month, that's 500 × 4 = 2,000 emails — about $1.60/month. The other $8.40 rolls forward. If you publish nothing, you're charged nothing.
Step 4Make sure Hugo publishes a usable RSS feed
Good news: Hugo generates RSS automatically. By default your home feed lives at
/index.xml (and each section gets its own, e.g. /posts/index.xml).
Confirm RSS output is enabled for the home page in your hugo.toml (or
config.toml):
[outputs]
home = ["HTML", "RSS"]
section = ["HTML", "RSS"]
Run a build and check the feed exists:
hugo
curl -s https://yourblog.com/index.xml | head -40
You should see <rss> with one <item> per post. One
thing worth deciding now: Hugo's default RSS template ships the post
summary, not the full body, inside <description>. That
means readers get an excerpt and click through to your site. If you'd rather email the full
post, override the RSS template to emit the full content. Create
layouts/_default/rss.xml and change the description line to use
.Content:
<description>{{ .Content | html }}</description>
(Copy Hugo's built-in RSS template as a starting point and tweak just that line.) Either way works with Emit — it's your call whether the email is a teaser or the whole thing. Rebuild and re-deploy so the live feed reflects your choice.
Step 5Connect the feed to Emit
Now point Emit at that feed and tell it which address to send from. In the dashboard: Feeds → Add feed, paste your feed URL, set the from-address, pick a schedule. Or via the API:
curl -sX POST $EMIT/v1/feeds \
-H "Authorization: Bearer $EMIT_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourblog.com/index.xml",
"from": "posts@yourblog.com",
"schedule": "on_publish"
}'
A few notes on the fields:
-
url— your Hugo feed. It must return RSS 2.0 or Atom over HTTPS and be reachable within 5 seconds (self-signed certs are rejected). -
from— any local part on the domain you verified in Step 2.posts@,newsletter@,hello@— your choice. -
schedule—on_publishemails each post as it appears. Prefer a digest? Use a polling/daily/weekly schedule instead so multiple posts batch into one email.
Emit immediately does a baseline poll and records the items already in your feed
without emailing them — so connecting a feed never blasts your whole archive. From
here on, only genuinely new items send. Conditional GET (ETag / Last-Modified)
is on by default, so polling is cheap and polite to your host.
Step 6Add a subscribe form to your Hugo site
A feed with no subscribers sends nothing. Emit collects readers through a double opt-in form that posts straight to the public endpoint — no API key in your HTML, no backend. Because Hugo is just templates, this is a partial.
Create layouts/partials/subscribe.html:
<form action="https://api.rssemit.com/v1/public/subscribe" method="post"
style="max-width:380px">
<input type="hidden" name="token" value="pk_live_8f2…">
<input type="hidden" name="redirect" value="https://yourblog.com/subscribed/">
<!-- honeypot: bots fill this, humans don't -->
<input type="text" name="hp" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px;" aria-hidden="true">
<label>Subscribe to the newsletter</label>
<input type="email" name="email" required placeholder="you@example.com">
<button type="submit">Subscribe</button>
<a href="https://rssemit.com?ref=embed">powered by emit</a>
</form>
Replace pk_live_8f2… with your own public token (the
pk_… value from Step 1, also shown in the dashboard's
Embed section, which generates this snippet pre-filled). Point
redirect at any "thanks, check your inbox" page on your own site.
Then drop the form wherever you want it — your footer partial, the homepage, a dedicated page:
{{ partial "subscribe.html" . }}
That's the whole integration. The form is plain HTML, so it works without JavaScript and survives any theme. It includes a honeypot field, and the endpoint is rate-limited, so casual bots are filtered out. Rebuild and deploy.
Why double opt-in? When someone submits the form, Emit creates them as pending and emails a confirmation link. They aren't added to your list — and you aren't billed to mail them — until they click it. That keeps your list clean and your sender reputation intact.
Step 7Test the whole loop
Before you tell the world, prove it works with your own address:
- Visit your site, enter your email in the subscribe form, submit.
- Check your inbox for the confirmation email and click the link.
-
Confirm you're now a subscriber — in the dashboard's Subscribers list, or
via
curl -s $EMIT/v1/subscribers -H "Authorization: Bearer $EMIT_KEY". You should appear with statusconfirmed. - Optionally, send yourself a test of the latest post: the dashboard's broadcast view has a Test send / preview button so you can eyeball the formatting before real posts go out.
If the confirmation email never arrives, re-check Step 2 — unverified or half-propagated DNS
is the usual culprit. The feed's last poll status (visible per-feed in the dashboard) will
also tell you if Emit is having trouble fetching your /index.xml.
Step 8Publish a post and watch it send
Now do the thing the whole setup exists for. Write a post and ship it:
hugo new posts/hello-newsletter.md
# write the post, then set draft = false
hugo # rebuild
# deploy however you normally deploy
Once the new <item> is live in
https://yourblog.com/index.xml, Emit's worker notices it on the next poll,
renders the email, and sends it to every confirmed subscriber from
posts@yourblog.com. Each send debits one credit from the balance you topped up
in Step 3. Every email carries a one-click List-Unsubscribe header, so readers
can leave cleanly and stay off your list.
That's the loop, and it's now permanent: write in Hugo → push → readers get an email. No dashboard visit required ever again.
Recap
- Account — created, API key saved, owner email verified.
-
Domain — DNS records added, verified, sending from
yourblog.com. - Funded — $10 of prepaid credits (12,500 emails), no recurring charge.
-
Feed — Hugo's
/index.xmlconnected on anon_publishschedule. - Form — a Hugo partial collecting double-opt-in subscribers.
From here you can add filters (skip certain tags, set a minimum word count), switch to a daily/weekly digest, or import an existing list. The API docs cover every endpoint, and there's an MCP server and a Claude Code skill if you'd rather drive Emit from an agent. Questions? team@rssemit.com.
Ready? Create your account and connect your first feed →