Using cookies for displaying dynamic status messages with Astro

by Sean C Davis

Status messages are a crucial component of almost any website. From authentication to form submissions, you often need the ability to post unique messages to specific users while the rest of the content on the page remains unaffected.

#TL;DR

We’re going to walk through how to work with status messages in Astro using server cookies. We’ll start with a fresh Astro project, walk through the basics, and end with some ideas on how you can make this pattern work for your site.

See this in action before we begin by visiting the demo site, or deploy the code to your own Netlify site.

Deploy to Netlify

#Create a new Astro site

Let’s get started by creating a new Astro site.

Terminal window
npm create astro@latest

As you step through the setup wizard, choose TypeScript (our example uses TypeScript) and install dependencies.

These were the choices I made for this example. I rolled the dice on project name, but also chose to start with an empty project. I did this so that we could focus purely on the pattern here. If you start with the basics, that’s fine, but your site will look different than the screenshots below.

Where should we create your new project? (default)
How would you like to start your new project? Empty
Do you plan to write TypeScript? Yes
How strict should TypeScript be? Strict
Install dependencies? Yes
Initialize a new git repository? Yes

#Configure SSR

Make sure that your site is configured to use server-side rendering by default. Otherwise, the frontmatter in our Astro page templates won’t be processed on every page load. We’ll see how that comes into play shortly.

// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
output: "server",
});

#Run development server

That should be all you need to boot the server up and see the site running on localhost:4321.

Terminal window
npm run dev

#How we’ll set dynamic messages

Before we go further, let’s pause to talk about how this will work.

In most cases, status messages are set as the result of some dynamic behavior of the site, such as a form submission, API, call, or other attempt at data manipulation.

#Working with “action” endpoints

To keep things tidy in Astro, I try to keep as much of the server-side logic in “action” endpoints. These endpoints do the work they need to do on the server with no rendering counterpart. The return value is always a redirect to the appropriate page.

#Using server cookies

The challenge in this process is that the endpoint and the page are separated by a request, so we have to find a way to communicate between the two. In some cases, this might be best persisted in the site’s data store (like a remote database). In this case, we’re dealing with a one-time communication that simply needs to span from the endpoint response to the page request and response.

Server cookies are a great way to bridge that gap. We can store our message as a cookie. Then, when the page loads, it will read the cookie, store the value in a variable, and display it on the page. The page will also be responsible for deleting the cookie so that the message only appears once.

#Accepting POST requests

In this case, I wanted to mimic working with a more typical situation, so we’re going to build our endpoint such that it only supports POST requests. We’ll see that come into play shortly.

Okay, back to the code.

#Create simple status message

To start with, let’s build out a hard-coded “Success!!!” message.

#Add button to the home page

To submit a POST request, we’re not just going to add a link, but will instead mimic submitting a form. Add the form with the button somewhere on your home page (src/pages/index.astro).

<form action="/actions/set-status" method="post">
<input type="submit" value="Show status message" />
</form>

If using the blank starter, your site will now look something like this:

CleanShot 2024-03-20 at 06.31.55@2x.png

#Wire up the endpoint

Add the action endpoint (src/pages/actions/set-status.ts) with the following code:

import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ cookies, redirect }) => {
cookies.set("status", "Success!!!", {
httpOnly: true,
sameSite: "strict",
path: "/",
});
return redirect("/");
};

A few important notes here:

  • Using httpOnly is what designates the cookie as a server cookie.
  • path being set to / ensures we can access the cookie from other pages (even though we’re only using it on the home page in this example).
  • We return a redirect call to avoid rendering anything at this endpoint. It’s just server behavior. We want the behavior to appear as simply reloading the home page.

#Retrieve and display the status

At this point, the button and endpoint are working properly. You could even try it. Click the button and use your browser’s developer tools to see that the cookie is set.

But the user wouldn’t know this because we’re not displaying anything on the home page. Let’s add the mechanism for reading the cookie to the home page.

---
const status = (Astro.cookies.get("status") || {})?.value;
---
<html lang="en">
<!-- <head> things ... -->
<body>
{status &&
<p>{status}</p>
}
<h1>Astro</h1>
<form action="/actions/set-status" method="post">
<input type="submit" value="Show status message" />
</form>
</body>
</html>

Click the button and you should see your message!

CleanShot 2024-03-20 at 06.31.55@2x.png

Also notice that if you refresh the page (without clicking the button) the status remains. We want this to be ephemeral.

To do this, add a line to delete the cookie after you’ve stored it in the status variable.

---
const status = (Astro.cookies.get("status") || {})?.value;
Astro.cookies.delete("status");
---
<!-- page HTML code ... -->

#Add custom status messages

To have a little fun and show how we can extend this, let’s add a required text field to a form that we can use to set the status value.

Add the form to the home page.

<form action="/actions/set-status" method="post">
<input type="text" name="message" required />
<input type="submit" value="Set status message" />
</form>

Then get the form data from the request in the action and use that as the cookie value.

import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ request, cookies, redirect }) => {
const formData = await request.formData();
const message = formData.get("message");
cookies.set("status", message || "(message not set)", {
httpOnly: true,
sameSite: "strict",
path: "/",
});
return redirect("/");
};

Try it again. Submit the form…

CleanShot 2024-03-20 at 06.31.55@2x.png

…and the success message appears!

CleanShot 2024-03-20 at 06.31.55@2x.png

That’s it! You’ve done the basics. Continue below for deployment instructions, along with some gotchas and suggestions for next steps.

#Configure the Netlify adapter

To deploy this to Netlify, you’ll need the Netlify adapter, which will ensure SSR works properly. Astro has a convenient tool for this.

Terminal window
npx astro add netlify

This should configure your astro.config.mjs to include the Netlify adapter.

import { defineConfig } from "astro/config";
import netlify from "@astrojs/netlify";
export default defineConfig({
output: "server",
adapter: netlify(),
});

#Gotchas!

There are a few gotchas! that I wanted to note. I ran into some trickiness with this pattern as my applications have grown in complexity.

#Noting the importance of the response

Cookies get set through the Set-Cookie header, which means they will only stick in places where the HTTP response is involved. You can’t set or clear the cookie from a layout or component. Even though you have server access, you don’t have access to manipulate the response, so the cookie set/delete action won’t stick.

#Be careful with client-side requests

This works particularly well because we’re using traditional HTTP request/response patterns. When you start submitting requests asynchronously from the client, you have to be extra cautious of this pattern. If you set a cookie during a client-side request, you can’t delete it without another request, which means it might show up after the user has loaded the page again.

#Account for multiple redirects

Be mindful of where you delete the cookies. As your site grows, you’ll likely implement various utilities that keep your code DRY and handle deleting these cookies. When the data logic grows in complexity, that can mean multiple redirects based on specific flows. Be mindful of where you’re setting and deleting cookies — it’s easier than you might think to delete in the middle of a redirect chain.

#What next?

I hope you’ll take this pattern and apply it to your Astro site. Here are a couple suggestions on next steps:

  • Use a shared utility to set and delete the cookies (which you call from page files) so that as your cookie logic becomes more complex, most of that complexity resides in one place.
  • Use a shared global component for displaying the feedback, so that it looks the same on all pages. (You still need to set and delete the cookie from the page.)
  • Routing becomes important when using this approach and can easily get tangled up. I like to use a constant that becomes the source of truth for all routes so that I can make structural changes with confidence.