# Markdown, Content, and Data Fetching

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:

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

Use YAML frontmatter for page metadata and route options:

```md
---
title: About
layout: default.html
permalink: /about-us/
---

# About
```

Astral uses MDEx for Markdown and `YamlElixir` for frontmatter. TOML frontmatter is not supported today.

## Markdown options and syntax highlighting

Pass [MDEx options](https://hexdocs.pm/mdex/MDEx.html) through the site configuration:

```elixir
site do
  markdown extension: [strikethrough: true, table: true]
end
```

The same options apply during discovery and final rendering of Markdown pages and
collection entries, including content used by feeds. Astral retains its YAML
frontmatter delimiter and heading ID convention, and enables HEEx in the final
page rendering pass. Existing sites keep their current defaults.

For server-rendered syntax highlighting, add Lumis and a parser package for each
language your code fences use to your site's Mix dependencies:

```elixir
{:lumis, "~> 0.10"},
{:lumis_wasm_elixir, "~> 0.26"},
{:lumis_wasm_bundle_web, "~> 0.1"}
```

Then select the native highlighter in `config/config.exs`:

```elixir
config :mdex_native, syntax_highlighter: :lumis
```

Then configure Lumis through MDEx in `astral.config.exs`:

```elixir
markdown syntax_highlight: [
  engine: :lumis,
  opts: [
    formatter: {:html_multi_themes,
      themes: [light: "github_light", dark: "github_dark"],
      default_theme: "light-dark()"}
  ]
]
```

Use language-tagged code fences such as `elixir` or `sql`. The generated HTML
needs no browser highlighter. The dual-theme formatter follows CSS `color-scheme`;
set it to `light dark` for the system preference or explicitly to `light`/`dark`
for a site toggle. A fence in a language without an installed parser renders as
plain text; the [language catalog](https://docs.lumis.sh/reference/languages) lists
every package, and bundles such as `lumis_wasm_bundle_web` install a set at once.

If MDEx Native was already compiled with another highlighter, run
`mix deps.clean mdex_native --build` before rebuilding (also in `MIX_ENV=test`
when running tests).

## Markdown plugins

[MDEx plugins](https://hexdocs.pm/mdex/plugins.html) go in the same options, in any
of the forms MDEx accepts:

```elixir
markdown plugins: [MDExGFM, {MDExMermaid, mermaid_version: "11"}]
```

A plugin is a module with `attach/2` that adds steps to the parsed document, so a
site can write its own in `lib/`. This one wraps every blockquote in a
`<div class="quote">`:

```elixir
defmodule MySite.Markdown.Quotes do
  alias MDEx.Document

  def attach(document, _options \\ []) do
    Document.append_steps(document, wrap_quotes: &wrap_quotes/1)
  end

  defp wrap_quotes(document) do
    Document.update_nodes(document, MDEx.BlockQuote, fn quote ->
      %MDEx.BlockDirective{info: "quote", nodes: [quote]}
    end)
  end
end
```

Plugins run on pages and collection entries while MDEx parses them, so they see
Markdown images before Astral turns local ones into `<.image>` components, and
the headings Astral collects for a table of contents include their changes.

## Components in Markdown

Markdown pages and collection entries can use local `.astral` components through HEEx syntax:

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

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

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

```elixir
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
end
```

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

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

```text
content/posts/hello.md     -> /blog/hello/
pages/blog/[slug].astral   -> /blog/:slug
```

For arbitrary derived routes such as tag pages, use setup-declared dynamic `.astral` paths:

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

```elixir
get "/products.json", content_type: "application/json" do
  products = Req.get!("https://api.example.com/products").body
  Jason.encode!(products)
end
```

Example page setup data:

```astral
---
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 `get` route for static JSON, feeds, indexes, or generated images,
- fetch in `.astral` setup 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.
