Your First Astro Project
The fastest way to understand Astro is to build something with it. In this quick start you will scaffold a new project, look at how its files are organized, edit the home page, and run the development server to watch your changes hot-reload in the browser. By the end you will have a working page rendered as zero-JavaScript HTML and a clear mental model of where everything lives.
Scaffold a new project
Astro ships a create command that sets up a project, installs dependencies, and optionally initializes git. Run it with your package manager of choice:
npm create astro@latest my-first-astro
The wizard asks a few questions. Choose the “Empty” template for the smallest possible starting point, pick TypeScript: Strict for the best authoring experience, and let it install dependencies. Then move into the folder and start the dev server:
cd my-first-astro
npm run dev
Output:
astro v5.0.0 ready in 312 ms
┃ Local http://localhost:4321/
┃ Network use --host to expose
watching for file changes...
Open http://localhost:4321/ and you will see your first page. The server stays running and rebuilds instantly as you edit files.
Tip: The default dev port is 4321 (the digits 4-3-2-1). If it is taken, Astro automatically picks the next free port and prints the new URL.
Understand the project layout
An empty Astro project is intentionally small. The pieces you will touch most often are:
| Path | Purpose |
|---|---|
src/pages/ | File-based routes. Each .astro, .md, or .mdx file becomes a URL. |
src/components/ | Reusable .astro (or framework) components. |
src/layouts/ | Shared page shells — <html>, <head>, navigation. |
public/ | Static assets served as-is (favicons, images, robots.txt). |
astro.config.mjs | Project configuration and integrations. |
The router is file-based: src/pages/index.astro maps to /, and src/pages/about.astro maps to /about. There is no route table to maintain.
Edit the home page
Open src/pages/index.astro. Replace its contents with the following. Notice the two parts of every Astro component: the component script between the --- fences (which runs only on the server) and the HTML template below it.
---
// src/pages/index.astro
const siteName = "My First Astro Site";
const features = ["Zero JS by default", "File-based routing", "Instant HMR"];
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{siteName}</title>
</head>
<body>
<main>
<h1>{siteName}</h1>
<p>You shipped a page rendered as plain HTML.</p>
<ul>
{features.map((feature) => <li>{feature}</li>)}
</ul>
</main>
<style>
main { max-width: 40rem; margin: 4rem auto; font-family: system-ui; }
h1 { color: #6d28d9; }
</style>
</body>
</html>
Save the file. The browser updates immediately — no full reload, no flicker. The siteName and features values were computed on the server, so the page you receive is already finished HTML with no client-side JavaScript.
Add a reusable component
Real sites are built from components. Create src/components/Card.astro and accept props through Astro.props:
---
// src/components/Card.astro
interface Props {
title: string;
href: string;
}
const { title, href } = Astro.props;
---
<a class="card" href={href}>
<h2>{title} →</h2>
<slot />
</a>
<style>
.card { display: block; padding: 1rem; border: 1px solid #e5e7eb; border-radius: 8px; text-decoration: none; color: inherit; }
.card:hover { border-color: #6d28d9; }
</style>
The <slot /> renders whatever children you pass between the component’s tags. Import and use it back in index.astro:
---
import Card from "../components/Card.astro";
---
<Card title="Read the docs" href="https://docs.astro.build">
<p>Everything you need to go further.</p>
</Card>
Astro components are HTML-first, so they compose just like ordinary markup — and they still ship no JavaScript.
Sprinkle in interactivity (optional)
When a piece of the page genuinely needs to run in the browser, you opt in with a client:* directive on a framework component. First add an integration:
npx astro add react
Then create a small island and hydrate only it:
---
// src/pages/index.astro
import Counter from "../components/Counter.jsx";
---
<!-- Hydrates only when scrolled into view -->
<Counter client:visible />
Everything else on the page remains static HTML. Only Counter downloads JavaScript, and only when the visitor reaches it.
Note: Component-script imports run at build/request time on the server. If you need browser-only APIs like
windoworlocalStorage, put that code inside a framework island, not in the.astroscript.
Build for production
When you are ready to ship, build the optimized output and preview it locally before deploying:
npm run build
npm run preview
Output:
building static entrypoints...
✓ Completed in 1.21s.
▶ src/pages/index.astro
└─ /index.html (+18ms)
Complete!
The static files land in dist/, ready to upload to any static host or CDN.
Best Practices
- Keep static content in
.astrocomponents; reach for a framework island only when client-side state is truly required. - Pass data through typed
Propsinterfaces so your components are self-documenting and autocomplete-friendly. - Use scoped
<style>blocks instead of global CSS to avoid cascade conflicts between components. - Prefer
client:visibleorclient:idleoverclient:loadfor non-critical widgets to keep the main thread free. - Run
npm run build && npm run previewbefore deploying to catch issues the dev server hides. - Commit early —
create astrocan initialize git for you, giving you a clean baseline.