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

Markdown pages

Markdown pages use MDEx and YAML frontmatter:

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

# About

Routes are derived from file paths:

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

Set permalink to override the route:

---
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:

# 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:

<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:

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:

<h1>{@entry.data.title}</h1>
<p>Slug: {@params["slug"]}</p>
<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:

<!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:

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