Skip to content
Astro as project 4 min read

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.

FolderPurposeSpecial?
src/pages/File-based routes — each file becomes a URLYes
src/content/Content collections (Markdown, MDX, data)Yes (when configured)
src/components/Reusable UI building blocksConvention
src/layouts/Page shells that wrap contentConvention
src/styles/Global and shared CSSConvention
src/assets/Images/fonts optimized by AstroConvention
src/lib/ or src/utils/Shared JS/TS helpersConvention

The src/ prefix is configurable via srcDir in astro.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:

DirectiveHydrates when
client:loadImmediately on page load
client:idleBrowser is idle (requestIdleCallback)
client:visibleComponent scrolls into the viewport
client:mediaA CSS media query matches
client:onlySkips SSR; renders only on the client

Reach for client:visible or client:idle before client: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 into src/components/ and src/layouts/.
  • Prefer .astro components for static markup and add framework islands only where interactivity is genuinely needed.
  • Use the most conservative hydration directive that works — client:visible/client:idle over client:load.
  • Put images that need optimization in src/assets/ (not public/) so Astro can resize and compress them.
  • Keep cross-cutting helpers in src/lib/ or src/utils/ and import them with the ~/ or relative alias for clarity.
  • Define content schemas in src/content/config.ts to get type-safe front matter and build-time validation.
Last updated June 14, 2026
Was this helpful?