The src Directory
The src/ directory is where your application code lives. Everything that Astro processes, transforms, bundles, or renders at build time originates here: pages and routes, components, layouts, content collections, styles, and shared utilities. Unlike public/, whose files are copied verbatim, every file in src/ is part of Astro’s build pipeline, so it can be optimized, type-checked, and tree-shaken. Understanding the conventions in this folder is the fastest way to become productive with Astro.
What Astro expects in src
Only one folder inside src/ is truly special: src/pages/. The rest are widely used conventions that Astro recommends but does not enforce. You are free to organize the non-special folders however you like, but following the conventions keeps your project legible to other Astro developers and to tooling.
| Folder | Purpose | Special? |
|---|---|---|
src/pages/ | File-based routes — each file becomes a URL | Yes |
src/content/ | Content collections (Markdown, MDX, data) | Yes (when configured) |
src/components/ | Reusable UI building blocks | Convention |
src/layouts/ | Page shells that wrap content | Convention |
src/styles/ | Global and shared CSS | Convention |
src/assets/ | Images/fonts optimized by Astro | Convention |
src/lib/ or src/utils/ | Shared JS/TS helpers | Convention |
The
src/prefix is configurable viasrcDirinastro.config.mjs, but changing it is rarely worth the churn. Stick with the default unless you have a strong reason.
The pages directory
src/pages/ is the heart of routing in Astro. Each .astro, .md, .mdx, or .html file becomes a page, and its path maps directly to a URL. There is no router config to maintain.
src/pages/
├── index.astro → /
├── about.astro → /about
├── blog/
│ ├── index.astro → /blog
│ └── [slug].astro → /blog/:slug
└── api/
└── hello.ts → /api/hello (endpoint)
Square brackets denote dynamic routes. A .ts/.js file in pages/ becomes an API endpoint instead of an HTML page.
---
// src/pages/blog/[slug].astro
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const posts = await getCollection("blog");
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
Components and islands
src/components/ holds reusable UI. Astro components (.astro) render to HTML with zero JavaScript shipped to the browser by default. When you need interactivity, drop in a framework component (React, Vue, Svelte, Solid) and hydrate it selectively with a client:* directive — the islands architecture.
---
// src/components/Header.astro
import ThemeToggle from "./ThemeToggle.tsx";
const { siteName } = Astro.props;
---
<header>
<a href="/">{siteName}</a>
<!-- Only this island ships JS; the rest stays static HTML -->
<ThemeToggle client:idle />
</header>
Common client:* directives:
| Directive | Hydrates when |
|---|---|
client:load | Immediately on page load |
client:idle | Browser is idle (requestIdleCallback) |
client:visible | Component scrolls into the viewport |
client:media | A CSS media query matches |
client:only | Skips SSR; renders only on the client |
Reach for
client:visibleorclient:idlebeforeclient:load. Hydrating only what the user can see keeps pages fast and preserves Astro’s zero-JS-by-default advantage.
Layouts
src/layouts/ contains components that define the shared shell of your pages — <html>, <head>, navigation, and footer — exposing a <slot /> where page content is injected.
---
// src/layouts/BaseLayout.astro
import Header from "../components/Header.astro";
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body>
<Header siteName="DevCraftly" />
<main><slot /></main>
</body>
</html>
Then wrap any page with it:
---
import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout title="Home">
<h1>Welcome</h1>
</BaseLayout>
Content, styles, and assets
src/content/ is where content collections live, defined and validated by a schema in src/content/config.ts. Collections give your Markdown and data front matter full TypeScript type-safety:
// src/content/config.ts
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
pubDate: z.date(),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
src/styles/ holds global CSS you import where needed, while component-scoped <style> blocks live inside .astro files. src/assets/ holds images and fonts that you want Astro to optimize via the <Image /> component and astro:assets:
---
import { Image } from "astro:assets";
import hero from "../assets/hero.png";
---
<Image src={hero} alt="Hero" width={800} />
Best practices
- Reserve
src/pages/for route entry points only; push real UI intosrc/components/andsrc/layouts/. - Prefer
.astrocomponents for static markup and add framework islands only where interactivity is genuinely needed. - Use the most conservative hydration directive that works —
client:visible/client:idleoverclient:load. - Put images that need optimization in
src/assets/(notpublic/) so Astro can resize and compress them. - Keep cross-cutting helpers in
src/lib/orsrc/utils/and import them with the~/or relative alias for clarity. - Define content schemas in
src/content/config.tsto get type-safe front matter and build-time validation.