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:
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.webpResponsive 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:
image quality: 82,
widths: [480, 768, 1200, 1600],
formats: [:webp],
fallback_format: :jpegFiles in public/ are still copied as-is. Use <.image>, <.picture>, or <.figure> when you want Astral to optimize and hash an image.
Public files and output precedence
Static builds write outputs in this order:
- copy files from
public/, - render pages from
pages/, - render generated routes from plugins and config-level
getdeclarations.
When two sources write the same output path, the later source wins. For example, pages/404.md overwrites public/404.html, and a generated route for /robots.txt overwrites public/robots.txt.
Prefer avoiding duplicate output paths. The current behavior is deterministic so small sites can intentionally override public files, but future diagnostics may warn about accidental collisions.
Inline SVG files
Use <.svg> when you want to include a project SVG file directly in the rendered HTML, such as clip-path definitions, masks, or a hand-authored logo:
<.svg src="@/icons/clip-paths.svg" class="sr-only" />Astral asks Volt to resolve the source path, so configured Volt aliases and tsconfig.json paths work the same way as browser asset imports. The SVG is parsed as XML, must have an <svg> root, and is rendered through Phoenix's HTML-safe protocol. Root attributes from the component call override attributes from the file.
Use <.svg> for trusted local source files. It is intentionally narrower than generic raw HTML injection.
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:
image do
allow_remote "https://images.example.com/**"
allow_remote "https://**.amazonaws.com/bucket/**"
endAllowed 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.commatches one subdomain level.**.example.commatches 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.
Image service and CDN boundaries
Astro exposes a public Image Service API with local services, external CDN services, getImage(), on-demand /_image endpoints, and adapter integrations such as Netlify Image CDN. Those APIs let image transformations move between build time, request time, and provider CDNs.
Astral's current image service boundary is narrower and Elixir-native:
Astral.Image.Serviceis an internal backend behaviour used by Astral's Vips/libvips transformer.- Static builds generate optimized image files into the asset output directory.
- Development uses Astral's
/_astral/image/...endpoint for on-demand local/dev transforms only. - Production on-demand image transformation, external image services, provider image CDNs, and a public
getImage()helper are not implemented yet.
Use ordinary <img> or host/CDN URLs when a provider should own transformation. Use <.image>, <.picture>, and <.figure> when Astral should transform files during the static build. Future deployment adapters may add provider-specific image service integration, but that belongs in Astral's site/runtime layer rather than Volt's browser asset layer.
Volt browser assets
Astral delegates browser assets to Volt.
Configure Volt assets
assets do
entry "app.ts"
url_prefix "/assets"
endThe 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.cssDisable hashes for examples or prototypes that need stable filenames:
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. See Astral's styling and browser code guide for current CSS, font, syntax-highlighting, script, and framework-island boundaries.