How I Replaced Revue With a Custom-Built Newsletter Service Using Nuxt 3, Supabase, Serverless, and Amazon SES
Twitter will shut down Revue on January 18, 2023, which I previously used as a newsletter provider for Weekly Vue News.
There exist potent alternatives like Substack, Buttondown, beehiv, and more. But I decided to build a custom solution for these reasons:
- Manage my content as Markdown files in my project's repository.
- Emails can use the same CSS styles as the website.
- A cheap solution that does not get too expensive.
This article explains how I built a custom newsletter service using Nuxt 3, Supabase, Serverless, and Amazon SES.
General Note
My proposed solution is not only limited to the mentioned frameworks & tools. You could easily accomplish the same functionality with other frameworks & tools of your choice.
Backend
Let's start by looking at the backend code I use for my newsletter solution.
Database Model
I use Supabase to store two database tables.
The first table stores the list of subscribers and has the following schema:
id
: primary key as an integercreated_at
: timestamp indicating when the user was added to the tableemail
: email address that should receive the newsletterverification_token
: a UUID used for the confirmation emailunsubscribe_token
: a UUID used for the unsubscribe mechanismverified
: A boolean indicating if the user has confirmed the confirmation mail
The second table stores the list of scheduled issues and has the following schema:
id
: primary key as an integerissue_id
: the ID of the newsletter issuehtml
: the html string that should be sent to the subscribers in the body of the emailtitle
: a string which will is used as a subject in the emails sent to the subscribersscheduled_at
: timestamp when the issue should or has been publishedpublished
: A boolean indicating if the issue is publishedsend_count
: A number that stores how many subscribers the issue has been sent tobeacon_data
: JSON object that stores analytics information
Amazon SES
I decided to send emails using Amazon SES; this tutorial will teach you how to set up and get started using Amazon SES. I use Nodemailer with SES transport to send my emails using Amazon SES. Nodemailer SES transport is a wrapper around aws.SES
from the @aws-sdk/client-ses package.
The main benefit is that Nodemailer provides rate limiting for SES out of the box. SES can tolerate short spikes, but you can’t flush all your emails at once and expect these to be delivered. Luckily, Amazon granted my request to increase the sending quota to 50,000 messages per day and a maximum sending rate of 14 messages per second.
A quick note about SES pricing: You pay only for what you use with no minimum fees or mandatory service usage. The current price is $0.10/1000 emails, which is cheap compared to other newsletter services like Substack, Buttondown or beehiv.
Enough theory; let's take a look at the code I wrote to wrap the Nodemailer integration:
Subscribe
Info
As my newsletter website is built with Nuxt 3, I use Nuxt server routes for the backend implementation.
Additionally, I use Nuxt Supabase as a wrapper around supabase-js to enable usage and integration within Nuxt.
If a new user wants to subscribe to the newsletter, we need to trigger an endpoint that receives the user's email address:
Let's analyze the above code. The first step is to validate the email and return an error if it is missing or invalid:
Next, we try to insert a new subscriber into the subscriber table with the provided email and return an error if we already have a subscriber with the given email address:
Finally, we send the confirmation mail that contains a link with the generated verificationToken
as query parameter:
Clicking on this link in the frontend will trigger the following backend endpoint:
We query the subscriber database for an entry where the verification_token
equals the given token
query parameter.
If an entry is found, we set its verified
value to true
.
A verified user is subscribed and will receive the newsletter emails.
Info
The advantages of using subscription confirmation emails and implementing a double opt-in process:
- Ensuring compliance with the General Data Protection Regulation (GDPR): The GDPR requires that you obtain explicit consent from users before adding them to your newsletter subscriber list and processing their personal data, such as their email address.
- Ensuring that your newsletter subscribers are actively engaged: By requiring confirmation of subscription, you can ensure that only users who actively want to receive your newsletters will be added to your subscriber list. This can help prevent accidental or unwanted subscriptions and improve the quality of your subscriber list.
- Maintaining a clean and accurate contact list: By requiring confirmation of subscription, you can ensure that only those users who are truly interested in receiving your company updates will be added to your subscriber list. This can help you maintain a high-quality list of engaged and interested contacts.
Unsubscribe
Of course, we must provide a way to unsubscribe from the newsletter. It's mainly based on the unsubscribe_token
column of the subscribers database table:
We query the subscriber database for an entry where the unsubscribe_token
equals the given token
query parameter.
If an entry is found, we remove it from the database, so he will not receive any further newsletter emails.
Schedule Issue
We need to provide an endpoint to schedule an issue:
This simple function upserts an issue based on the given parameters in the event body.
Serverless Cron Function
My newsletter is sent every Monday at 3 pm. I wrote a serverless cron function that checks if a scheduled issue exists and then sends it to all subscribers. Therefore I used the Serverless framework.
The serverless configuration:
And the corresponding code for the Lambda function. I removed the error handling in the following code snippet to keep the code clean and concise:
A lot is going on in this function; let's break it down:
- Initialize Supabase & Nodemailer clients
- Get all verified subscribers
- Get all stored issues that haven't been published yet
- Find the unpublished issue which
schedule_at
timestamp is today - Send this issue to all verified subscribers
- Set
published
to true and updatesend_count
in the stored issue
Frontend
Let's look at the frontend part of my custom-built newsletter solution. I won't explain every component, as I mainly use Nuxt's Data Fetching composables to trigger the above-defined backend endpoints and display the result.
But one interesting aspect is the generation of the HTML string I send via email to my subscribers.
Generating HTML string of the rendered Markdown file
I use Nuxt Content to store my newsletter issues as Markdown files. Here is a simple example:
These files are rendered on a Nuxt page using <ContentRenderer>
from Nuxt Content :
Using Supabase Auth I provide a way to log in as admin and scheduling issues:
Let's now focus on the getHtml()
method in IssueAdminControls.vue
that we use to generate an HTML string of the rendered Markdown content:
The allCss
variable collects all CSS stylesheets attached to the current document and joins their textual representation as string. I then use juice to inline all CSS properties into the style
attribute.
Info
Inline CSS styles are a smart approach to style HTML emails.
I use props.contentHtml.replace(/<!--(?:(?!-->)[\s\S])*-->/g, '')
to replace HTML comments from the outerHTML
string that is passed to the component via the contentHtml
property (check again the pages/issues/[...slug].vue
component above). This setup worked well for my content and styles, but you likely need to adjust your implementation.
To schedule an issue, I send a POST request to /api/issue
, which I already explained in the backend section.
Conclusion
I’m delighted with my solution. I’m 100% in control of my content and the emails I send to my subscribers!
It was a lot of hard work to build this thing, but I also learned a lot during this process. I hope this will help me grow my newsletter and keep the costs low for an increasing number of subscribers.
A special thanks to Simon Høiberg and Michael Thiessen that provided the technical inspiration for this solution.
Leave a comment if you have questions or feedback or can provide an alternative solution for such a custom-built newsletter service.
If you liked this article, follow me on Twitter to get notified about new blog posts and more content from me. Alternatively (or additionally), you can subscribe to my weekly Vue newsletter.
Create a Table of Contents With Active States in Nuxt 3
I'm a big fan of a table of contents (ToC) on the side of a blog post page, especially if it is a long article. It helps me gauge the article's length and allows me to navigate between the sections quickly.
Use Shiki to Style Code Blocks in HTML Emails
I recently developed a custom newsletter service using Nuxt 3. One of the main reasons why I developed it on my own was that I wanted to use good-looking code blocks in my emails.