Skip to content
Astro as layouts 4 min read

Nested Layouts

Most real sites need more than one layout: a marketing page, a blog post, and a documentation article all share the same document shell but differ in their inner chrome. Rather than copy the <head>, navigation, and footer into every layout, you nest layouts — a specialized layout renders its own wrapper and then drops itself inside a shared base layout. Because Astro layouts are just components, nesting them is the same as composing any two components: one renders the other and passes content through a slot. This keeps the document shell in exactly one file while letting each page type own its unique structure.

Why nest instead of duplicate

A BaseLayout.astro typically owns everything that must be identical across the whole site: the <!doctype html>, the <head> metadata, fonts, the global header, and the footer. A BlogPostLayout.astro adds things only blog posts need — a title block, author byline, reading-time, a prose container. Without nesting you would have to repeat the entire HTML shell inside the blog layout, and any change to the header would mean editing every layout. Nesting inverts that: the blog layout delegates the shell to the base and contributes only the blog-specific markup.

The mechanism is forwarding. A child layout accepts content through its own <slot />, wraps it, and renders the result inside the base layout — forwarding props like the page title up to the base as it goes.

Building a nested layout

Start with a base layout that exposes a default slot and any named slots it needs (for example, a slot for extra <head> tags).

---
// 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} />
    <title>{title}</title>
    <slot name="head" />
  </head>
  <body>
    <header><nav><a href="/">Home</a> · <a href="/blog">Blog</a></nav></header>
    <main>
      <slot />
    </main>
    <footer{new Date().getFullYear()} DevCraftly</footer>
  </body>
</html>

Now write the specialized layout. It imports BaseLayout, renders it, and places its own markup inside. The page content it receives flows through its own <slot />, which lands in the <main> of the base.

---
// src/layouts/BlogPostLayout.astro
import BaseLayout from "./BaseLayout.astro";

interface Props {
  title: string;
  description?: string;
  author: string;
  pubDate: Date;
}

const { title, description, author, pubDate } = Astro.props;
---

<BaseLayout title={title} description={description}>
  <link slot="head" rel="stylesheet" href="/styles/prose.css" />

  <article class="prose">
    <header>
      <h1>{title}</h1>
      <p class="byline">
        By {author} ·
        <time datetime={pubDate.toISOString()}>
          {pubDate.toLocaleDateString("en-US", { dateStyle: "long" })}
        </time>
      </p>
    </header>
    <slot />
  </article>
</BaseLayout>

A page then uses only the specialized layout — it never touches the base directly:

---
// src/pages/blog/hello.astro
import BlogPostLayout from "../../layouts/BlogPostLayout.astro";
---

<BlogPostLayout
  title="Hello, nested layouts"
  description="A first post"
  author="Ada"
  pubDate={new Date("2026-06-14")}
>
  <p>This paragraph is rendered inside the article, inside the base shell.</p>
</BlogPostLayout>

The rendered DOM nests cleanly, with the page content deepest:

Output:

<body>
  <header><nav>…</nav></header>
  <main>
    <article class="prose">
      <header><h1>Hello, nested layouts</h1>…</header>
      <p>This paragraph is rendered inside the article, inside the base shell.</p>
    </article>
  </main>
  <footer>© 2026 DevCraftly</footer>
</body>

Tip: Slots forward by name across nesting boundaries. The slot="head" element placed inside BlogPostLayout travels all the way up to the base’s <slot name="head" />, so child layouts can inject <head> content without the base knowing what it is.

Passing props up the chain

Each layer is responsible for the props the next layer needs. BlogPostLayout consumes author and pubDate itself, but it must explicitly forward title and description to BaseLayout. If you find yourself relaying many props, spread them — <BaseLayout {...Astro.props}> passes everything through, and the base ignores any props it does not declare.

How nesting compares to alternatives

ApproachShell duplicated?Page-type chromeBest for
One layout per page typeYes, copied everywherePer layoutTiny sites
Nested layoutsNo, shell lives in basePer child layoutMost sites
Single layout + conditionalsNoBranching if blocks2-3 small variants

Nesting scales best because each layer stays small and single-purpose, and the shell has exactly one owner.

Gotcha: Layout nesting is component composition, not inheritance — there is no extends. The child must render <BaseLayout> explicitly; simply importing it does nothing.

Best practices

  • Keep the document shell (<!doctype>, <head>, header, footer) in a single base layout and never duplicate it.
  • Give the base a named head slot so child layouts can inject styles or meta tags without the base hard-coding them.
  • Have each child layout forward only the props the base declares; spread {...Astro.props} when relaying many at once.
  • Let pages import only the most specific layout they need — they should not reach past it to the base.
  • Limit nesting to two or three levels; deeper chains become hard to trace and rarely earn their complexity.
  • Type every layout’s Props interface so missing forwarded props fail at build time, not in the browser.
Last updated June 14, 2026
Was this helpful?