The Component Script
Every .astro file can open with a block of JavaScript or TypeScript fenced by a pair of triple dashes (---). Astro borrows the term frontmatter from Markdown, but this fence is far more powerful: the code inside runs only on the server, at build time (or on each request in SSR mode), and never ships to the browser. This is where you import components, fetch data, read props, and compute the values your template will render — and understanding what lives in the script versus the template is the key to writing fast, zero-JS-by-default pages.
What the component script is
The component script is the region between the two --- fences at the top of a .astro file. Astro evaluates it in a server context, then discards the source before sending HTML to the client. Anything you console.log here appears in your terminal, not the browser console.
---
// src/components/ProductCard.astro
import Badge from "./Badge.astro";
const { name, price } = Astro.props;
const onSale = price < 50;
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(price);
---
<article class="card">
<h2>{name}</h2>
<p>{formatted}</p>
{onSale && <Badge label="On sale" />}
</article>
The fence supports standard ES module syntax plus top-level await, so you can write straightforward asynchronous code without wrapping it in an IIFE.
Astro strips the component script from the final bundle. None of the variables, imports, or logic above the fence reach the browser unless you explicitly pass them into a client-side island.
Importing and computing values
The most common job of the script is wiring together other components and deriving the data your template needs. You can import other .astro files, UI framework components, JSON, images, and any npm package.
---
import Layout from "../layouts/Layout.astro";
import { Image } from "astro:assets";
import heroImg from "../assets/hero.png";
const links = ["Docs", "Blog", "Pricing"];
const year = new Date().getFullYear();
---
<Layout title="Home">
<Image src={heroImg} alt="Hero banner" />
<nav>{links.map((l) => <a href={`/${l.toLowerCase()}`}>{l}</a>)}</nav>
<footer>© {year} DevCraftly</footer>
</Layout>
Fetching data with top-level await
Because the script runs on the server, you can call fetch(), hit a database, or read content collections directly — no loading spinners, no client-side waterfalls. The fetched data is baked into static HTML at build time, or resolved per request when rendering on demand.
---
interface Post {
id: number;
title: string;
}
const res = await fetch("https://api.example.com/posts");
const posts: Post[] = await res.json();
---
<ul>
{posts.map((post) => <li>{post.title}</li>)}
</ul>
To pull from local Markdown or MDX, use the content collections API instead of fetch:
// inside a .astro fence
import { getCollection } from "astro:content";
const articles = await getCollection("blog", ({ data }) => !data.draft);
articles.sort((a, b) => +b.data.pubDate - +a.data.pubDate);
Accessing props and the Astro global
The component script reads incoming props from Astro.props and exposes the request, params, and rendering utilities through the Astro global object. A few of the most useful members:
| Member | Purpose |
|---|---|
Astro.props | Attributes passed to this component |
Astro.params | Dynamic route segments (e.g. [slug]) |
Astro.request | The standard Request object (SSR) |
Astro.url | A URL instance for the current page |
Astro.slots | Inspect and render slotted content |
Astro.redirect() | Issue a redirect response (SSR) |
---
const { slug } = Astro.params;
const isAdmin = Astro.url.searchParams.get("role") === "admin";
console.log("Rendering slug:", slug);
---
Output:
Rendering slug: getting-started
Server-only versus client-side code
The fence runs only once on the server, so it cannot use browser APIs like window, document, or localStorage. Code that needs the browser belongs in a <script> tag in the template, or inside a framework island hydrated with a client:* directive.
| Concern | Component script (---) | Client <script> / island |
|---|---|---|
| Runs where | Server (build or request) | Browser |
| Has access to | fetch, DB, file system, env | window, document, events |
| Shipped to client | No | Yes |
| Reruns on navigation | No (rendered once) | Yes (per page) |
---
const count = 5; // server value, computed once
---
<button id="inc">Clicked 0 times</button>
<script>
// runs in the browser
let n = 0;
document.getElementById("inc")?.addEventListener("click", (e) => {
n++;
(e.target as HTMLButtonElement).textContent = `Clicked ${n} times`;
});
</script>
Need a server value inside a client island? Pass it as a prop to a framework component (
<Counter client:load start={count} />). Plain<script>tags are isolated and do not see fence variables directly.
Best practices
- Keep the script focused on data prep and imports; push presentation logic into the template with expressions like
{condition && <El />}. - Use top-level
awaitfor data fetching instead of client-side requests — it is faster and ships zero JavaScript. - Prefer
getCollection/getEntryfor local content over rawfetchso you get type safety and validation. - Never reach for
windowordocumentin the fence; that code runs on the server and will throw. - Type your
Astro.propswith aninterface Props {}so editors and the build catch shape mismatches. - Remember the script runs once per render — avoid putting interactive state there; that belongs in an island.