
What is the component script (frontmatter) in an Astro component?
The component script is the block of JavaScript or TypeScript between the two --- fences at the top of an .astro file. People often call it the frontmatter, because the fences look like Markdown frontmatter. Code in it runs on the server only, once each time the component renders: at build time for prerendered pages, or on each request for pages rendered on demand. You use it to import components, read props through Astro.props, fetch data, compute values and, on pages, control the response. Its variables are available in the template below, and none of the code is ever sent to the browser.
Understanding exactly when and where this code runs explains most of Astro's behaviour, and most of the mistakes newcomers make. In this article you'll learn what belongs in the component script, how it runs during builds and requests, what the Astro global gives you, how to fetch data and use environment variables safely, what pages can do that components can't, how to pass values to browser scripts, and the pitfalls to avoid. Every example was built and run with Astro 7.4.2.
The Basics
---
// src/pages/team/index.astro
import Base from '../../layouts/Base.astro';
import team from '../../data/team.json';
const sorted = [...team].sort((a, b) => a.name.localeCompare(b.name));
const builtAt = new Date().toLocaleDateString('en-GB');
console.log(`Rendering team page with ${sorted.length} people`);
---
<Base title="Our team">
<h1>Our team</h1>
<ul>
{sorted.map((person) => (
<li><a href={`/team/${person.slug}`}>{person.name}</a> ({person.role})</li>
))}
</ul>
<p>Built on {builtAt}</p>
</Base>
Everything above the closing --- is the component script. It imports a layout and a JSON file, sorts the data, formats a date and logs a message. The template then uses sorted and builtAt.
When you run npx astro build, the log message appears in your terminal, in the middle of the build output:
prerendering static routes
├─ /team/ada/index.html (+1ms)
├─ /team/alan/index.html (+1ms)
├─ /team/index.htmlRendering team page with 2 people
And the HTML contains only the result:
<h1>Our team</h1><ul><li><a href="/team/ada">Ada Lovelace</a> (Engineer)</li><li><a href="/team/alan">Alan Turing</a> (Researcher)</li></ul><p>Built on 24/10/2026</p>
No import, no array, no sort() and no console.log reached the browser. "Built on" shows the build date, because that's when the code ran.
What You Can Do in the Component Script
The script is a normal ES module running in Node.js (or your adapter's server runtime), written in TypeScript by default. Common uses:
- Import components: Astro components, layouts and framework components such as React or Svelte.
- Import data and assets: JSON files, images from
src/, CSS files, utility modules. - Read props:
const { title } = Astro.props; - Fetch data: from APIs, a CMS, content collections or a database, using top-level
await. - Compute values: sort, filter, format, build URLs.
- Declare types: an
interface Propsthat Astro uses to type-check how the component is used. - Export special values from pages:
getStaticPaths()andprerender.
Because the code is fenced in, you can safely work with private API keys and database credentials here. They're used on the server and only the rendered output is sent.
Top-Level Await
You don't need an async wrapper. await works directly in the script:
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog', ({ data }) => !data.draft)).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<ul>
{posts.map((post) => <li><a href={`/blog/${post.id}/`}>{post.data.title}</a></li>)}
</ul>
The template waits for the data, so it never renders a loading state. On a prerendered page, the fetch happens once during the build; on an on-demand page, it happens on every request. See how to fetch data at build time in Astro for patterns with external APIs.
Environment Variables
import.meta.env gives you variables from .env files and the environment. With this .env:
PUBLIC_API_URL=https://api.example.com
SECRET_TOKEN=abc123
this page:
---
// src/pages/env.astro
const apiUrl = import.meta.env.PUBLIC_API_URL;
const secret = import.meta.env.SECRET_TOKEN;
---
<p>API: {apiUrl}</p>
<p>Has secret: {secret ? 'yes' : 'no'}</p>
<p>Mode: {import.meta.env.MODE}, prod: {String(import.meta.env.PROD)}</p>
builds to:
<p>API: https://api.example.com</p><p>Has secret: yes</p><p>Mode: production, prod: true</p>
The secret was used in the script and never written to the output. Only variables prefixed with PUBLIC_ are available to client-side code. For on-demand rendering, prefer the type-safe astro:env API, which separates client, public server and secret server variables and keeps secrets out of your bundles. It's covered in type-safe environment variables with astro:env.
When the Script Runs
This is the most important thing to understand:
| Page type | When the component script runs | Output |
|---|---|---|
| Prerendered (the default) | Once per page, during astro build | Static HTML file |
On demand (prerender = false or output: 'server') | On every request, on the server | HTML response |
Server island (server:defer) | On the island's own request, after the page loads | HTML fragment |
Development (astro dev) | On every request to the dev server | HTML response |
The script runs once per render, top to bottom. There's no re-render when a value changes, because there's no state on the server. Values in the template are dynamic (they can come from anywhere) but not reactive (they never change after rendering). If you need something to update in the browser, that's a job for a <script> tag or an island.
The Astro Global
Inside the component script you have access to the Astro global. The most commonly used properties are:
| Property | What it gives you |
|---|---|
Astro.props | Props passed to this component, or from getStaticPaths() |
Astro.params | Dynamic route parameters, such as slug in [slug].astro |
Astro.url | A URL object for the current page |
Astro.site | Your configured site URL, if set |
Astro.request | The standard Request (headers and body need on-demand rendering) |
Astro.cookies | Read and set cookies (on-demand rendering) |
Astro.redirect() | Return a redirect response (pages only) |
Astro.response | Set status and headers (pages only) |
Astro.locals | Data set by middleware for this request |
Astro.slots | Check for and render slotted content |
Props and Types
Declare a Props interface and Astro uses it for type checking and autocompletion:
---
// src/components/Card.astro
interface Props {
title: string;
href: string;
}
const { title, href } = Astro.props;
---
<article class="card">
<h2><a href={href}>{title}</a></h2>
<slot />
</article>
Using <Card title="Missing href" /> then fails npx astro check with "Property 'href' is missing". Default values work through destructuring, as in const { greeting = 'Hello' } = Astro.props;. More on this in how to pass props to an Astro component.
Request Data on Prerendered Pages
A prerendered page is built before any visitor arrives, so there are no real request headers or cookies to read. Astro warns you if you try. This page:
---
const ua = Astro.request.headers.get('user-agent');
---
<p>{ua}</p>
produces an empty paragraph and this warning during the build:
[WARN] `Astro.request.headers` was used when rendering the route `src/pages/headers.astro'`. `Astro.request.headers` is not available on prerendered pages. If you need access to request headers, make sure that the page is server-rendered using `export const prerender = false;` or by setting `output` to `"server"` in your Astro config to make all your pages server-rendered by default.
Astro.url does work during prerendering. On the generated /team/ada/ page, Astro.url.pathname was /team/ada/.
Pages Can Export Special Values
getStaticPaths()
A dynamic route like src/pages/team/[slug].astro needs to know which pages to build. It exports getStaticPaths() from the component script:
---
// src/pages/team/[slug].astro
import type { GetStaticPaths } from 'astro';
import Base from '../../layouts/Base.astro';
import team from '../../data/team.json';
export const getStaticPaths = (() => {
return team.map((person) => ({
params: { slug: person.slug },
props: { person },
}));
}) satisfies GetStaticPaths;
const { person } = Astro.props;
---
<Base title={person.name}>
<h1>{person.name}</h1>
<p>{person.role}</p>
<p>You are viewing {Astro.url.pathname}</p>
</Base>
getStaticPaths() runs once, before the pages are rendered, and in its own scope: it can't use variables from the rest of the component script, although it can use imports. Each returned object becomes a page, with params filling the URL and props passed to Astro.props. The full API is in dynamic routes with getStaticPaths.
prerender
export const prerender = false tells Astro to render this page on each request (you need an adapter). In output: 'server' mode, export const prerender = true does the reverse.
Returning a Response
On an on-demand page, the component script can stop rendering and return a Response, including a redirect:
---
// src/pages/account.astro
export const prerender = false;
const session = Astro.cookies.get('session')?.value;
if (!session) {
return Astro.redirect('/login');
}
---
<html lang="en">
<head><title>Account</title></head>
<body><h1>Your account</h1></body>
</html>
Testing it against the built Node server:
curl -sI http://localhost:4567/account
HTTP/1.1 302 Found
location: /login
With a session cookie, the same URL returns the page. This only works in pages, not in components or layouts. By the time a component renders, Astro has already started streaming the response and its headers can't change.
Passing Values to Browser Scripts
The component script and a <script> tag in the template run in different places: the first on the server, the second in the browser. A variable from the frontmatter isn't automatically available to a browser script. Pass it through the HTML instead, usually with a data-* attribute:
---
// src/components/AstroGreet.astro
const { message = 'Welcome, world!' } = Astro.props;
---
<astro-greet data-message={message}>
<button>Say hi!</button>
</astro-greet>
<script>
class AstroGreet extends HTMLElement {
connectedCallback() {
const message = this.dataset.message;
const button = this.querySelector('button');
button?.addEventListener('click', () => {
alert(message);
});
}
}
customElements.define('astro-greet', AstroGreet);
</script>
The alternative, define:vars, also works but turns the script into an inline, unbundled script. The trade-offs are covered in how to pass data to scripts in Astro.
Common Mistakes
- Using window or document: they don't exist on the server. Move browser code into a
<script>tag or an island. - Expecting reactivity: changing a variable after rendering does nothing. Use client-side code for anything that updates.
- Putting event handlers in the template:
onclick={fn}can't carry a server function to the browser. UseaddEventListenerin a script. - Reading cookies or headers on a prerendered page: you'll get nothing and a warning. Make the page on-demand.
- Redirecting from a component: only pages can return responses or change headers.
- Leaking data through props: anything passed to a hydrated island is serialised into the HTML. Pass only what the island needs.
FAQ: The Astro Component Script
The code fence uses the same --- delimiters as Markdown frontmatter, and the idea was inspired by it. Astro's documentation calls it the component script, but both names refer to the same block at the top of an .astro file.
No. It only runs on the server, either during the build or on each request. Its output is HTML, and none of its code or variables are sent to the browser unless you deliberately put them into the markup.
Yes. Top-level await works directly, so you can await fetch(), getCollection() or a database query without wrapping anything in an async function.
In the terminal that's running astro dev or astro build, or in your server logs for on-demand pages. To log in the browser console, put console.log inside a script tag in the template.
No. getStaticPaths runs in its own scope before the page renders, so it can't see other variables in the component script. It can use imports, and it passes data to the page through props.
Yes, as long as you don't render them into the HTML or pass them as props to a hydrated island. Prefer astro:env secret variables for on-demand pages so secrets stay out of your bundles.
Conclusion
The component script is the server half of every Astro component. It runs once per render, at build time or on request, and prepares everything the template needs: imports, props, fetched data and computed values. Through the Astro global it can read the URL, params, cookies and request, and on pages it can export getStaticPaths(), opt in to on-demand rendering and return redirects or custom responses.
Keep browser concerns out of it, remember that nothing it computes is reactive, and pass values to client scripts through data attributes. With that mental model, the split between the fenced script and the template becomes the simplest part of Astro: server work goes above the fence, HTML goes below.


