Creating a Base Layout
A layout in Astro is simply a regular .astro component, but one that renders the full HTML document shell — the <!doctype html>, <html>, <head>, and <body> — and leaves a hole where each page can drop its own content. Centralizing the shell in one BaseLayout.astro means every page shares the same metadata, fonts, navigation, and footer, and you fix the boilerplate in a single place. The “hole” is created by Astro’s <slot />, the mechanism that injects whatever a page wraps inside the layout. This page builds that base layout from scratch.
Anatomy of a layout component
A layout has nothing special about it structurally; what makes it a layout is convention. It lives in src/layouts/, it owns the document shell, and it accepts page content through a default <slot />. Pages import it, pass props like a page title, and nest their content as children.
---
// src/layouts/BaseLayout.astro
interface Props {
title: string;
description?: string;
}
const { title, description = "Built with Astro" } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
The <slot /> element is the placeholder. When a page uses this layout, everything the page nests between the layout’s opening and closing tags is rendered exactly where <slot /> sits. Because the script fence runs on the server, the title and description props are baked into static HTML at build time — zero JavaScript ships for any of this.
Always include
<!doctype html>as the very first line and the<meta charset>/<meta name="viewport">tags. Astro does not inject these for you; the layout is the single source of truth for the document head.
Using the layout from a page
A page imports the layout, supplies its props, and wraps its markup inside the component tags. The wrapped markup becomes the slotted content.
---
// src/pages/index.astro
import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout title="Home" description="Welcome to DevCraftly">
<main>
<h1>Hello from the home page</h1>
<p>This paragraph lands inside the layout's default slot.</p>
</main>
</BaseLayout>
The rendered output stitches the page’s <main> into the layout’s <body>:
Output:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Welcome to DevCraftly">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Home</title>
</head>
<body>
<main>
<h1>Hello from the home page</h1>
<p>This paragraph lands inside the layout's default slot.</p>
</main>
</body>
</html>
Adding shared chrome
The real payoff arrives when you put navigation, a footer, or global styles into the layout. Everything outside <slot /> is shared by every page; everything that flows into <slot /> is page-specific. You can also import other components — like a Header — directly inside the layout.
---
// src/layouts/BaseLayout.astro
import Header from "../components/Header.astro";
import "../styles/global.css";
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
</head>
<body>
<Header />
<slot />
<footer>© {new Date().getFullYear()} DevCraftly</footer>
</body>
</html>
Now the header and footer render on every page automatically, while each page contributes only its unique middle. A <style> block placed in the layout is scoped to the layout by default and is bundled efficiently across pages that use it.
Slot defaults and what a layout owns
If a page renders the layout with no children, the default slot is empty. You can provide fallback content by putting markup inside the <slot> element — it shows only when nothing is passed.
<body>
<slot>
<p>No content was provided for this page.</p>
</slot>
</body>
This table summarizes the typical division of responsibility between a base layout and the pages that consume it.
| Concern | Lives in the layout | Lives in the page |
|---|---|---|
Document shell (html, head, body) | Yes | No |
| Global metadata and favicon | Yes | Via props |
| Site-wide header / footer | Yes | No |
| Page title and description | Defined as props | Passed in |
| Main content | <slot /> placeholder | The slotted markup |
Best practices
- Keep exactly one document shell — a single
BaseLayout.astroshould be the only component that emits<!doctype html>and the<head>. - Type the layout’s incoming data with an
interface Props {}so the build flags any page that forgets a required prop liketitle. - Pass per-page metadata (title, description, canonical URL) as props rather than hardcoding it in the shell.
- Use a default value for optional props (
description = "...") so pages stay terse. - Put shared chrome (header, footer, skip links) outside the
<slot />and page-unique markup inside it. - Provide fallback content inside
<slot>...</slot>when a layout may be rendered without children. - Import global CSS once in the layout instead of repeating it on every page.