Project Setup & the .astro File
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
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.
Markup Expressions: JSX-Style, Not a Directive Language
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
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
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
| Concept | React (JSX) | Vue / Svelte | Astro |
|---|---|---|---|
| Template expressions | Real JS, embedded directly | A separate directive language (v-for, {#each}) | Real JS, embedded directly — same style as JSX |
| Script re-runs? | Every render (client) | Reactively, on state change | Once, at build/request time — never again by default |
| Multiple root elements | Needs a Fragment | Allowed natively | Allowed natively |
Coding Challenges
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 solutionAdd an optional featured boolean prop with a typed Props interface, and conditionally render a badge using a {condition && ...} expression.
📄 View solutionBuild a component with two sibling top-level elements (no wrapping div), and confirm it renders correctly with no Fragment needed.
📄 View solutionChapter 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 viainterface 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