Project Structure
A freshly scaffolded Astro project looks familiar if you know Next.js, but several conventions are subtly different. The most important: src/pages/ is the only directory Astro treats as special for routing — everything else is up to you.
Side-by-side structure
Section titled “Side-by-side structure”my-next-app/├── src/│ ├── app/ # App Router pages (Next 13+)│ │ ├── page.tsx # → /│ │ └── about/│ │ └── page.tsx # → /about│ ├── components/ # Shared React components│ └── lib/ # Utilities├── public/ # Static assets (served at /)├── next.config.mjs # Framework config└── package.jsonmy-astro-app/├── src/│ ├── pages/ # File-based routes (special)│ │ ├── index.astro # → /│ │ └── about.astro # → /about│ ├── components/ # .astro and framework components│ ├── layouts/ # Layout wrappers (Astro convention)│ └── content/ # Content Collections (MDX, YAML, JSON)├── public/ # Static assets (served at /)├── astro.config.mjs # Framework config└── package.jsonKey differences to notice:
src/pages/— notsrc/app/. Astro uses the Pages Router mental model: a file atsrc/pages/about.astrorenders at/about. No specialpage.tsxwrapper needed.src/layouts/is a convention, not a framework rule. Astro does not enforce alayouts/directory — but every Astro project uses one by convention because layout components are a first-class pattern (covered in the Layouts module).src/content/is for Content Collections — structured content like MDX blog posts, team member YAML files, or JSON data. Astro provides a typed API to query it. You would usegetStaticPaths+getEntryinstead ofgetStaticProps+ a file read.astro.config.mjsreplacesnext.config.mjs. Same idea — framework config, integrations, Vite overrides.
The public/ folder
Section titled “The public/ folder”Both Next.js and Astro have a public/ directory. Files placed there are served at the root URL as-is — no bundling, no transforms. A file at public/logo.png is available at /logo.png.
The difference: in Next.js you reference images with the <Image> component for optimisation. In Astro you use a plain <img> tag (or Astro’s built-in image optimisation — import { Image } from 'astro:assets').
astro.config.mjs
Section titled “astro.config.mjs”import { defineConfig } from 'astro/config';import react from '@astrojs/react';import tailwind from '@astrojs/tailwind';
export default defineConfig({ site: 'https://my-site.com', integrations: [react(), tailwind()], // Vite config lives inside vite: {}});Compare to next.config.mjs — the shape is similar (a default export returning a config object) but the keys differ. Astro does not have experimental, rewrites, or redirects as top-level keys; those go through Vite or Astro middleware.
Runnable page demonstrating structure
Section titled “Runnable page demonstrating structure”---
// src/pages/index.astro
// Frontmatter: runs at build/server time
const siteName = "My Astro Site";
const navLinks = [
{ href: "/", label: "Home" },
{ href: "/about", label: "About" },
{ href: "/blog", label: "Blog" },
];
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{siteName}</title>
<style>
body { font-family: sans-serif; margin: 0; }
nav { background: #1e1b4b; padding: 1rem 2rem; display: flex; gap: 1.5rem; }
nav a { color: #c7d2fe; text-decoration: none; font-size: 0.9rem; }
nav a:hover { color: #fff; }
main { padding: 2rem; }
.badge { background: #ede9fe; color: #4c1d95; padding: 0.2rem 0.6rem; border-radius: 4px; font-size: 0.75rem; }
</style>
</head>
<body>
<nav>
{navLinks.map(link => (
<a href={link.href}>{link.label}</a>
))}
</nav>
<main>
<span class="badge">src/pages/index.astro</span>
<h1>Welcome to {siteName}</h1>
<p>This page lives at <code>src/pages/index.astro</code> and renders at <code>/</code>.</p>
<p>Components go in <code>src/components/</code>, layouts in <code>src/layouts/</code>.</p>
</main>
</body>
</html>