Astro Pages, Layouts and Components: The First Three Concepts to Learn

How Astro's file-based routing, shared layouts, and reusable components work, explained for WordPress developers who think in themes, templates and shortcodes.

Quick answer

How do pages, layouts and components work in Astro?

In Astro, files inside src/pages/ become routes. Layouts are reusable wrapper components that define shared page structure and use <slot /> to receive page content. Components are smaller reusable UI pieces, such as headers, footers, cards, and banners, that can be imported into pages, layouts, or other components.

Astro pages, layouts, and components diagram showing routes, BaseLayout, reusable components, and browser output
First-hand experience: Based on direct hands-on use. These are the exact files built for astro-content-lab, the demo project for this series.

View the completed Article 03 source code at commit 27e734e.

Version note: This project was originally built with Astro 6. The Pages, Layouts, Components, props, and slot workflow shown here also works in Astro 7.

After setting up your first Astro project, you’re looking at a near-empty project with only one page file: src/pages/index.astro.

That’s it. One page. No theme. No template system to configure.

VS Code showing the starter Astro project with src pages index astro open and the dev server running
A fresh Astro project starts small: one page file in src/pages and a local dev server watching for changes.

To build from that starting point, focus on three concepts first: Pages, Layouts, and Components. Learn these three and you can build the foundation of a content site.

If you want the official reference while following along, Astro documents these ideas in its Pages, Routing, Layouts, and Components guides. This article explains the same ideas in the language of a WordPress developer.


Pages: Files in src/pages become routes

This is the part that surprised me first.

In WordPress, URL routing is handled by WordPress itself. It reads your permalink settings, queries the database, finds the right template, and assembles the page. You don’t create a file to get a URL.

In Astro, routing is simpler: files inside src/pages/ become routes.

For this beginner article, we will use .astro files. So every .astro file you create in src/pages/ becomes a URL automatically.

src/pages/index.astro   →   yoursite.com/
src/pages/about.astro   →   yoursite.com/about
src/pages/blog.astro    →   yoursite.com/blog

No configuration. No routing file. No permalink settings for this basic setup. Create a page file, get a URL.

Let’s create two new pages. In your terminal:

touch src/pages/about.astro
touch src/pages/blog.astro

I use touch here because I am on a macOS/Linux-style terminal. On Windows, the easiest path is to create the files directly in VS Code: right-click the pages folder, click New File.

Open src/pages/about.astro and add:

---
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>About — Astro Content Lab</title>
  </head>
  <body>
    <h1>About</h1>
    <p>This is the about page.</p>
  </body>
</html>

Save it. Go to localhost:4321/about.

The page is live. No configuration needed.

Browser preview of the Astro about page running at localhost 4321 slash about
Creating src/pages/about.astro automatically gives you the /about route in the browser.

Do the same for src/pages/blog.astro:

---
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Blog — Astro Content Lab</title>
  </head>
  <body>
    <h1>Blog</h1>
    <p>Blog posts will go here.</p>
  </body>
</html>

Visit localhost:4321/blog. Another live page.


The problem with three separate pages

You now have three working pages. But look at the code: you’ve already written this three times.

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>...</title>
  </head>
  <body>
    ...
  </body>
</html>

If you want to add a Google Analytics script, change the font, or add a global CSS file, you’d have to edit all three files. With a 10-page site, that’s manageable. With a 100-page site, it’s a maintenance nightmare.

This is exactly the problem that Layouts solve.


Layouts: One wrapper, shared by all pages

In this project, BaseLayout is a component that wraps each page. It contains the shared HTML boilerplate, such as <html>, <head>, <body>, header, and footer, and leaves a placeholder for each page to inject its own content.

More generally, an Astro layout is a reusable component that provides shared structure to pages or other layouts. It does not have to contain the entire HTML document.

The placeholder used in this layout is <slot />.

src/layouts is a common Astro convention. Astro does not force this exact folder name, but using it keeps a beginner project easy to understand.

Create a new folder and file:

mkdir src/layouts
touch src/layouts/BaseLayout.astro

Open src/layouts/BaseLayout.astro and add:

---
interface Props {
  title: string
}

const { title } = Astro.props
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>
  </head>
  <body>
    <slot />
  </body>
</html>

A few things to notice:

The frontmatter (between the --- dashes) defines a Props interface. This is TypeScript saying “this layout accepts a title prop of type string”. Then Astro.props extracts that value so you can use it in the template.

The Props interface is optional. The layout would still work without it, but keeping it gives VS Code better type checking and autocomplete.

{title} in the <title> tag: curly braces render a JavaScript variable in the HTML template.

<slot />: this is where page content goes. It’s a placeholder. Whatever you put between your layout tags in a page file will appear here.

VS Code showing BaseLayout astro with a title prop and slot placeholder inside the body
BaseLayout holds the shared HTML shell, while slot marks the exact place where each page's content will be inserted.

Now update src/pages/index.astro to use the layout:

---
import BaseLayout from '../layouts/BaseLayout.astro'
---

<BaseLayout title="Home — Astro Content Lab">
  <h1>Astro Content Lab</h1>
  <p>Welcome to the lab.</p>
</BaseLayout>

Save it. Open localhost:4321. The page should look the same, but now the HTML wrapper comes from the layout, not the page file.

Update about.astro and blog.astro to use the layout too:

---
import BaseLayout from '../layouts/BaseLayout.astro'
---

<BaseLayout title="About — Astro Content Lab">
  <h1>About</h1>
  <p>This is the about page.</p>
</BaseLayout>
---
import BaseLayout from '../layouts/BaseLayout.astro'
---

<BaseLayout title="Blog — Astro Content Lab">
  <h1>Blog</h1>
  <p>Blog posts will go here.</p>
</BaseLayout>

Now all three pages share the same HTML wrapper. Change anything in BaseLayout.astro, a font, a script, a meta tag, and it updates everywhere at once.


Components: Reusable UI pieces

You have a layout. Now let’s add a navigation menu.

You could put the nav directly in BaseLayout.astro. But navigation is a self-contained piece of UI. It has its own HTML, its own styles, its own logic. Better to give it its own file.

That’s a Component.

Native Astro components use .astro files to represent reusable pieces of UI. They can be used anywhere: in layouts, in pages, or inside other components.

Astro can also use components from frameworks such as React, Vue, and Svelte, but this series starts with Astro’s built-in component format.

src/components is also a convention. You could organize components differently later, but for this series we will keep the boring, predictable structure.

Create a components folder and a Header file:

mkdir src/components
touch src/components/Header.astro

Open src/components/Header.astro:

---
---

<header>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
    <a href="/blog">Blog</a>
  </nav>
</header>

Now import and use it in BaseLayout.astro:

---
import Header from '../components/Header.astro'

interface Props {
  title: string
}

const { title } = Astro.props
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>
  </head>
  <body>
    <Header />
    <slot />
  </body>
</html>

Save both files. Every page now has navigation, without touching a single page file.

VS Code showing BaseLayout importing Header astro and rendering Header before the slot
The Header component is imported once in the layout, so every page wrapped by BaseLayout gets the same navigation.
Browser preview of the Astro home page showing Home About and Blog navigation links above the page content
The result is plain but important: one Header component now appears across the site without copying navigation into each page.

The complete picture

Here’s what you’ve built and how the pieces connect:

BaseLayout.astro       ← shared HTML wrapper
    ├── Header.astro   ← nav component (imported into layout)
    └── <slot />       ← page content goes here

index.astro            ← uses BaseLayout, passes title prop
about.astro            ← uses BaseLayout, passes title prop
blog.astro             ← uses BaseLayout, passes title prop

The flow when someone visits /about:

  1. Astro finds src/pages/about.astro
  2. about.astro uses BaseLayout, so Astro renders the layout
  3. BaseLayout imports Header, so Astro renders the header
  4. BaseLayout has <slot />, so Astro fills it with the content from about.astro
  5. The result is a complete HTML page sent to the browser

In the default static setup we are using in this series, this happens at build time, not when someone visits. The HTML is pre-built and ready to serve.


How this compares to WordPress themes

If you’ve built WordPress themes, this structure should feel familiar:

WordPressAstro equivalent
header.phpHeader.astro component
footer.phpFooter.astro component
get_header() in templates<Header /> in layout
Page template filessrc/pages/*.astro
the_content() in templates<slot /> in layout
get_template_part()Component imports

The concepts map directly. A traditional uncached WordPress site typically assembles pages at request time using PHP and database content. In the default static Astro setup used here, Astro assembles pages at build time. In both approaches, the browser ultimately receives HTML; the important difference is when and where that HTML is assembled.


Your project structure now

After this article, your project should look like this:

astro-content-lab/
├── src/
│   ├── pages/
│   │   ├── index.astro    ← /
│   │   ├── about.astro    ← /about
│   │   └── blog.astro     ← /blog
│   ├── layouts/
│   │   └── BaseLayout.astro
│   └── components/
│       └── Header.astro
├── public/
├── astro.config.mjs
└── package.json

This structure, pages, layouts, components, is the foundation of every Astro project, from a simple blog to a full content site. The next article adds CSS to make it actually look good.


What this article does not cover yet

This article only covers static pages, one shared layout, and one simple component. It does not cover dynamic routes, Markdown/MDX blog posts, Content Collections, nested layouts, or CMS-driven content yet.

Those come later in the series.

For styling, the next article will cover how component-scoped CSS and global CSS work in Astro. The official Astro styling guide is the reference I use when checking the details.


Common mistakes

Forgetting to save the file. Hot reload only works when the file is saved. If your changes aren’t showing up, check for the unsaved indicator in VS Code: a dot next to the filename in the tab. Cmd + S to save.

Wrong import path. If you move a file, update the import paths. ../layouts/BaseLayout.astro means “go up one folder, then into layouts folder, then BaseLayout.astro”. Get the path wrong and you’ll see an import error.

<slot /> vs <slot></slot>. Both work. <slot /> is the self-closing shorthand. Use whichever you prefer.

Putting styles in the wrong place. CSS written inside a <style> tag in an .astro file is scoped to that component by default. In this series, CSS that should apply across the site will live in a separate global.css file imported by the layout. Astro also supports <style is:global>, but an external global stylesheet keeps this beginner project easier to organize. The next article covers both scoped and global styling.


Frequently Asked Questions

How does routing work in Astro?
Astro uses file-based routing. Files inside src/pages/ become routes. In this beginner article, we use .astro files: index.astro becomes /, about.astro becomes /about, and blog.astro becomes /blog. No router configuration needed for these basic pages.
What is the slot tag in Astro?
The slot tag is a placeholder inside a layout component. Whatever you put between the opening and closing layout tags in your page file gets inserted where slot is. Think of it as a socket, the layout is the wall, and each page plugs content into it.
What is the difference between a layout and a component in Astro?
In this project, BaseLayout wraps the entire page and contains the shared html, head, and body structure. More generally, an Astro layout is a reusable component that provides shared structure to pages or other layouts. A component is a reusable piece of UI such as a navigation menu, card, or button.
Do I need React to use Astro components?
No. Astro has its own .astro component format that looks like HTML with a JavaScript frontmatter section. You do not need React or another UI framework to build components for this project, although Astro can use framework components later.
How is this different from WordPress themes?
A traditional uncached WordPress site typically uses PHP templates and database content to assemble a page at request time. In the default static Astro setup used in this series, layouts and components are assembled at build time into HTML files. The concept feels familiar; the execution is different.