SvelteKit is a first-party framework to build websites and applications with Svelte. In this post, we’ll learn how to create a custom newsletter component for your SvelteKit project, how to use environment variables in SvelteKit, and how to use SvelteKit’s form actions to progressively enhance our newsletter component.
Creating a newsletter component
If you want to follow along from scratch, create a new SvelteKit project before getting started.
npm create svelte@latest mailcoach-sveltekit cd mailcoach-sveltekit npm install npm run dev
We’re going to create a generic <Newsletter />
that component we can embed anywhere in our app: as a CTA, in the footer… We’ll create our component in src/lib/components/Newsletter.svelte
.
<script> let subscribed = false; let error = ''; </script> <form> <label for="email"> Email <input type="email" name="email" id="email" required> </label> <button type="Submit"> Submit </button> {#if subscribed} <p>Thanks for subscribing!</p> {/if} {#if message} <p>Whoops! {message}</p> {/if} </form>
Our component contains a text input so visitors can provide their email address, and a submit button. We’ll also declare variables to the result of the subscription later. We’ll store a boolean in subscribed
to display a success message, and store the error message returned from the server in error
.
Handle submissions with form actions
Form actions live in +page.server.js
files, which run on the server. We’ll create a new routes/subscribe/+page.server.js
file, so our subscribe action is available through a /subscribe
endpoint. First we’ll declare an empty action, ready to process a request.
export const actions = { default: ({ request }) => { // … }, };
The <form>
in our Newsletter
component has an email
field. In our action, we can access the FormData
of the form that triggers a submission. Because formData
needs to be await
ed, we’ll make mark the action as async
.
export const actions = { default: async ({ request }) => { const formData = await request.formData(); const email = formData.get('email'); }, };
Next, we’ll use the fetch
API to post the new subscriber via Mailcoach’s API, parse the response, and return a result object.
export const actions = { default: async ({ request }) => { const formData = await request.formData(); const response = await fetch('???', { method: 'POST', body: JSON.stringify({ email: formData.get('email'), }), headers: { Authorization: 'Bearer: ???', Accept: 'application/json', 'Content-Type': 'application/json', }, }); const data = await response.json(); return { subscribed: response.status === 200, error: response.status === 200 ? '' : data?.message, }; }, };
When the Mailcoach API returns any other response code than 200, the user was not subscribed. In that case, we’ll also pass the API’s response message to the frontend.
There are still a few blanks we need to fill in: the URL we want to post to and our API token. Instead of hard coding them into the API request, we’ll set up a few environment variables in a new .env
file in our project’s root directory.
MAILCOACH_API_TOKEN=your-token MAILCOACH_API_ENDPOINT=https://[[your-domain]].mailcoach.app/api MAILCOACH_EMAIL_LIST_UUID=your-email-list-uuid
Under the hood, Vite (which powers SvelteKit) will read the .env
file and add it to the global process.env
variable. SvelteKit exposes these environment variables through $env/static/private
.
We don’t need to worry about exposing our credentials to the frontend. Anything in +page.server.js
will only be available to—and executed on—the server.
import { MAILCOACH_API_TOKEN, MAILCOACH_API_ENDPOINT, MAILCOACH_EMAIL_LIST_UUID, } from '$env/static/private'; export const actions = { default: async ({ request }) => { const formData = await request.formData(); const response = await fetch(`${MAILCOACH_API_ENDPOINT}/email-lists/${MAILCOACH_EMAIL_LIST_UUID}/subscribers`, { method: 'POST', body: JSON.stringify({ email: formData.get('email'), }), headers: { Authorization: `Bearer ${MAILCOACH_API_TOKEN}`, Accept: 'application/json', 'Content-Type': 'application/json', }, }); const data = await response.json(); return { subscribed: response.status === 200, error: response.status === 200 ? '' : data?.message, }; }, };
Tying a form action to a form
Now that our form action is in place, we’ll bind it to the form of our newsletter component. First, we’ll point the form action and method to our SvelteKit server action.
<script> let subscribed = null; let error = ''; </script> <form method="POST" action="/subscribe"> <!-- … --> </form>
Next, we’ll use SvelteKit’s enhance
action to progressively enhance the form to submit with AJAX. With enhance
, you can specify a callback tp execute code before the request gets sent, and after the response has been received.
<script> import { enhance } from '$app/forms'; let subscribed = null; let message = ''; </script> <form method="POST" action="/subscribe" use:enhance={() => { // Transform the request before sending… return async ({ result }) => { // Handle the response… }; }}> <!-- … --> </form>
For our newsletter subscription, we don’t need to touch the request as the email
field will already be included in the form’s data. We do however want to read the response and update our error
and success
state accordingly.
<script> import { enhance } from '$app/forms'; let subscribed = null; let error = ''; </script> <form method="POST" action="/subscribe" use:enhance={() => { return async ({ result }) => { subscribed = result.subscribed; error = result.error; }; }}> <!-- … --> </form>
Progressive enhancement
As a cherry on the pie, we’re going to make sure our form behaves well when JavaScript is not available. Form actions are progressively enhanced by default. They run on the server, then display the page component defined for their URL (in our case, src/routes/subscribe/+page.svelte
).
Since we don’t have a page component for the /subscribe
URL yet, SvelteKit would throw a server error after processing the data. We’ll create a src/routes/subscribe/+page.svelte
page.
When SvelteKit redirects a form action to a page, it will populate a form
property and pass it to the page. First we’ll check if the form
prop is populated. If it is, we know the user attempted to subscribe and we’ll display the result. If form
is null
, it’s a direct visit to /subscribe
, and we’ll display our newsletter form instead.
<script> import Newsletter from "$lib/components/Newsletter.svelte"; export let form; </script> {#if form} {#if form.subscribed} <p>Thanks for subscribing!</p> {:else} <p>Whoops! {form.message}</p> {/if} {:else} <Newsletter /> {/if}
To summarize:
-
When JavaScript is enabled, our callback in
use:enhance
will hijack the form submission, send it with AJAX, and process the response in place. -
When JavaScript is not available, the browser will make a traditional form request to
/subscribe
, SvelteKit will process the request on the server, and redirect to our subscribe page. -
If a user would somehow land on
/subscribe
without submitting a form, the page will fall back to a generic subscription page.
With SvelteKit, we get the best of all worlds!