Tutorial

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.

Plake
Feb 15, 20266 min read1,029 words
Getting Started with Astro 5: A Beginner's Guide

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.

Astro project overview illustration

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.

Share:

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 more

Keep reading

Related Articles

Back to AllBack to All
Deploying Astro Applications to the Cloud

Apr 1, 2026

5 min

Deploying Astro Applications to the Cloud

Web Performance Optimization Techniques

Mar 20, 2026

8 min

Web Performance Optimization Techniques

Mastering Async/Await in JavaScript

Mar 5, 2026

7 min

Mastering Async/Await in JavaScript