Components and layouts

Build a page layout

Share the complete document structure and page metadata while keeping each route responsible for its own content.

Once a site has two pages, the shared HTML starts to repeat. The doctype, the <head>, the navigation, the footer. Change the header and you’re editing five files, hoping you got them all.

A layout fixes this. It’s an Astro component that owns the shared document structure. Each page supplies only its own content. By convention layouts live in src/layouts/, but they’re ordinary components. Nothing special about the folder.

Here is src/layouts/Layout.astro:

---
interface Props {
  title: string
  description: string
}

const { title, description } = Astro.props
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="description" content={description} />
    <title>{title}</title>
  </head>
  <body>
    <nav><a href="/">Home</a></nav>
    <main><slot /></main>
    <footer>Built with Astro</footer>
  </body>
</html>

The <slot /> marks where the page content goes. The props fill the metadata.

A page imports the layout and wraps itself in it:

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

<Layout title="About" description="Who built this site and why">
  <h1>About</h1>
  <p>This content fills the layout's slot.</p>
</Layout>

Everything between <Layout> and </Layout> replaces the slot. Build the site and dist/about/index.html contains the full document: doctype, a head with the right title, the nav, the h1 inside main, the footer.

Who owns what

The split is the useful part. The page knows its own title and description, so it passes them. The layout knows how to emit a valid document, so it owns the doctype, the lang attribute, the nav. One place to fix the header. One place to add a stylesheet.

I make title and description required in Props on purpose. Forget one and the build tells you. A page without a description is a mistake I want caught early.

Layouts compose

Because a layout is a component, it can use another layout. A blog can have src/layouts/PostLayout.astro that imports Layout, adds the post header inside it, and passes title and description through:

---
import Layout from './Layout.astro'

interface Props {
  title: string
  description: string
  date: Date
}

const { title, description, date } = Astro.props
---

<Layout title={title} description={description}>
  <article>
    <h1>{title}</h1>
    <time>{date.toLocaleDateString('en-GB')}</time>
    <slot />
  </article>
</Layout>

Pages pick the most specific layout they need. Nested layouts still cost nothing in the browser, because they all render on the server.

The missing slot

The classic mistake: you write a layout and forget <slot />. Nothing errors. The page renders a perfect shell with an empty <main>. If content disappears inside a correct-looking page, check the layout for a missing slot before you debug the page.

Build two pages with the same layout and compare their HTML. The shell should be identical. Only the title, the description, and the main content should differ.

Lesson completed