Skip to content

Media Library

EmDash includes a media library for managing images, documents, and other files. This guide covers uploading, finding, and using media in your content.

Open the media library from the admin sidebar by clicking Media. The Main library shows folders and files that are not assigned to a folder. Open a folder to see its files.

EmDash media library showing image grid with upload button

Open a file in EmDash’s media library to see the content entries that reference it. While EmDash scans existing content, the list includes the references found so far and may be incomplete.

If media usage tracking is off, an administrator can turn it on:

  1. Finish any content edits. If another application writes directly to the content database, pause it and wait for any writes in progress to finish.
  2. Open Settings → Media usage tracking, select Enable tracking, then confirm.
  3. When the page shows Indexing existing content, editing and other database writes can resume.
  4. Keep the page open until it shows Ready. If you leave, return to continue from saved progress.

Once media usage tracking is on, it cannot be turned off.

  1. Open Media in the admin sidebar.

  2. Select Upload Files, then Browse files to choose one or more files. You can also drag files anywhere onto the media library.

  3. Uploads start automatically. The dialog shows each file’s status and lets you cancel or retry individual files.

  1. In the rich text editor, click the image button

  2. Click Upload in the media picker

  3. Select a file from your computer

  4. Add alt text and click Insert

EmDash accepts these file types by default:

CategoryExtensions
Images.jpg, .jpeg, .png, .gif, .webp, .avif
Documents.pdf
Video.mp4, .webm, .mov
Audio.mp3, .wav, .ogg

Image and file fields can allow other MIME types, including image/svg+xml for SVG files.

EmDash supports multiple storage backends. Configure storage in your Astro config:

astro.config.mjs
import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
export default defineConfig({
integrations: [
emdash({
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});

Files are stored in the ./uploads directory. Suitable for development and single-server deployments.

The admin uses the upload target flow:

  1. The client requests an upload target, and EmDash creates a pending media item.

  2. The client uploads the file to the returned target.

  3. The client confirms the upload.

  4. EmDash validates the stored file and marks the media item as ready.

S3-compatible storage returns a signed URL so the file can bypass the application runtime. Local storage and native R2 return a same-origin streaming endpoint instead.

Use the search box to find files by name. Search matches partial filenames.

Use the type filter to show images, documents, video, or audio files.

Editors can select Add new folder from the Main library. Open a folder by selecting its name. Without a search term, folder pages show only the media assigned to that folder. Filename searches cover the whole library, including other folders and the Main library.

To move a local file into a visible folder, drag its grid card or list row onto the folder. You can also open Media Details, choose a Location, and select Save. Use Location to return a file to the Main library or to move it without dragging.

Authors can move local files they uploaded. Editors can move any local file. Files from external providers cannot be assigned to folders.

Uploads enter the Main library. Move them into a folder after upload using either method above.

Deleting a folder returns its media to the Main library. The media files, URLs, and content references remain unchanged.

  1. Place your cursor where you want the image

  2. Click the image button in the toolbar

  3. Select an image from the media library or upload a new one

  4. Enter alt text

  5. Click Insert

  1. Open a content entry in the editor

  2. Find the Featured Image field in the sidebar

  3. Click Select Image

  4. Choose from the media library or upload

  5. Click Save

For fields configured as image or file types, click the field to open the media picker.

A focal point keeps the important part of a local image visible when a card, gallery, or other layout crops it to fill a fixed shape.

  1. Open Media, then select an image from the local library.
  2. Select Focal point.
  3. Click or drag the marker onto the important part of the image. You can also use the Arrow keys.
  4. Check the square, landscape, and portrait previews, then select Save.

Select Reset to remove a custom focal point. The saved point is copied when you select the image for a content field or gallery. Content that already uses the image keeps its stored point until you select the image again.

Access media URLs from your content data:

src/pages/posts/[slug].astro
---
import { getEmDashEntry } from "emdash";
const { entry: post } = await getEmDashEntry("posts", Astro.params.slug);
---
{post?.data.featured_image && (
<img
src={post.data.featured_image}
alt={post.data.featured_image_alt ?? ""}
/>
)}

For EmDash media fields, use the Image component from emdash/ui:

---
import { Image } from "emdash/ui";
import { getEmDashEntry } from "emdash";
const { entry: post } = await getEmDashEntry("posts", Astro.params.slug);
---
{post?.data.featured_image && (
<Image
image={post.data.featured_image}
width={800}
height={450}
priority
/>
)}

priority is for the primary above-the-fold image. It sets loading="eager" and fetchpriority="high": loading controls whether loading is deferred, and fetchpriority gives the browser a priority hint for the request.

When the field value carries a dark counterpart, Image renders both and shows the one matching the visitor’s color scheme. Dark Mode covers enabling the slot on a field and the <html> class convention the component relies on.

EmDash installs an image endpoint that produces the resized variants on request. On Cloudflare Workers that endpoint uses the IMAGES binding. Image Transformation covers where the binding comes from and what happens when it is absent.

  1. Select the file(s) you want to delete

  2. Click Delete

  3. Confirm the deletion

Use the REST API to upload, list, update, delete, and organize local media. The media endpoint reference documents the direct multipart upload and upload target flows, request parameters, response shapes, permissions, and folder operations.

In addition to local storage, EmDash supports external media providers for specialized image and video hosting. Media providers appear as tabs in the media picker, letting editors choose from multiple sources.

Cloudflare Images provides image hosting with automatic optimization, resizing, and format conversion.

astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { cloudflareImages } from "@emdash-cms/cloudflare";
export default defineConfig({
integrations: [
emdash({
// ... database, storage config
mediaProviders: [
cloudflareImages({
accountId: import.meta.env.CF_ACCOUNT_ID,
apiToken: import.meta.env.CF_IMAGES_TOKEN,
// Optional: custom delivery domain
deliveryDomain: "images.example.com",
}),
],
}),
],
});

Features:

  • Browse and upload images directly from the admin
  • Automatic image optimization and format conversion
  • URL-based transformations (resize, crop, format)
  • Flexible variants for responsive images

You can configure multiple providers. Each appears as a tab in the media picker:

astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { cloudflareImages, cloudflareStream } from "@emdash-cms/cloudflare";
export default defineConfig({
integrations: [
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
mediaProviders: [
cloudflareImages({
accountId: import.meta.env.CF_ACCOUNT_ID,
apiToken: import.meta.env.CF_IMAGES_TOKEN,
}),
cloudflareStream({
accountId: import.meta.env.CF_ACCOUNT_ID,
apiToken: import.meta.env.CF_STREAM_TOKEN,
}),
],
}),
],
});

The local media library (“Library” tab) is always available alongside any configured providers.

Use the Image component to render media:

src/pages/posts/[slug].astro
---
import { Image } from "emdash/ui";
import { getEmDashEntry } from "emdash";
const { entry: post } = await getEmDashEntry("posts", Astro.params.slug);
---
{post?.data.featured_image && (
<Image
image={post.data.featured_image}
width={800}
height={450}
/>
)}

The component automatically:

  • Detects the provider from the stored value
  • Renders an optimized <img> element
  • Applies provider-specific optimizations (e.g., Cloudflare Images transformations)

A file field stores a reference and a metadata snapshot. Cached fields such as url, filename, mimeType, and size are optional because persisted values may omit them:

interface FileValue {
id: string;
url?: string; // Legacy cached URL
src?: string; // Direct URL from an external provider
filename?: string; // Cached original filename
mimeType?: string; // Cached MIME type
size?: number; // Cached size, when available
provider?: string; // Defaults to "local"
meta?: Record<string, unknown>;
}

getEmDashEntry() and getEmDashCollection() return this stored value without an extra media query. For current metadata, use the configured provider’s get() method explicitly. Use getEmbed() for the provider-specific render URL:

---
const file = post.data.attachment;
const provider = file
? Astro.locals.emdash?.getMediaProvider(file.provider ?? "local")
: undefined;
const current = file ? await provider?.get?.(file.id) : null;
const embed = file && provider ? await provider.getEmbed(file) : null;
---

Authenticated HTTP clients can make the same explicit lookup through GET /_emdash/api/media/:id for local media or GET /_emdash/api/media/providers/:providerId/:itemId for another provider.

For a local file URL, use the stored meta.storageKey with the public URL helper. This respects a configured R2 or S3 public domain without querying the media table:

---
const storageKey =
typeof file?.meta?.storageKey === "string" ? file.meta.storageKey : undefined;
const url = storageKey
? Astro.locals.emdash?.getPublicMediaUrl?.(storageKey)
: file?.src ?? file?.url;
---

Provider lookups may perform network or database work. Avoid one lookup per file on logged-out collection pages; use the stored snapshot and rendering components unless the request needs fresh metadata.

Media fields store a MediaValue object containing provider information:

interface MediaValue {
provider?: string; // Provider ID, defaults to "local"
id: string; // Provider-specific ID
src?: string; // Direct URL (for local media or plain-string values)
previewUrl?: string; // Preview URL for admin display (external providers)
filename?: string; // Original filename
mimeType?: string; // MIME type
width?: number; // Image/video width
height?: number; // Image/video height
focalX?: number; // Horizontal focal position from 0 to 1
focalY?: number; // Vertical focal position from 0 to 1
alt?: string; // Alt text
meta?: Record<string, unknown>; // Provider-specific metadata
}

This allows EmDash to render media correctly regardless of where it’s hosted.