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. The source code is on GitHub.

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

That’s it. One page. No theme. No template system. Nothing.

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.

Here’s what you need to understand to build from that starting point: Astro has three core concepts, 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.

Browser preview of the Astro blog page running at localhost 4321 slash blog
The same file-based routing rule applies again: src/pages/blog.astro becomes /blog.

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

A Layout is a component that wraps your pages. It contains all the HTML boilerplate that every page shares, like <html>, <head>, <body>, header, footer, and leaves a placeholder for each page to inject its own content.

That placeholder 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.

{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.

VS Code showing index astro importing BaseLayout and passing a title prop around home page content
Once index.astro uses BaseLayout, the page only owns its content while the shared document structure lives in the layout.

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.

VS Code showing blog astro importing BaseLayout and wrapping blog page content in the layout
Every page can use the same layout pattern: import BaseLayout, pass a title, and place page-specific content inside.

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.

Components in Astro are .astro files that represent a piece of UI. They can be used anywhere: in layouts, in pages, inside other components.

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. The main difference: WordPress assembles pages at request time using PHP. In the default static Astro setup used here, Astro assembles pages at build time. The output, HTML, is the same.


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 only. It won’t affect other components. CSS you want to apply globally needs to go in a separate global.css file and be imported in the layout, covered in the next article.


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 is needed for these basic pages.
What is the slot tag in Astro?
The slot tag is a placeholder inside a layout component. Whatever content 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 provides the structure, and each page plugs its content into the slot.
What is the difference between a layout and a component?
A layout wraps an entire page. It contains the html, head, and body tags plus shared UI like header and footer. A component is a smaller reusable piece of UI like a navigation menu or a button. Layouts typically use components inside them.
Do I need React to use Astro components?
No. Astro has its own .astro component format. It looks like HTML with a JavaScript section at the top. No React knowledge required. You can add React components later if needed, but for a content site you don't need any framework.
How is this different from WordPress themes?
WordPress themes use PHP template files that get assembled at request time with database content. In the default static Astro setup used in this series, layouts and components are assembled at build time into HTML files. The concepts map almost directly: header.php becomes Header.astro, get_header() becomes importing and using the Header component, and the_content() becomes slot.