Skip to main content

guides/features/plugins.md

# Plugins and Integrations

Astral plugins extend static site discovery, rendering, and build lifecycle behavior. Volt plugins extend the browser asset graph. Together they cover the same broad territory that Astro calls integrations, but the responsibilities are split by layer.

Use an **Astral plugin** for site semantics:

- generated routes such as feeds, sitemaps, search indexes, and API-like static files,
- content or route discovery changes,
- rendered HTML transforms,
- build lifecycle hooks.

Use a **Volt plugin** or Volt config for browser assets:

- JavaScript, TypeScript, CSS, and imported assets,
- Vue, React, Svelte, Solid, and other framework compilation,
- virtual browser modules and entries,
- compile-time constants and `import.meta.env`,
- asset HMR and production bundling.

## Configure plugins

```elixir
plugin MySite.SEOPlugin
plugin MySite.AnalyticsPlugin, id: "G-XXXX"
```

Plugins are modules implementing `Astral.Plugin`. Options are passed to callbacks that define an extra argument.

## Render hook example

```elixir
defmodule MySite.AnalyticsPlugin do
  @behaviour Astral.Plugin

  @impl true
  def name, do: "analytics"

  @impl true
  def render_page(html, _page, _site, opts) do
    id = Keyword.fetch!(opts, :id)
    {:ok, String.replace(html, "</body>", ~s(<script data-id="#{id}"></script></body>))}
  end
end
```

## Generated route example

```elixir
defmodule MySite.SearchIndex do
  @behaviour Astral.Plugin

  @impl true
  def name, do: "search-index"

  @impl true
  def routes(site) do
    [Astral.Route.new("/search.json", site.config, content_type: "application/json")]
  end

  @impl true
  def render_route(%Astral.Route{path: "/search.json"}, site) do
    entries = Map.get(site.entries, :posts, [])
    {:ok, JSON.encode!(Enum.map(entries, &%{title: &1.data.title, url: &1.route_path}))}
  end

  def render_route(_route, _site), do: nil
end
```

## Config-generated routes

For one-off static outputs, prefer top-level `get` declarations in `astral.config.exs` instead of writing a reusable plugin:

```elixir
get "/robots.txt", content_type: "text/plain" do
  "User-agent: *\nAllow: /\n"
end

get "/search-index.json", content_type: "application/json" do
  Jason.encode!(MySite.Search.index(site))
end
```

Use `plug` for middleware around these generated responses:

```elixir
plug MySite.GeneratedHeaders, cache: "public, max-age=3600"
```

## Framework and asset integrations

Frontend frameworks are configured through Volt and Astral islands, not Astral site plugins. The basic example enables Vue and React with Volt plugins:

```elixir
# config/config.exs
config :volt,
  plugins: [Volt.Plugin.Vue, Volt.Plugin.React],
  import_source: "react"
```

Then `.astral` pages can mount client islands:

```astral
<.vue component="islands/Gallery.vue" client={:visible} props={%{title: "Gallery"}} />
<.react component="islands/ReactCounter.jsx" client={:load} props={%{count: 1}} />
```

See the assets, islands, and Volt plugin documentation when the integration affects browser code rather than site discovery or rendering.

## Installation and scaffolding

Astral does not currently have an `astro add`-style command for arbitrary integrations. Use normal Mix and Hex workflows:

- install Astral in a project with `mix igniter.install astral`,
- scaffold starter files with `mix astral.new`,
- add Elixir packages to `mix.exs`,
- add browser packages to `package.json` only when your chosen Volt/browser tooling needs them,
- configure Astral plugins in `astral.config.exs` and Volt plugins in `config/config.exs`.

## Hooks

Available hooks include:

- `config/1`
- `build_start/1`
- `site_discovered/1`
- `routes/1`
- `render_route/2`
- `render_page/3`
- `build_done/1`

Use `enforce/0` to return `:pre` or `:post` when ordering matters.