Skip to content
Astro as components 4 min read

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:

MemberPurpose
Astro.propsAttributes passed to this component
Astro.paramsDynamic route segments (e.g. [slug])
Astro.requestThe standard Request object (SSR)
Astro.urlA URL instance for the current page
Astro.slotsInspect 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.

ConcernComponent script (---)Client <script> / island
Runs whereServer (build or request)Browser
Has access tofetch, DB, file system, envwindow, document, events
Shipped to clientNoYes
Reruns on navigationNo (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 await for data fetching instead of client-side requests — it is faster and ships zero JavaScript.
  • Prefer getCollection / getEntry for local content over raw fetch so you get type safety and validation.
  • Never reach for window or document in the fence; that code runs on the server and will throw.
  • Type your Astro.props with an interface 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.
Last updated June 14, 2026
Was this helpful?