Project Setup & the .astro File

Chapter 2
Project Setup & the .astro File
The frontmatter fence, expression-based markup, and props — the anatomy of one Astro component

Every .astro file has two parts: a frontmatter fence at the top for plain JavaScript or TypeScript, and a markup section below it. Both parts behave differently from every sibling framework in this series in ways worth understanding precisely, not just by analogy.

The Frontmatter Fence: Runs Once, Not Reactive

--- // this is the frontmatter — plain JS/TS const title = 'My Site'; const items = ['Alpha', 'Beta', 'Gamma']; --- <h1>{title}</h1>

The code between the two --- lines runs exactly once — at build time for a static page, or once per request in server-rendered mode (Chapter 10) — and never again after that. There's no re-run on the client, because by default there's no client-side runtime at all. This is a genuinely different mental model from Svelte's own reactive <script> block, which re-runs logic in response to state changes in the browser.

The closest real parallel: React Server Components
Among every framework covered in this series, an Astro frontmatter's "runs once, on the server, never again" behavior is closest in spirit to a Next.js React Server Component's own async function body — both execute server-side and produce static output with no client re-execution. The genuine difference is that in Astro, this behavior is the default for every component, not an opt-in mode layered onto an otherwise client-rendered framework.

Markup Expressions: JSX-Style, Not a Directive Language

--- const items = ['Alpha', 'Beta', 'Gamma']; const showList = true; --- <ul> {items.map((item) => <li>{item}</li>)} </ul> {showList && <p>The list is visible.</p>}

Astro's own markup expressions — { } — contain real JavaScript expressions, evaluated directly: .map() for lists, && or a ternary for conditionals. This is genuinely closer to React's JSX than to Vue's v-for/v-if directives or Svelte's own {#each}/{#if} block syntax — Astro doesn't introduce a separate template directive language at all; the markup expressions are JavaScript.

Props via Astro.props

--- // src/components/Badge.astro interface Props { label: string; featured?: boolean; } const { label, featured = false } = Astro.props; --- <span>{label}</span> {featured && <strong> ★ Featured</strong>}

An optional interface Props gives typed, self-documenting props — genuinely useful in TypeScript projects, and entirely optional. Destructuring Astro.props in the frontmatter is the direct equivalent of a React function component's own props parameter.

Multiple Root Elements — No Wrapping Fragment Needed

<h2>Section Title</h2> <p>Some content.</p>

Astro components can have multiple top-level elements natively — there's no historical single-root-element rule to work around the way early React needed <Fragment>/<></> for. Vue 3 and Svelte both allow this too; it's React's own older constraint (long since solved by Fragments) that made this worth calling out explicitly.

Frontmatter and Markup, Compared Across Frameworks

ConceptReact (JSX)Vue / SvelteAstro
Template expressionsReal JS, embedded directlyA separate directive language (v-for, {#each})Real JS, embedded directly — same style as JSX
Script re-runs?Every render (client)Reactively, on state changeOnce, at build/request time — never again by default
Multiple root elementsNeeds a FragmentAllowed nativelyAllowed natively

Coding Challenges

Challenge 1

Build a List.astro component that accepts an items array prop and renders it as a <ul> using a .map() expression directly in the markup.

📄 View solution
Challenge 2

Add an optional featured boolean prop with a typed Props interface, and conditionally render a badge using a {condition && ...} expression.

📄 View solution
Challenge 3

Build a component with two sibling top-level elements (no wrapping div), and confirm it renders correctly with no Fragment needed.

📄 View solution

Chapter 2 Quick Reference

  • Frontmatter fence (---) — plain JS/TS, runs once, never again on the client
  • Markup expressions ({ }) — real JavaScript, JSX-style, not a separate directive language
  • Astro.props — destructure props in the frontmatter, optionally typed via interface Props
  • No wrapping Fragment needed — multiple top-level elements are allowed natively
  • Closest real parallel — a Next.js React Server Component's own server-only execution model
  • Next chapter: file-based routing and dynamic routes