Skip to content
Astro as project 4 min read

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.

PathPurposeSpecial to Astro?
src/pages/File-based routes — each file becomes a URLYes (required)
src/components/Reusable .astro, React, Vue, Svelte componentsNo (convention)
src/layouts/Page shells that wrap content with <slot />No (convention)
src/assets/Images/fonts processed and optimized at buildYes (for astro:assets)
src/styles/Global and shared CSSNo (convention)
public/Files served verbatim, untouched by the buildYes
astro.config.mjsProject configuration and integrationsYes

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. Reserve public/ 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.

FileRole
astro.config.mjsIntegrations, adapters, output mode, Vite options
tsconfig.jsonTypeScript paths and the Astro base config
package.jsonDependencies and dev/build/preview scripts
.envEnvironment 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 into src/components/ and src/layouts/.
  • Import images from src/assets/ so Astro can optimize and hash them; reserve public/ for files needing fixed URLs.
  • Use the ~/ or @/ path alias in tsconfig.json to avoid brittle ../../ import chains.
  • Co-locate content under src/content/ with a typed schema in content.config.ts for safe, autocompleted queries.
  • Add only the integrations you need in astro.config.mjs to keep builds lean and preserve zero-JS-by-default output.
  • Commit astro.config.mjs, tsconfig.json, and lockfiles; never commit .env files containing secrets.
Last updated June 14, 2026
Was this helpful?