Skip to main content

guides/features/pages-and-layouts.md

# Pages and Layouts

Astral discovers pages from the configured `pages/` directory and renders them into static HTML routes.

## Markdown pages

Markdown pages use MDEx and YAML frontmatter:

```markdown
---
title: About
layout: default.html
---

# About
```

Routes are derived from file paths:

```text
pages/index.md      -> /
pages/about.md      -> /about/
pages/blog/post.md  -> /blog/post/
```

Set `permalink` to override the route:

```markdown
---
title: About
permalink: /about-us/
---
```

Set `layout: false` to render a page without a layout.

## Components in Markdown

Markdown pages and collection entries can use local `.astral` components through MDEx's HEEx integration:

```md
# Project

<.callout>
  This block is rendered by `components/callout.astral`.
</.callout>
```

Components are discovered from the configured `components/` directory, the same as `.astral` pages and layouts. Use HEEx local component syntax (`<.name>`) rather than MDX imports.

Assigns are available in Markdown with HEEx expression syntax:

```md
<p>{@metadata["title"]}</p>
<p>{@entry.data.title}</p>
```

Prefer static Markdown heading text. Heading ids are generated by the Markdown parser before HEEx expressions are evaluated.

## HTML pages

Plain `.html` files in `pages/` are supported for pages that do not need Markdown processing.

## Dynamic file routes

Use bracket segments for collection-backed dynamic pages:

```text
pages/blog/[slug].astral   -> /blog/:slug
pages/docs/[...path].md    -> /docs/*path
```

Bracket filenames are portable file-route sugar. Astral converts them to Elixir/Phoenix-style route patterns internally.

Dynamic page templates render once for each collection entry whose route matches the file route pattern. Route params are exposed as string-keyed `@params` assigns:

```astral
<h1>{@entry.data.title}</h1>
<p>Slug: {@params["slug"]}</p>
```

```eex
<p>Path: <%= @params["path"] %></p>
```

A dynamic file route overrides the default collection entry page for the same route, letting the page template own the final HTML.

## Layouts

Layouts live in the configured layouts directory. EEx layouts use `@content` where the page HTML should be inserted:

```html
<!doctype html>
<html lang="en">
  <head>
    <title><%= @page.title || "My Site" %></title>
  </head>
  <body>
    <main data-route="<%= @route %>">
      <%= @content %>
    </main>
  </body>
</html>
```

Common assigns:

- `@content` — rendered page HTML.
- `@page` — current `%Astral.Content{}`.
- `@metadata` — decoded frontmatter map.
- `@route` — route path such as `/about/`.
- `@params` — string-keyed route params for dynamic file routes.
- `@site` — discovered `%Astral.Site{}`.
- `@collections` — content entries grouped by collection name.
- `@entry` — current collection entry, otherwise `nil`.
- `@routes` — generated routes.

## Heading anchors

Markdown headings get stable `id` attributes. Heading metadata is available as `@page.headings` for table-of-contents layouts:

```eex
<nav>
  <%= for heading <- @page.headings do %>
    <a href="#<%= heading.id %>"><%= heading.text %></a>
  <% end %>
</nav>
```