Astral has two asset paths:

  • Astral owns site images rendered with <.image> and <.picture>.
  • Volt owns browser assets such as TypeScript, JavaScript, CSS, and files imported by them.

Optimized images

Use built-in HEEx components from .astral pages, layouts, components, and component-aware Markdown:

<.image src="images/hero.jpg" alt="Hero" width={1200} format={:webp} quality={82} />

Ordinary local Markdown images are optimized too:

![Hero](./hero.jpg "Optional title")

src resolves from the configured image source directories. By default, Astral looks under assets/, the site root, and public/.

Static builds write compressed, content-hashed files into the configured asset output directory:

assets/images/hero.jpg
  -> dist/assets/hero-1200x800-k4L9v8B2qa.webp

Responsive pictures generate multiple variants and a fallback image:

<.picture
  src="images/hero.jpg"
  alt="Hero"
  widths={[480, 768, 1200]}
  formats={[:webp, :avif]}
  fallback_format={:jpeg}
  sizes="(max-width: 768px) 100vw, 1200px"
/>

The image pipeline is backed by the Elixir Image package and libvips. Output filenames include source content and transform options, so changing the source, dimensions, format, quality, or fit creates a new browser URL. Markdown image syntax uses the default image format and original dimensions unless you use <.image>, <.picture>, or <.figure> for explicit transforms.

Use <.figure> for semantic captions:

<.figure
  src={@entry.data.cover}
  alt={@entry.data.title}
  caption={@entry.data.description}
  width={800}
/>

Or pass rich caption content through the default slot:

<.figure src="images/chart.png" alt="Revenue by month" width={800}>
  <strong>Figure 1.</strong> Monthly revenue.
</.figure>

Configure defaults with the image option:

site do
  image quality: 82,
        widths: [480, 768, 1200, 1600],
        formats: [:webp],
        fallback_format: :jpeg
end

Files in public/ are still copied as-is. Use <.image>, <.picture>, or <.figure> when you want Astral to optimize and hash an image.

Resolved collection image fields expose dimensions and format:

<p>{@entry.data.cover.width} × {@entry.data.cover.height} {@entry.data.cover.format}</p>

Use Astral.Image.metadata/1 when you need metadata for an image path during rendering:

---
assigns = assign(assigns, :hero, Astral.Image.metadata("images/hero.jpg"))
---
<p>{@hero.width} × {@hero.height} {@hero.format}</p>

During development, Astral emits /_astral/image/... URLs and generates optimized images on demand into the image cache. Responses use no-cache headers like Volt's dev asset server, so browser refreshes reflect source changes.

Remote images

Astral does not optimize arbitrary remote URLs by default. Allow trusted remote image sources explicitly:

site do
  image do
    allow_remote "https://images.example.com/**"
    allow_remote "https://**.amazonaws.com/bucket/**"
  end
end

Allowed remote images flow through the same <.image>, <.picture>, and <.figure> components:

<.image src="https://images.example.com/hero.jpg" alt="Hero" width={800} />

Remote pattern wildcards follow Astro's model:

  • *.example.com matches one subdomain level.
  • **.example.com matches any subdomain depth.
  • /assets/* matches one nested path segment.
  • /assets/** matches any nested path below /assets/.

Remote redirects are followed only when every destination also matches an allow_remote pattern. Astral caches the downloaded original in the image cache and reuses response validators such as ETag and Last-Modified when available.

Static builds fetch allowed remote originals during generation. Because the original is available at build time, Astral can infer a missing dimension from the remote image metadata:

<.image src="https://images.example.com/hero.jpg" alt="Hero" width={800} />

Development mirrors Astro's endpoint model: page rendering emits /_astral/image/... without downloading the remote original, and the remote request happens when the browser asks for that optimized image. In dev, specify both width and height for remote images so Astral can avoid fetching during page render.

Volt browser assets

Astral delegates browser assets to Volt.

Configure Volt assets

site do
  assets "assets" do
    entry "app.ts"
    url_prefix "/assets"
  end
end

The source root is assets/; the browser URL prefix is /assets.

Reference assets from layouts

Use Astral.asset_path/2 with the source entry name:

<script type="module" src="<%= Astral.asset_path(@site, "app.ts") %>"></script>

In development this returns a source URL served by Volt, such as /assets/app.ts. In static builds it reads Volt's manifest and returns the emitted output path, such as /assets/app-5e6f7a8b.js.

Content hashes

Content hashes are enabled by default for production caching:

app.ts -> app-5e6f7a8b.js
styles.css -> styles-1a2b3c4d.css

Disable hashes for examples or prototypes that need stable filenames:

assets "assets" do
  entry "app.ts"
  hash false
end

.astral template assets

<style> and <script> blocks in .astral templates are extracted as Volt embedded modules:

<style>
  .card { border: 1px solid currentColor; }
</style>

<script lang="ts">
  console.log("loaded");
</script>

Volt builds those blocks alongside the configured asset entry.

Volt features

For TypeScript, CSS, imported assets, asset query modes, workers, code splitting, HMR, and formatting/linting, see the Volt documentation.