Media & Images

In Dune, media files live next to the content that uses them. No media library, no upload forms — just files in folders.

Co-located media

Place images, PDFs, or any files directly alongside your content:

content/02.blog/01.hello-world/
├── post.md              # your content
├── cover.jpg            # hero image
├── diagram.png          # an illustration
├── presentation.pdf     # a downloadable file
└── screenshots/
    ├── step-1.png
    └── step-2.png

Reference them with plain relative paths in your Markdown:

![Cover photo](cover.jpg)
![Step 1](screenshots/step-1.png)
[Download the slides](presentation.pdf)

Dune resolves these to the correct served URLs. The files stay right next to the content that uses them — easy to find, easy to manage, easy to version control.

Media metadata

Attach metadata to any media file with a YAML sidecar:

cover.jpg.meta.yaml:

alt: "A sunset over the mountains"
credit: "Photo by Jane Doe"
title: "Mountain Sunset"
focal_point: [50, 30]

The metadata is available in templates for rendering proper alt text, image credits, and smart cropping.

Sidecar naming

The sidecar file name is always {filename}.meta.yaml:

Media file Sidecar file
cover.jpg cover.jpg.meta.yaml
diagram.png diagram.png.meta.yaml
document.pdf document.pdf.meta.yaml

Co-located HTML embeds

Co-located .html files can be embedded as iframes in your content. This is useful for interactive demos, standalone data visualisations, or any self-contained HTML document you want to display inline.

Place the HTML file alongside your post and reference it with a relative <iframe src>:

content/02.blog/01.my-post/
├── post.md
└── chart.html       ← standalone HTML file
<iframe src="./chart.html" width="100%"></iframe>

Dune rewrites the relative src to the correct absolute URL and the file is served with the proper text/html content type.

Automatic height synchronisation

When Dune serves a co-located HTML file, it automatically injects a small script that reports the document height to the parent frame. The parent page receives this message and resizes the iframe to fit the content exactly — no fixed height attribute needed, no scrollbars.

The resizing is reactive: if the iframe content changes size (e.g. due to a window resize), the height is updated automatically.

Requirement: trusted_html

Iframes require trusted_html to be enabled, since the HTML sanitiser strips <iframe> tags by default. Set it at the page or site level:

# In post frontmatter (page level)
trusted_html: true
# In config/site.yaml (site level — applies to all pages)
trusted_html: true

A complete example:

---
title: "My Post"
trusted_html: true
---

Here is the interactive chart:

<iframe src="./chart.html" width="100%"></iframe>

The chart shows...

Supported file types

Dune serves any file placed in a content folder. Common types:

Images: .jpg, .jpeg, .png, .gif, .webp, .avif.svg

Video: .mp4, .webm.ogg

Audio: .mp3.wav

Documents: .pdf, .zip, .csv.html

In TSX content pages

TSX content pages access media through the media helper prop:

export default function Page({ media }: ContentPageProps) {
  return (
    <article>
      <img src={media.url("hero.jpg")} alt="Hero" />
      <p>Credit: {media.get("hero.jpg")?.meta.credit}</p>

      {media.list().map((file) => (
        <img key={file.name} src={file.url} alt={file.meta.alt || file.name} />
      ))}
    </article>
  );
}

The media helper provides:

Method Returns Description
media.url(filename) string URL to serve the file
media.get(filename) MediaFile | null Full file object with metadata
media.list() MediaFile[] All media files for this page

Image processing

Dune can resize, crop, and convert images on the fly. Add query parameters to any image URL:

/blog/post/cover.jpg?width=800&format=webp

Parameters

Param Alias Description
width w Target width in pixels
height h Target height in pixels
quality q Output quality 1–100 (default: 80)
format f Output format: jpeg, png, webp, avif
fit Resize mode: cover (default), contain, fill, inside, outside
focal Focal point for cover crop as x,y percentages — e.g. 50,30

Example URLs

# Resize to 800px wide, keep aspect ratio
cover.jpg?width=800

# Resize and convert to WebP
cover.jpg?width=800&format=webp

# Thumbnail at 320×240, crop from top-center
cover.jpg?width=320&height=240&fit=cover&focal=50,20

# Keep size, convert and compress
cover.jpg?format=webp&quality=60

Allowed sizes

Width and height values must match one of the sizes in system.images.allowed_sizes (default: 320, 640, 768, 1024, 1280, 1536, 1920). Requests outside this list return 400. This protects against resize attacks.

Caching

Processed images are cached on disk (.dune/cache/images/) and served with a one-year immutable cache header. The first request processes the image; all subsequent requests are served from cache.

Best practices

Name files descriptively. cover.jpg is better than IMG_3847.jpg. File names appear in URLs.

Use folders for groups. If a page has many screenshots, put them in a screenshots/ subfolder.

Add alt text. Either in the Markdown ![alt text](image.jpg) or in the .meta.yaml sidecar. Screen readers and SEO need it.

Keep files near content. Resist the urge to create a global images/ folder. Co-location makes content portable — move a folder and everything comes with it.