Bit of preamble before I kick this off, subscribers to my newsletter will know that I've been through a couple of platforms now (Zoho, SendGrid, Revue then Substack). I settled on Substack because of the editing experience what I didn't want to settle on was the janky embed you get with Substack.
Depending on what theme you're using this can potentially be acceptable, probably not though!
I did get my own custom form working with Substack locally. The Tl;Dr is I popped open the network tab in the browser and made a note of where the submission was going and checked out the payload so I could try make a similar submission with Insomnia. This worked locally! But on deploying to Vercel the submit wasn't working and I went back to using the Substack embed. Sad times!
Revue has an open API
Then I remembered that Revue had an open API with docs and everything so I created an account (I deleted my old one) and used Insomnia to try out some of the API methods, it worked locally with Insomnia!
So I swapped out the Substack endpoint with the Revue one deployed it to Vercel and tried it out. I could add new subscribers to my Revue account! Joy! Ok onto the how to!
Testing the Revue endpoint
Because Revue has an open API that means there's ✨documentation✨
If you're following along you will need your Revue API key, you can find it at the bottom of the integrations page.
Scroll to the bottom and look for something like this:
Your API key is
2f09ecd9-6a64-4d5b-9c77-a5587cedbcf7
.Usage of the API must follow Revue’s Terms of Service and Privacy Policy.
⚠️ Usual warning about exposing API keys here, there doesn't seem to be a way to generate a new Revue API key, so if it's leaked somewhere I'm not sure how you'd go about revoking it.
Using Insomnia the first thing I did was check out the POST method with https://www.getrevue.co/api/v2/subscribers
the request body was this:
{
"email": "spences10apps+test@gmail.com",
"first_name": "",
"last_name": "",
"double_opt_in": false
}
As a side note you can add a +
to the end of an email address in Gmail to give it a unique name. So in the case of the example +test
is what I'm using as a way to identify the email address.
The Bearer token looked like this Token <your-api-key>
.
Hit Send button and wait for the response! I get a 200 OK with the preview reply looking something like this:
{
"id": 5654821249,
"list_id": 216568,
"email": "spences10apps+test@gmail.com",
"first_name": "",
"last_name": "",
"last_changed": "2021-08-31T20:10:24.197Z"
}
Alright, sweet! I can now add a subscriber to my Revue account via the Revue API!
Setup the project
In this example like the last couple of examples I've done I'll be using Matt Jennings' SvelteKit blog template; it's what this site is based off of.
ℹ️ This is for a SvelteKit project hosted on Vercel, if you're following along then this is what I'm doing:
git clone git@github.com:mattjennings/sveltekit-blog-template.git
cd sveltekit-blog-template
npm i
Matt's example uses the SvelteKit adapter-static
and because I'm deploying to Vercel I'll need to install adapter-vercel
and add that in the svelte.config.js
:
# uninstall adapter-static
npm un @sveltejs/adapter-static
# install adapter-vercel
npm i @sveltejs/adapter-vercel@next
Then it's a case of swapping out the first line here adapter-static
with adapter-vercel
:
import adapter from '@sveltejs/adapter-vercel'
import { mdsvex } from 'mdsvex'
import preprocess from 'svelte-preprocess'
import mdsvexConfig from './mdsvex.config.js'
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: ['.svelte', ...mdsvexConfig.extensions],
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [
mdsvex(mdsvexConfig),
[
preprocess({
postcss: true,
}),
],
],
kit: {
target: '#svelte',
adapter: adapter(),
},
}
export default config
// Workaround until SvelteKit uses Vite 2.3.8 (and it's confirmed to fix the Tailwind JIT problem)
const mode = process.env.NODE_ENV
const dev = mode === 'development'
process.env.TAILWIND_MODE = dev ? 'watch' : 'build'
The rest of the config here isn't really pertenant, what matters is that I have swapped out adapter-static
with adapter-vercel
I've literally copied the code of what I'm working on.
Setting up environment variables
Because I'm going to be using an API key I don't want exposed there's a few things I'll need to do!
First is to add .env
to the .gitignore
file. For some reason this isn't in the default skeleton you make with npm init svelte@next
project so I'll be adding .env
to the .gitignore
file. I'll be doing this via the terminal, you can edit the file manually if you like:
echo .env >> .gitignore
SvelteKit uses Vite and you can prefix you environment variables with VITE_
so they're available to the client (the browser) this also means that they can be seen from the client.
Although the code for an endpoint runs on the server and adding the VITE_
means that you can access the variable in development mode it shouldn't be exposed to the client but I prefer to use process.env
to access the variables.
I've made a short post on how to use .env
secrets in SvelteKit if you need a bit more detail on that.
I'm going to install env-cmd
and add that to the dev script, first up install the package:
npm i -D env-cmd
Then add it to the dev script:
"scripts": {
"dev": "env-cmd svelte-kit dev",
No I can access environment variables in development mode.
Setting up the endpoint
Now I'll need to set up the endpoint to submit the email to the Revue API. I'll do this in the terminal:
# make the directory
mkdir src/routes/email-submit
# create the file
touch src/routes/email-submit/index.json.js
Now for the endpoint post
function!
Now I can scaffold ou the function to submit the email to the Revue API.
For now, to test it's worked I'll hardcode in the email address to the POST
body, then I'll build on that once I've validated it's working.
export async function post() {
const REVUE_API_KEY = process.env['REVUE_API_KEY']
try {
const res = await fetch('https://www.getrevue.co/api/v2/subscribers', {
method: 'POST',
headers: {
Authorization: `Token ${REVUE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'spences10apps+test@gmail.com',
first_name: '',
last_name: '',
double_opt_in: false,
}),
})
if (res.ok) {
return {
status: 200,
body: JSON.stringify({
message: 'email sent!',
}),
}
}
if (res.status !== 200) {
return {
status: 400,
body: JSON.stringify({
message: 'bad request',
}),
}
}
} catch (error) {
return {
status: 500,
body: JSON.stringify({
message: 'something went wrong with the email submit!',
}),
}
}
}
Nice big wall of text! Like that? Apologies, you could just remove the error checks and YOLO it if you like, I'm not your mum! 😂
So everything is wrapped in a try
block and if there's an bad request made to the Revue API then that'll be caught and a response of bad request
given.
There's a final catch if the Revue request fails as well.
ℹ️ One thing to note is that I'm not asking for a first or last name and that I'm not requiring users to double opt in (i.e. reply to another email to say yes sign me up to the list I've just subscribed to).
Note again, if you didn't catch it earlier, the email address is hardcoded in here:
body: JSON.stringify({
email: 'spences10apps+test@gmail.com',
first_name: '',
last_name: '',
double_opt_in: false,
})
I'll be changing that once I've validated the submit is working. I cover that in the Receive email in endpoint section.
If you want you can do what you like with these options, my aim is to remove as much friction as possible.
Sweet! Now that the endpoint is set up I can test it by submitting a request from a page.
Setting up the submit form
I'm going to create a sign up component and then use that on the index page of the project, first I'll create the component in the lib
folder:
touch src/lib/components/submit.svelte
Then add the following script to the component:
<script>
let email = ''
let showMessage = false
let responseMessage = ''
async function submitForm() {
const submit = await fetch('/email-submit.json', {
method: 'POST',
body: JSON.stringify({ email }),
})
const data = await submit.json()
if (data.message === 'bad request') {
showMessage = true
responseMessage = `That looks like a bad request`
}
if (data.message === 'email sent!') {
showMessage = true
responseMessage = `Sweet! You're signed up!`
}
if (data.message === 'something went wrong with the email submit!') {
showMessage = false
// deal with failed response from server
}
}
</script>
So this is setting up the call to the endpoint using the browser fetch API to the endpoint /email-submit.json
then setting the success
variable if there's no issues.
In the body of the component I'll add the form and the submit button, the project uses Tailwind so I've added some minimal styles:
<div class="mb-10">
{#if success}
<div class="text-center">
<h3 class="font-extrabold text-3xl">{responseMessage}</h3>
</div>
{:else}
<div class="text-center">
<h3 class="font-extrabold text-3xl">Sign up for the newsletter</h3>
<form class="" on:submit|preventDefault={submitForm}>
<label for="email" class="label">
<span class="sr-only">Your Email</span>
</label>
<input id="email" aria-label="email" type="email" name="email" autocomplete="email" placeholder="your@email.com" required bind:value={email} />
<input type="submit" />
</form>
</div>
{/if}
</div>
The main part to note here is in the <form>
element and the call to submitForm
via on:submit|preventDefault={submitForm}
. this is going to call the submitForm
function defined in the <script>
at the top of the component.
This is all wrapped in a Svelte {# if}
directive so that there can be a message displayed with the showMessage
variable to the user once they have submitted the form.
Test the submit
Time to add the sign up form to the index page of the project and hit submit!
I'll import the <Submit />
component into src/routes/index.svelte
here's what the top of the file looks like for me:
<script>
import ButtonLink from '$lib/components/ButtonLink.svelte'
import Submit from '$lib/components/submit.svelte'
import { name } from '$lib/info.js'
import { format } from 'date-fns'
export let posts
export let page
$: isFirstPage = page === 1
$: hasNextPage = posts[posts.length - 1]?.previous
</script>
<svelte:head>
<title>{name}</title>
</svelte:head>
<Submit />
<div class="flex flex-col flex-grow">
<!-- rest of the code here -->
Now I can enter an email address and hit submit! It doesn't matter what the email is because it's hardcoded into the endpoint at the moment!
I'll hit submit and I'll go over to my Revue subscribers list and check to see if the email is there!
Sweet! Now I've validated the submit is working I can delete the subscriber from my subscribers list and go about having the endpoint receive what is submitted from the component!
Receive email in endpoint
Now all I need to do is add the the req
parameter to the post
function on the email-submit
endpoint and pull out (destructure) the email
from the req.body
!
export async function post(req) {
const { email } = JSON.parse(req.body)
const REVUE_API_KEY = process.env['REVUE_API_KEY']
try {
const res = await fetch('https://www.getrevue.co/api/v2/subscribers', {
method: 'POST',
headers: {
Authorization: `Token ${REVUE_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
first_name: '',
last_name: '',
double_opt_in: false
})
})
// rest of the code unchanged
Test it's worked on Vercel
Ok, now time to test it's working when deployed to Vercel! I use the Vercel CLI so I can push this off from my terminal with once command vc
and off it goes!
I've selected the default for all the CLI options, here's what I have:
➜ vc
Vercel CLI 23.0.1
? Set up and deploy “~/repos/svelte-kit-form-submission”? [Y/n] y
? Which scope do you want to deploy to? Scott Spence
? Link to existing project? [y/N] n
? What’s your project’s name? svelte-kit-form-submission
? In which directory is your code located? ./
Auto-detected Project Settings (SvelteKit):
- Build Command: `npm run build` or `svelte-kit build`
- Output Directory: public
- Development Command: svelte-kit dev --port $PORT
? Want to override the settings? [y/N] n
There is one issue however, currently there's no environment variable for the Revue api set up on Vercel, so if I go to the preview generated and try submit an email I'll get the bad response message!
From the Vercel project I'll navigate to Settings > Environment Variables and add in the REVUE_API_KEY
name and value. Now I can run the Vercel CLI again and test the form again, wait for the submit then the Revue subscribers list again!
Success 🎉
Wrap up!
That's it, I've gone and added an email submit from to a site that uses the Revue API with SvelteKit endpoints!
I cann now use this pattern in other projects!