---
title: Media Library
description: Upload and manage images and files in EmDash.
---

import { Aside, Steps, Tabs, TabItem } from "@astrojs/starlight/components";
import mediaLibraryImg from "../../../assets/screenshots/admin-media-library.png";

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

## Accessing the Media Library

Open the media library from the admin sidebar by clicking **Media**. The library displays all uploaded files with previews, filenames, and upload dates.

<img src={mediaLibraryImg.src} alt="EmDash media library showing image grid with upload button" />

## Uploading Files

### From the Media Library

1. Click **Media** in the admin sidebar

2. Click **Upload** or drag files onto the upload area

3. Select one or more files from your computer

4. Wait for uploads to complete

### From the Content Editor

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**

## Supported File Types

EmDash supports common web file types:

| Category  | Extensions                                                |
| --------- | --------------------------------------------------------- |
| Images    | `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.avif`, `.svg` |
| Documents | `.pdf`, `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx` |
| Video     | `.mp4`, `.webm`, `.mov`                                   |
| Audio     | `.mp3`, `.wav`, `.ogg`                                    |

<Aside type="caution">
	Maximum file size depends on your storage configuration. The default limit is 10MB per file.
</Aside>

## Storage Backends

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

<Tabs>
  <TabItem label="Local Storage">
```js title="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.

  </TabItem>
  <TabItem label="Cloudflare R2">
```js title="astro.config.mjs"
import { defineConfig } from "astro/config";
import emdash, { r2 } from "emdash/astro";

export default defineConfig({
  integrations: [
    emdash({
      storage: r2({
        binding: "MEDIA_BUCKET",
        publicUrl: "https://media.example.com",
      }),
    }),
  ],
});
```

Requires an R2 bucket configured in `wrangler.jsonc`:

```jsonc title="wrangler.jsonc"
{
	"r2_buckets": [
		{
			"binding": "MEDIA_BUCKET",
			"bucket_name": "my-media-bucket",
		},
	],
}
```

  </TabItem>
  <TabItem label="S3-Compatible">
```js title="astro.config.mjs"
import { defineConfig } from "astro/config";
import emdash, { s3 } from "emdash/astro";

export default defineConfig({
  integrations: [
    emdash({
      storage: s3({
        endpoint: "https://s3.amazonaws.com",
        bucket: "my-media-bucket",
        accessKeyId: process.env.S3_ACCESS_KEY_ID,
        secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
        region: "us-east-1",
        publicUrl: "https://media.example.com",
      }),
    }),
  ],
});
```

Works with Cloudflare R2 (via S3 API), MinIO, and other S3-compatible services.

  </TabItem>
</Tabs>

## How Uploads Work

EmDash uses signed URLs for secure uploads:

1. Client requests an upload URL from the API

2. Server generates a signed URL with expiration

3. Client uploads directly to storage using the signed URL

4. Server records the file metadata in the database

This approach keeps large files off your application server and enables direct uploads to cloud storage.

<Aside>
	R2 bindings do not support pre-signed URLs. When using the R2 binding adapter, uploads go through
	your Worker.
</Aside>

## Organizing Media

### Folders

Create folders to organize your media:

1. Click **New Folder** in the media library

2. Enter a folder name

3. Click **Create**

4. Drag files into folders to organize them

### Search

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

### Filters

Filter media by:

- **Type** - Images, Documents, Video, Audio
- **Date** - Upload date range
- **Folder** - Specific folder

## Using Media in Content

### In the Rich Text Editor

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**

### As a Featured Image

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**

### In Custom Fields

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

## Displaying Media in Templates

Access media URLs from your content data:

```astro title="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 ?? ""}
  />
)}
```

### Responsive Images

For responsive images, use Astro's Image component with external URLs:

### Responsive Images

For responsive images, use Astro's Image component with external URLs:

```astro
---
import { Image } from "astro:assets";
import { getEmDashEntry } from "emdash";

const { entry: post } = await getEmDashEntry("posts", Astro.params.slug);
---

{post?.data.featured_image && (
  <Image
    src={post.data.featured_image}
    alt={post.data.featured_image_alt ?? ""}
    width={800}
    height={450}
  />
)}
```

<Aside type="tip">
Configure allowed image domains in `astro.config.mjs` to use the Image component with external URLs:

```js
export default defineConfig({
	image: {
		domains: ["media.example.com"],
	},
});
```

</Aside>

## Deleting Media

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

2. Click **Delete**

3. Confirm the deletion

<Aside type="caution">
	Deleting media does not remove references in your content. Ensure you update or remove content
	that uses deleted files.
</Aside>

## Media API

Access media programmatically using the admin API.

### Upload a File

Upload media as multipart form data:

```bash
POST /_emdash/api/media
Content-Type: multipart/form-data
Authorization: Bearer YOUR_API_TOKEN

file=<binary file data>
```

Response:

```json
{
	"success": true,
	"data": {
		"item": {
			"id": "01ABC123",
			"filename": "hero-image.jpg",
			"mime_type": "image/jpeg",
			"storage_key": "media/abc123/hero-image.jpg",
			"width": 1200,
			"height": 800
		}
	}
}
```

### List Media

```bash
GET /_emdash/api/media?prefix=images/&limit=20
Authorization: Bearer YOUR_API_TOKEN
```

### Delete Media

```bash
DELETE /_emdash/api/media/images/hero.jpg
Authorization: Bearer YOUR_API_TOKEN
```

## Media Providers

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.

### Available Providers

<Tabs>
  <TabItem label="Cloudflare Images">
[Cloudflare Images](https://developers.cloudflare.com/images/) provides image hosting with automatic optimization, resizing, and format conversion.

```js title="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

</TabItem>
<TabItem label="Cloudflare Stream">
[Cloudflare Stream](https://developers.cloudflare.com/stream/) provides video hosting with HLS/DASH adaptive streaming.

```js title="astro.config.mjs"
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { cloudflareStream } from "@emdash-cms/cloudflare";

export default defineConfig({
  integrations: [
    emdash({
      // ... database, storage config
      mediaProviders: [
        cloudflareStream({
          accountId: import.meta.env.CF_ACCOUNT_ID,
          apiToken: import.meta.env.CF_STREAM_TOKEN,
          // Optional: player settings
          controls: true,
          autoplay: false,
          loop: false,
        }),
      ],
    }),
  ],
});
```

**Features:**
- Browse, search, and upload videos from the admin
- HLS and DASH adaptive streaming
- Automatic thumbnail generation
- Direct upload for large files

</TabItem>
</Tabs>

### Using Multiple Providers

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

```js title="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.

### Rendering Provider Media

Use the `Image` component to render media:

```astro title="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)

### MediaValue Type

Media fields store a `MediaValue` object containing provider information:

```ts
interface MediaValue {
  provider?: string;    // Provider ID, defaults to "local"
  id: string;           // Provider-specific ID
  src?: string;         // Direct URL (for local media or legacy data)
  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
  alt?: string;         // Alt text
  meta?: Record<string, unknown>; // Provider-specific metadata
}
```

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

## Next Steps

- [Working with Content](/guides/working-with-content/) - Use media in your content
- [Create a Blog](/guides/create-a-blog/) - Add images to blog posts
- [Querying Content](/guides/querying-content/) - Display media in templates
