Astral supports Markdown pages, Markdown-backed content collections, .astral templates, and build-time Elixir data loading. This guide maps Astro's Markdown, content collection, and data-fetching features to Astral's current APIs.
Markdown pages
Markdown files in pages/ become routes:
pages/index.md -> /
pages/about.md -> /about/
pages/blog/post.md -> /blog/post/Use YAML frontmatter for page metadata and route options:
---
title: About
layout: default.html
permalink: /about-us/
---
# AboutAstral uses MDEx for Markdown and YamlElixir for frontmatter. TOML frontmatter is not supported today.
Components in Markdown
Markdown pages and collection entries can use local .astral components through HEEx syntax:
# Project
<.callout>
Rendered by `components/callout.astral`.
</.callout>Use local component calls such as <.callout>, not MDX imports. MDX/JSX expressions are not currently supported in Astral Markdown.
Assigns are available with HEEx expression syntax. @entry is present for collection entry pages, and @entry.data contains only schema-declared fields:
<p>{@metadata["title"]}</p>
<p>{@entry.data.title}</p>Prefer static Markdown heading text. Heading IDs and @page.headings are generated before HEEx expressions are evaluated.
Rendering Markdown content
Collection detail pages can render the current entry's already-rendered Markdown safely:
<article>
<h1>{@entry.data.title}</h1>
{@entry.content}
</article>@entry.content implements Phoenix's HTML-safe protocol. Prefer this over raw string injection.
Astral does not currently expose Astro-style Markdown file imports, compiledContent(), rawContent(), <Content />, or import.meta.glob() for Markdown. Use collection helpers, ordinary Elixir file APIs, or Volt browser imports depending on the layer you are working in.
Content collections today
Define local Markdown collections in astral.config.exs:
collection :posts, "content/posts" do
permalink "/blog/:slug/"
layout "post.html"
schema do
field :title, :string, required: true
field :date, :date, required: true
field :draft, :boolean, default: false
field :tags, {:array, :string}, default: []
end
endEach Markdown file becomes an entry. Use a schema to expose cast atom-keyed values and defaults in entry.data; without a schema, entry.data is %{}. entry.metadata remains the original string-keyed frontmatter.
Collection helpers read schema-normalized data: published/1 uses entry.data[:draft], sort_by_date/2 uses entry.data[:updated] or entry.data[:date], and tags/1 uses entry.data[:tags]. Declare these fields in your schema when you want helpers or templates to see them. Prefer entry.data in your own layouts, pages, feeds, and generated routes when you need schema defaults or cast values.
Query collections from .astral setup blocks, layouts, generated routes, or plugins:
---
posts =
@site
|> Astral.Collection.entries(:posts)
|> Astral.Collection.published()
|> Astral.Collection.sort_by_date(:desc)
assigns = assign(assigns, :posts, posts)
---
<ul>
<li :for={post <- @posts}>
<a href={post.route_path}>{post.data.title}</a>
</li>
</ul>Astral does not yet provide Astro's content-loader system, single-file JSON/YAML/TOML collection loaders, collection references, generated TypeScript types, live collections, or request-time collection queries.
Generated routes from content
For one page per collection entry, use a collection permalink and optional matching dynamic .astral file route:
content/posts/hello.md -> /blog/hello/
pages/blog/[slug].astral -> /blog/:slugFor arbitrary derived routes such as tag pages, use setup-declared dynamic .astral paths:
---
posts = Astral.Collection.entries(@site, :posts)
paths =
for tag <- Astral.Collection.tags(posts) do
matching = Enum.filter(posts, &(tag in &1.data.tags))
path tag: tag, assigns: %{posts: matching}
end
---
<h1>{@params.tag}</h1>This is the current Astral equivalent of Astro's build-time static path generation, expressed as Elixir data in the page that owns the route.
Build-time data fetching
In static output mode, data loaded during discovery or rendering is build-time data. Use ordinary Elixir libraries such as Req, File, Path, Jason, database clients, or service SDKs in setup blocks, config-generated routes, or plugins.
Example one-off JSON output from fetched data:
get "/products.json", content_type: "application/json" do
products = Req.get!("https://api.example.com/products").body
Jason.encode!(products)
endExample page setup data:
---
response = Req.get!("https://api.example.com/status")
assigns = assign(assigns, :status, response.body)
---
<p>{@status["message"]}</p>During a static build, this runs when the page or route is generated. During development, it runs when Astral renders the page or generated route. Do not put secrets in browser assets; use server-side Elixir environment access for build-time data and Volt import.meta.env only for public browser values.
Remote content and CMS data
Astral does not yet have a first-class remote content loader API. For CMS or API content today, choose the shape that matches your site:
- fetch data in a plugin during discovery and add generated routes,
- fetch inside a config-level
getroute for static JSON, feeds, indexes, or generated images, - fetch in
.astralsetup for small page-local build-time data, - materialize remote content into Markdown or data files before running
mix astral.build.
Live content collections and request-time data freshness belong with future hybrid/runtime modes. See the backend, authentication, and testing guide for the current CMS/backend-service boundary.