Astro Crash Course

Astro Build Log

A complete crash course: how Astro thinks, every core API, moving a vanilla site or a React app over, and a full boilerplate you can copy file by file.

01Mental model

Astro is a content-first framework. It renders HTML on the server, either once at build time or on each request, and ships zero client JavaScript unless a component asks for it. Treat it as HTML templates with superpowers, not as a React app that happens to output HTML.

Three ideas carry the whole framework. Every page is a real route in a multi-page app, so navigation is a normal page load. Components run once on the server and become static HTML. When a piece of UI needs interactivity, you mark it as an island and only its JavaScript reaches the browser. React, Vue, Svelte, Solid and Preact components can all be islands, even on the same page.

Good fitWeak fit
Blogs, docs, marketing sites, portfolios, storefronts, dashboards with mostly static chromeApps where nearly every pixel is stateful (an editor, a mail client). They can live inside Astro as one large island, but most of the benefit disappears.
Version notes

High confidence, verified against the Astro 6 release post: Astro 6 is the current stable major, with a redesigned dev server, stable live content collections, a stable CSP API, a stable Fonts API, and Zod imported from astro/zod.

From memory, worth checking in the docs before pinning: the exact latest 6.x minor, the Node.js minimum (a third-party tutorial says 22.x), the live collections config file name, and the exact shape of the fonts config option.

02Setup and project structure

One command scaffolds a project. Pick the minimal or blog template when it asks.

dev starts the dev server on port 4321. build writes the production site to dist/, and preview serves that build locally. astro add installs an integration and edits astro.config.mjs for you; it is the safest way to add React, MDX, a sitemap or a deployment adapter. astro check type-checks your .astro files.

PathWhat lives there
public/Copied as-is to the site root: favicons, robots.txt, files you do not want processed
src/pages/The only folder Astro enforces. Every file here becomes a route.
src/components/Reusable .astro and framework components (convention)
src/layouts/Page shells with html, head, nav and footer (convention)
src/assets/Images you want optimized and hashed at build
src/content.config.tsContent collection definitions
src/middleware.tsCode that runs before requests (optional)
src/actions/Type-safe server functions (optional)
astro.config.mjsIntegrations, adapter, output mode, site URL

03Anatomy of a .astro component

A .astro file has two parts. The frontmatter between the --- fences runs on the server only. The template below it looks like HTML with JSX-style expressions.

FrontmatterPlain TypeScript that runs once per render on the server. Top-level await works, secrets stay safe, and nothing here reaches the browser. interface Props types the props and Astro.props reads them. You will also use Astro.cookies, Astro.request, Astro.url, Astro.params and Astro.redirect() a lot.

Template{} holds expressions. Conditionals use && or ternaries, lists use .map(), as in JSX. The differences: write class instead of className, attributes are plain HTML, class:list builds class strings from arrays and objects, and there are no hooks or state because the template runs once.

Slots<slot /> is Astro's version of children. A named slot is filled by a child carrying slot="footer". Content inside the slot tag is the fallback.

<style>Scoped to this component automatically. Use <style is:global> or :global(.x) to escape the scope.

<script>Client JavaScript. Astro bundles it, compiles TypeScript and imports, loads it as a module, and includes it once even if the component appears ten times. Add is:inline to ship it exactly as written instead.

Passing server data to a client script

Render the data into the DOM, then read it in the script. A custom element is the cleanest version of this pattern.

The server renders a custom element with a data-api attribute. The bundled script defines the element once, and the browser upgrades every copy on the page, including copies that arrive later through view transitions. For one-off values you can also use define:vars={{ value }} on a script, which makes it inline.

04Routing

Routes are files in src/pages/. Pages can be .astro, .md, .mdx (with the integration) or .html. .ts and .js files become endpoints.

FileURL
src/pages/index.astro/
src/pages/about.astro/about
src/pages/blog/index.astro/blog
src/pages/blog/[slug].astro/blog/hello-world
src/pages/docs/[...path].astro/docs/a/b/c (rest parameter)
src/pages/api/users.ts/api/users (endpoint)
src/pages/404.astroCustom not-found page
src/pages/_draft.astroIgnored because of the underscore

Static dynamic routes

In static mode, the default, a dynamic route lists every path at build time with getStaticPaths.

getStaticPaths runs once at build and returns one object per page. params fills the bracketed segment; props goes straight to the page so you never refetch. render(post) turns a Markdown entry into a <Content /> component plus its heading list, which you can use for a table of contents.

Pagination is built in: return paginate(posts, { pageSize: 10 }) from getStaticPaths in a [page].astro file, then read Astro.props.page for data, url.prev, url.next and currentPage.

On-demand routes

With an adapter installed (section 08), prerender = false renders the page per request. There is no getStaticPaths; you read Astro.params directly and can return a redirect or a Response from the frontmatter.

Endpoints

Each exported HTTP method returns a standard Response. The context gives you params, request, url, cookies, locals and redirect. Leave out prerender = false and the endpoint runs once at build and writes a file, which is how rss.xml and JSON feeds are generated. Permanent redirects belong in config: redirects: { '/old': '/new' }.

05Layouts and styling

A layout is an ordinary component that renders the whole HTML document and exposes a slot for the page.

Importing global.css in the layout applies it to every page that uses the layout. Astro.site comes from the site config option and is needed for canonical URLs, sitemaps and RSS. <ClientRouter /> is optional and turns on view transitions. The named head slot lets a page add its own meta tags:

ApproachHowUse for
Scoped styles<style> in any .astro fileThe default for component styles
Global CSSimport '../styles/global.css' in a layoutResets, tokens, typography
Props into CSS<style define:vars={{ color }}>Server values as CSS variables
Tailwindnpx astro add tailwindUtility-first styling. I believe it now wires up Tailwind v4 through its Vite plugin; confirm after install.
Sass or LessInstall the preprocessor, then <style lang="scss">Existing Sass codebases
CSS Modulesimport s from './x.module.css'Mostly inside React islands
Gotcha

Scoped styles do not reach into child components. Style the child in its own file, pass a class prop through, or use :global() in the parent.

06Islands and client directives

A framework component with no directive renders to static HTML and ships nothing. Add a client:* directive and it becomes an island: Astro sends its HTML plus the code to hydrate it, and each island hydrates on its own.

DirectiveHydrates whenUse for
noneNeverAnything that only displays data
client:loadRight awayAbove-the-fold UI that must work at once
client:idleBrowser is idleLower-priority widgets
client:visibleScrolled into viewComments, charts, footers
client:media="…"A media query matchesMobile-only menus
client:only="react"Client only, no server renderCode that touches window or browser-only libraries

Props must serializeStrings, numbers, booleans, arrays, plain objects, Date, Map, Set, URL and RegExp cross the boundary. Functions do not, so an .astro parent cannot pass an onClick. Keep event handling inside the island.

ChildrenChildren passed to an island are rendered on the server and arrive as static HTML (children in React). Static content can sit inside an interactive shell.

No shared contextEach island is its own React root, so context providers do not span islands. Share state with a small store such as Nano Stores, custom DOM events, or the URL.

src/stores/cart.ts
src/components/CartBadge.tsx

The store exports one atom any island can import. useStore subscribes the badge, so when another island calls cartCount.set(3) the badge updates, even though the two islands are separate React roots. A vanilla <script> can import the same atom and call .subscribe().

07Content collections

Collections give you typed, validated content from Markdown, MDX, JSON, YAML or any remote source. Define them once with a loader and a Zod schema.

globReads a folder of files; each file path becomes the entry id.

fileReads one JSON or YAML file holding an array or a keyed object.

inline loaderAny async function returning objects with an id. It runs at build, so remote data becomes static content.

schemaValidates at build and generates types. image() validates and optimizes local images; reference() links to entries in another collection. In Astro 6, import z from astro/zod.

getCollection returns every entry, with an optional filter; here drafts show in dev and disappear in production. getEntry resolves a reference to the full entry. The body is rendered with render(entry), shown in section 04.

Live collectionsStable in Astro 6. They fetch at request time instead of build time, for content that must change without a rebuild. Define them with defineLiveCollection (I believe in src/live.config.ts) and read them with getLiveCollection and getLiveEntry, which return { entry, error }. They need on-demand routes. Prefer build-time collections unless freshness matters.

08Server features

Every route is prerendered by default. To render routes per request, add an adapter for your host and opt those routes out of prerendering.

SetupBehaviour
output: 'static' + adapterEverything prerendered; pages with prerender = false render on demand
output: 'server' + adapterEverything on demand; pages with prerender = true are prerendered
No adapterPure static site; prerender = false is an error

Official adapters: @astrojs/node, @astrojs/vercel, @astrojs/netlify, @astrojs/cloudflare. Install with npx astro add <name>.

Server islands

server:defer lets a static, cacheable page hand one personalised piece to the server. The page loads at once with the fallback, then UserMenu is rendered separately (it can read cookies and query a database) and swapped in. Needs an adapter.

Actions

src/actions/index.ts
src/pages/subscribe.astro

Every key on server becomes an action. accept: 'form' parses FormData and validates it before the handler runs; context holds cookies, locals and the request. The page posts the form straight to the action with no client JavaScript and reads the result on re-render. From a React island you call await actions.subscribe(formData) and get { data, error }.

Middleware

auth reads the session cookie, stores the user on locals (read it as Astro.locals.user in any page), and redirects anonymous visitors away from /admin. timing wraps next() to add a header after the page renders. sequence runs them in order. Type locals by declaring App.Locals in src/env.d.ts. In Astro 6, middleware also runs during the build for prerendered pages, where there is no real request, so guard cookie and header reads.

Environment variables

Any .env value is available on the server as import.meta.env.NAME; only names starting with PUBLIC_ reach client code. The schema above adds validation and types: import secrets from astro:env/server and public values from astro:env/client.

09Vanilla HTML/CSS/JS to Astro

Any valid HTML is valid Astro. Rename index.html to src/pages/index.astro and it works. Everything after that is refactoring for reuse.

Vanilla siteAstro equivalent
about.html, contact.htmlsrc/pages/about.astro, src/pages/contact.astro
Header and footer pasted into every filesrc/layouts/BaseLayout.astro with a slot
Repeated card markupsrc/components/Card.astro with props
<link rel="stylesheet" href="style.css">Import in the layout, or scoped <style>
<script src="main.js"><script> in the component that needs it
images/src/assets/ to optimize, public/ to leave untouched
Hand-maintained list of postsA content collection and getCollection
jQuery-style DOM wiring<script> plus a custom element
  1. Move static files

    Put favicon.ico, robots.txt and anything referenced by absolute URL into public/. Paths like /img/logo.png keep working.

  2. Extract the shell

    Move <html>, <head> and the nav into BaseLayout.astro (section 05). Each page shrinks to its own content.

Before · about.html
After · src/pages/about.astro

Links change from /about.html to /about.

  1. Convert scripts per component

    A global main.js still works if the layout imports it inside a <script>. Better, move each behaviour next to its markup so it ships only on pages that use it.

The frontmatter takes a typed list and the template maps it to native <details> elements. The script is bundled once per page, even with several accordions, and closes the other panels when one opens.

  1. Plan for view transitions

    If you add <ClientRouter /> later, scripts that run once on load do not re-run after client navigation. Wrap setup in document.addEventListener('astro:page-load', () => { … }), or use custom elements, which upgrade automatically.

  2. Turn repetition into content

    Ten near-identical project pages become one collection and one [slug].astro route.

10React (Vite or CRA) to Astro

The shift: in a React SPA everything is client-side by default and you fetch in effects. In Astro everything is server-rendered by default, data loads in frontmatter, and React stays only where there is real interaction.

React SPAAstro
React Router <Routes>Files in src/pages/
useParams()Astro.params or getStaticPaths
useEffect + fetch for page dataawait fetch() in frontmatter
index.html + <App /> shellA layout component
react-helmet for titles and metaProps on the layout, or a named head slot
<Link to="/x"><a href="/x">, plus <ClientRouter /> for SPA-like transitions
Context provider at the rootNano Stores, or keep the provider inside one island
import.meta.env.VITE_X / REACT_APP_Ximport.meta.env.PUBLIC_X or astro:env/client
Static presentational componentsRewrite as .astro, or render as React with no directive
Stateful widgetsKeep as .tsx with client:*

Path A: incremental, keep the SPA running

Install React with npx astro add react, copy src/ components across, and mount the whole app as one client-only island behind a catch-all route. Then peel off pages into real Astro routes one at a time.

The rest parameter sends every /app/* URL to this one page, and client:only skips server rendering so browser-only code inside the app keeps working. Set React Router's basename to /app. This needs an adapter because the paths are unknown at build. On a pure static host, use a single app.astro page and a host rewrite of /app/* to it instead.

Path B: convert a page properly

Before · Posts.tsx (SPA)
After · src/pages/posts/index.astro

The loading state, the two pieces of state and the effect all disappear: the list is fetched on the server and arrives as finished HTML. Link becomes a plain anchor. LikeButton is untouched React code that now hydrates only when scrolled into view, so the rest of the list ships no JavaScript. API_URL has no PUBLIC_ prefix because it is only read on the server.

Converting a presentational component

Before · Badge.tsx
After · Badge.astro

className becomes class (here class:list), children becomes <slot />, and the types move into interface Props. Only convert components with no state or handlers; anything with useState stays React.

React gotchas

Hooks only run inside islands. Code that touches window at import time breaks server rendering; move it into an effect or use client:only. Global CSS imported inside a React component still works and is bundled by Vite.

11Full boilerplate

A complete starter: layout with SEO, a typed blog collection, RSS, sitemap, a vanilla theme toggle, one React island, a 404 page and a health endpoint. Pick a file to read it. Create the project with npm create astro@latest (minimal template), then run npx astro add react mdx sitemap and npm i @astrojs/rss so the package versions come from your install.

12Essentials

Images

Importing from src/assets/ gives Astro the image's real size, so it can resize, convert to WebP and set width and height to stop layout shift. <Picture> outputs several formats. Remote images need explicit dimensions, and their domains must be listed in image.domains in config to be optimized. I am fairly sure priority is the current prop for eager-loading a hero image; check it if your version complains. Files in public/ are never optimized.

Fonts

The Fonts API downloads and self-hosts the font at build, generates fallbacks and exposes it as a CSS variable. Both imports are confirmed by the Astro 6 post; the exact option names inside each entry are from memory, so compare with the Fonts guide.

View transitions

<ClientRouter /> (formerly <ViewTransitions />) intercepts link clicks and animates between pages. transition:name pairs elements across pages for morph animations; transition:persist keeps an element alive, such as a playing audio player. Use astro:page-load for setup code. Without the router, modern browsers can still do cross-document transitions with the CSS @view-transition { navigation: auto; } rule.

Prefetch, Markdown and SEO

NeedHow
Faster navigationprefetch: true in config; per link data-astro-prefetch="viewport"
Components in Markdownnpx astro add mdx, then import components inside .mdx
Code highlightingShiki is built in; set markdown.shikiConfig.theme
Sitemap@astrojs/sitemap with site set
RSS@astrojs/rss in an endpoint (boilerplate)
Glob-import filesimport.meta.glob('./posts/*.md', { eager: true }); Astro.glob is gone

Deployment

HostStatic siteWith on-demand routes
VercelImport the repo; build npm run build, output distnpx astro add vercel
NetlifySame build settingsnpx astro add netlify
CloudflarePages or Workers static assetsnpx astro add cloudflare
VPS, Docker, Raspberry PiServe dist/ with nginx or Caddy@astrojs/node standalone, then node dist/server/entry.mjs
GitHub PagesOfficial withastro/action workflow; set base for project pagesNot supported

13Cheat sheet and gotchas

Virtual modules

Import fromWhat you get
astro:contentdefineCollection, getCollection, getEntry, render, reference
astro/loadersglob, file
astro/zodz
astro:assetsImage, Picture, getImage, Font
astro:transitionsClientRouter, navigate
astro:actionsdefineAction, actions, ActionError
astro:middlewaredefineMiddleware, sequence
astro:env/server, astro:env/clientTyped environment variables

The Astro global

PropertyMeaning
Astro.propsProps passed to the component
Astro.paramsDynamic route segments
Astro.url, Astro.requestCurrent URL and request
Astro.cookiesget, set, has, delete (on-demand pages)
Astro.localsData set by middleware
Astro.redirect(url, status)Return a redirect from frontmatter
Astro.siteThe site config value as a URL
Astro.slots.has('name')Whether a named slot was filled

Template directives

DirectiveEffect
client:*Hydrate a framework component (section 06)
server:deferRender a component later on the server
class:listBuild a class string from arrays and objects
set:htmlInsert raw HTML (trusted content only)
set:textInsert escaped text
is:inlineShip a script or style unprocessed
is:globalUnscoped style block
define:varsPass server values into a style or script
transition:name, transition:persistView transition pairing and persistence

Gotchas

SymptomCauseFix
React button does nothingNo client:* directiveAdd client:load or client:visible
window is not definedBrowser code runs during server renderMove it into an effect, or use client:only
Function prop is undefined in an islandFunctions do not serializeHandle events inside the island
Script stops working after navigationClientRouter swaps the page without reloadingUse astro:page-load or custom elements
Styles do not reach a childScopingPass class, or use :global()
getStaticPaths required errorDynamic route in static modeReturn paths, or set prerender = false with an adapter
Cookies empty or warnings at buildReading request data in a prerendered pageMake that route on-demand
Secret shows up in client bundleRead in a client script or PUBLIC_ prefixRead it in frontmatter, use astro:env/server
Zod import error after upgradingAstro 6 moved itImport z from astro/zod
Wrong canonical or sitemap URLssite missingSet site in config

Astro Crash Course · written October 2026 for Astro 6. Source for the version facts: astro.build/blog/astro-6. Full reference: docs.astro.build.