Getting Started with Astro 5: A Beginner's Guide
Learn the basics of Astro 5: A Beginner's Guide and how to set up your first project in minutes.

Astro has become a go-to framework for building content-driven websites — and for good reason. It ships zero JavaScript by default, handles routing and content collections out of the box, and lets you mix React, Vue, Svelte, or plain HTML on the same page. This guide takes you from zero to a running project, covering the concepts you will reach for every day.
Installation
The fastest way to start is create astro, which scaffolds a complete project and lets you add TypeScript and integrations interactively:
npm create astro@latest my-app
cd my-app
npm run dev
Open http://localhost:4321. You should see the Astro welcome screen. The development server supports hot module replacement — changes appear in the browser without a full reload.
Project Structure
my-app/
├── src/
│ ├── layouts/ # Reusable page shells
│ │ └── Layout.astro # Wraps every page
│ ├── pages/ # File-based routes
│ │ ├── index.astro # Home route "/"
│ │ └── blog/
│ │ ├── index.astro # "/blog"
│ │ └── [slug].astro # "/blog/:slug"
│ ├── components/ # Shared UI components
│ └── content/ # Content collections (Markdown/MDX)
├── public/ # Static assets (images, fonts)
├── astro.config.mjs # Framework configuration
└── tsconfig.json
Every file inside src/pages/ becomes a publicly accessible route. The file extension can be .astro, .md, .mdx, or .html, and dynamic routes use square brackets in the filename.
Astro Components vs Framework Components
This is the most important concept in Astro. An .astro component renders to HTML on the server and ships zero JavaScript to the browser by default:
---
// Component script — runs on the server at build time.
// Never ships to the browser.
const products = await db.query("SELECT * FROM products LIMIT 20");
---
<ul>
{products.map((p) => <li>{p.name}</li>)}
</ul>
When you need interactivity, browser APIs, or hooks like useState, bring in a framework component (React, Vue, Svelte) and hydrate it with a client:* directive — this is the islands model:
---
import Counter from "../components/Counter.jsx";
---
<!-- Static HTML by default -->
<Counter client:load />
// Counter.jsx — a React island
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
A practical rule: keep as much as possible as static .astro markup. Push client:* directives to the leaves of your component tree — small interactive islands inside a mostly-static page.
File-Based Routing
Routes map directly to the file system. No config file needed.
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/shop/[...path].astro → /shop/a/b/c
Dynamic segments use square brackets. Access the value through Astro.params:
---
// src/pages/blog/[slug].astro
const { slug } = Astro.params;
const post = await fetchPost(slug);
---
<article>{post.title}</article>
Content Collections
For Markdown and MDX content, Astro provides content collections — a type-safe way to query and validate your posts. Define a schema in src/content.config.ts:
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const blog = defineCollection({
loader: glob({ pattern: "**/*.mdx", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
tags: z.array(z.string()),
}),
});
export const collections = { blog };
Query the collection from any page or component:
---
import { getCollection } from "astro:content";
const posts = await getCollection("blog");
---
<ul>
{posts.map((post) => (
<li><a href={`/blog/${post.id}`}>{post.data.title}</a></li>
))}
</ul>
Layouts and Metadata
A layout wraps multiple pages and keeps shared markup — navigation bars, footers, and <head> tags — in one place:
---
// src/layouts/Layout.astro
const { title, description } = Astro.props;
---
<html lang="en">
<head>
<title>{title} | My Site</title>
<meta name="description" content={description} />
</head>
<body>
<nav>…</nav>
<main><slot /></main>
<footer>…</footer>
</body>
</html>
Pass per-page metadata in as props:
---
// src/pages/blog/[slug].astro
import Layout from "../../layouts/Layout.astro";
const post = await getPost(Astro.params.slug);
---
<Layout title={post.title} description={post.excerpt}>
<article set:html={post.html} />
</Layout>
Static vs Server Rendering
By default, Astro builds every page to static HTML at build time — the result loads instantly from a CDN with no server involved. For dynamic routes, tell Astro which slugs exist with getStaticPaths:
---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
const posts = await getAllPosts();
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
---
<article>{post.title}</article>
Need on-demand rendering? Add an adapter and set output: "server" (or use export const prerender = false per page) to render at request time instead.

Environment Variables
Store secrets in .env (never commit this file):
# .env
DATABASE_URL=postgres://localhost/mydb
STRIPE_SECRET_KEY=sk_test_...
# .env.example ← commit this with empty values
DATABASE_URL=
STRIPE_SECRET_KEY=
Prefix public vars with PUBLIC_ to expose them to the browser:
PUBLIC_SITE_URL=https://example.com
Access them through astro:env or import.meta.env:
const dbUrl = import.meta.env.DATABASE_URL; // server only
const siteUrl = import.meta.env.PUBLIC_SITE_URL; // server + browser
Image Optimisation
Use Astro’s built-in <Image> component for automatic WebP/AVIF conversion, lazy loading, and layout stability:
---
import { Image } from "astro:assets";
import avatar from "../assets/avatar.png";
---
<Image src={avatar} alt="User avatar" width={64} height={64} class="rounded-full" />
For images that span the full viewport, add loading="eager" to preload them and avoid LCP regressions:
<Image src={hero} alt="Hero" loading="eager" class="object-cover" />
Deploying
Push to GitHub and connect the repo on vercel.com or netlify.com. The host detects Astro automatically. Every git push triggers a deploy; every pull request gets a preview URL.
For self-hosted deployments:
npm run build # produces dist/
npm run preview # serves the built site locally
Or containerise with Docker for more control (see the Docker guide in this blog for a production-ready Dockerfile).
What to Learn Next
- Integrations — add React, Tailwind, sitemaps, and more with
npx astro add - Middleware — run code before every request in
src/middleware.ts - Server endpoints — API routes in
src/pages/api/*.ts - View Transitions — smooth, app-like page navigations with a single component
The official Astro docs are excellent and keep pace with releases. Read the “Core Concepts” section front-to-back once — it pays back quickly.
About the author
The Plake Team
Writes long-form essays on craft, software, and the slow web. Curates the Sunday digest read by 88,000+ readers.
Read moreKeep reading
Related Articles








