Astro page layout and middleware execution order
By Flavio Copes
Understand the order pages, layouts, and middleware run in Astro: page code runs when you call next(), but layout code runs after the middleware finishes.
In Astro, the page frontmatter runs when the middleware calls next(), but the layout frontmatter runs later, after your middleware code has finished. I discovered this the hard way (through trial and error), and it matters as soon as you try to pass data between a layout and the middleware.
Quick recap: Astro middleware is a function that runs on every request, before the page is rendered. You can inspect the request, set values in locals, and call next() to let Astro render the page. Whatever next() returns is the response.
Here’s what happened to me.
I was doing something related to caching, and I had a workflow where I was doing something in a page, like setting a response header, or setting a value in Astro.locals:
---
Astro.locals.test = 'test'
Astro.response.headers.set('test', 'test')
---
<p>test</p>
After doing so, I had access to those values after calling next() in the middleware:
import type { MiddlewareHandler } from 'astro'
export const onRequest: MiddlewareHandler = async (context, next) => {
const response = await next()
console.log(context.locals.test)
console.log(response.headers.get('test'))
return response
}
Both logs printed test. So far so good.
Then I moved the code to a layout
I decided to move some of the logic I had in the page into a layout, because I was duplicating some portion of code across multiple pages:
---
import Layout from '@layouts/Layout.astro'
---
<Layout />
In this layout I did the exact same thing I had in the page, previously:
---
Astro.locals.test = 'test'
Astro.response.headers.set('test', 'test')
---
<p>test</p>
But to my surprise, none of those values were now available in the middleware. Both logs printed nothing useful.
Why does this happen?
Turns out that (to my understanding) the order of execution is different.
The page frontmatter runs when you call next() in the middleware. The layout, though, is a component the page renders in its template. Its frontmatter runs during rendering, and Astro streams the rendered HTML. So by the time the layout code runs, await next() has already returned and my middleware code after it had already executed.
Same file structure, completely different timing.
This also explains why setting a response header from a layout is unreliable: the response may already be on its way to the browser.
The fix
To fix my problem I eventually moved some of the logic I had in the middleware to my layout.
The general rule I follow now: if the middleware needs to read a value after next(), set that value in the page frontmatter, not in a layout. If the logic must live in the layout, move the middleware code that depends on it into the layout too.
Related posts about astro: