Project Structure
Every Astro project follows a small, predictable convention: source code lives in src/, static assets live in public/, and a handful of config files sit at the root. Understanding this layout up front saves you hours of guesswork, because Astro infers routes, processes assets, and resolves imports based purely on where files live. This page walks through a default project and explains the role of each top-level folder so you always know where a file belongs.
Scaffolding a new project
The create astro CLI generates the canonical structure. Pick a template, install dependencies, and you have a working zero-JS site.
npm create astro@latest my-site
cd my-site
npm run dev
Output:
astro v5.0.0 ready in 312 ms
┃ Local http://localhost:4321/
┃ Network use --host to expose
The default layout
A freshly generated project looks like this. The directory names are not arbitrary — src/pages and public in particular are special and Astro treats them by convention.
my-site/
├── public/
│ └── favicon.svg
├── src/
│ ├── assets/
│ ├── components/
│ ├── layouts/
│ ├── pages/
│ │ └── index.astro
│ └── styles/
├── astro.config.mjs
├── package.json
└── tsconfig.json
Top-level folders
The table below summarizes what each entry is for and whether Astro gives it special meaning.
| Path | Purpose | Special to Astro? |
|---|---|---|
src/pages/ | File-based routes — each file becomes a URL | Yes (required) |
src/components/ | Reusable .astro, React, Vue, Svelte components | No (convention) |
src/layouts/ | Page shells that wrap content with <slot /> | No (convention) |
src/assets/ | Images/fonts processed and optimized at build | Yes (for astro:assets) |
src/styles/ | Global and shared CSS | No (convention) |
public/ | Files served verbatim, untouched by the build | Yes |
astro.config.mjs | Project configuration and integrations | Yes |
src — your application code
src/ is where everything Astro processes lives: components, pages, styles, and TypeScript. Files here are bundled, transformed, and tree-shaken. The only mandatory directory is src/pages/, which drives routing. A minimal page renders to fully static HTML with no client JavaScript shipped at all.
---
// src/pages/index.astro
const title = "Welcome to Astro";
const items = ["Fast", "Flexible", "Zero JS by default"];
---
<html lang="en">
<head><title>{title}</title></head>
<body>
<h1>{title}</h1>
<ul>
{items.map((item) => <li>{item}</li>)}
</ul>
</body>
</html>
UI framework components only ship JavaScript when you opt in with a client:* directive, the core of Astro’s islands architecture.
---
import Counter from "../components/Counter.jsx";
---
<!-- Static by default; hydrates only when visible -->
<Counter client:visible />
public — static assets
Anything in public/ is copied to the build output as-is and referenced from the site root. Use it for files that must keep an exact name and path, such as favicon.svg, robots.txt, or manifest.webmanifest. There is no hashing, optimization, or import resolution.
<!-- public/favicon.svg → served at /favicon.svg -->
<link rel="icon" href="/favicon.svg" />
Prefer
src/assets/for images you import in components — Astro optimizes them and adds content hashes for caching. Reservepublic/for assets that need a stable, predictable URL.
Content collections
When you author Markdown or MDX content, organize it under src/content/ and describe each collection’s schema in src/content.config.ts (Astro 5) for type-safe queries.
// src/content.config.ts
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const blog = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
pubDate: z.date(),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
Root configuration files
The repository root holds the files that configure the toolchain rather than your pages.
| File | Role |
|---|---|
astro.config.mjs | Integrations, adapters, output mode, Vite options |
tsconfig.json | TypeScript paths and the Astro base config |
package.json | Dependencies and dev/build/preview scripts |
.env | Environment variables loaded at build/runtime |
A typical config registers integrations and an output target.
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({
integrations: [react()],
output: "static",
});
Best practices
- Keep
src/pages/focused on routing — push real markup and logic intosrc/components/andsrc/layouts/. - Import images from
src/assets/so Astro can optimize and hash them; reservepublic/for files needing fixed URLs. - Use the
~/or@/path alias intsconfig.jsonto avoid brittle../../import chains. - Co-locate content under
src/content/with a typed schema incontent.config.tsfor safe, autocompleted queries. - Add only the integrations you need in
astro.config.mjsto keep builds lean and preserve zero-JS-by-default output. - Commit
astro.config.mjs,tsconfig.json, and lockfiles; never commit.envfiles containing secrets.