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.
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.
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.
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.
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:
- Astro finds
src/pages/about.astro about.astrousesBaseLayout, so Astro renders the layoutBaseLayoutimportsHeader, so Astro renders the headerBaseLayouthas<slot />, so Astro fills it with the content fromabout.astro- 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:
| WordPress | Astro equivalent |
|---|---|
header.php | Header.astro component |
footer.php | Footer.astro component |
get_header() in templates | <Header /> in layout |
| Page template files | src/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.