Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 18 additions & 76 deletions docs/src/content/docs/guides/media-library.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Open the media library from the admin sidebar by clicking **Media**. The library

## Supported File Types

EmDash supports common web file types:
EmDash accepts these file types by default:

| Category | Extensions |
| --------- | ----------------------------------------------- |
Expand All @@ -45,8 +45,11 @@ EmDash supports common web file types:
| Video | `.mp4`, `.webm`, `.mov` |
| Audio | `.mp3`, `.wav`, `.ogg` |

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

<Aside type="caution">
Maximum file size depends on your storage configuration. The default limit is 10MB per file.
The default limit is 50 MB per file. Set
[`maxUploadSize`](/reference/configuration/#maxuploadsize) to change it.
</Aside>

## Storage Backends
Expand Down Expand Up @@ -134,48 +137,33 @@ Works with Cloudflare R2 (via S3 API), MinIO, and other S3-compatible services.

## How Uploads Work

EmDash uses signed URLs for secure uploads:
The admin uses the [upload target flow](/reference/rest-api/#upload-target-flow):

1. Client requests an upload URL from the API
1. The client requests an upload target, and EmDash creates a pending media item.

2. Server generates a signed URL with expiration
2. The client uploads the file to the returned target.

3. Client uploads directly to storage using the signed URL
3. The client confirms the upload.

4. Server records the file metadata in the database
4. EmDash validates the stored file and marks the media item as ready.

This approach keeps large files off your application server and enables direct uploads to cloud storage.
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.

<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
## Finding Media

### Search

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

### Filters
### Filter by type

Filter media by:

- **Type** - Images, Documents, Video, Audio
- **Date** - Upload date range
- **Folder** - Specific folder
Use the type filter to show images, documents, video, or audio files.

## Using Media in Content

Expand Down Expand Up @@ -267,55 +255,9 @@ EmDash installs an image endpoint that produces the resized variants on request.

## 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>
```

A successful upload returns the stored media item:

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

The following request lists media under a prefix:

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

### Delete Media

The following request deletes a stored file:

```bash
DELETE /_emdash/api/media/images/hero.jpg
Authorization: Bearer YOUR_API_TOKEN
```
Use the REST API to upload, list, update, and delete media from another application. The
[media endpoint reference](/reference/rest-api/#media-endpoints) documents both the direct
multipart upload and the upload target flow for local, R2, and S3-compatible storage.

## Media Providers

Expand Down
177 changes: 159 additions & 18 deletions docs/src/content/docs/reference/rest-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: REST API Reference
description: HTTP endpoints for content, media, and schema management.
---

import { Aside } from "@astrojs/starlight/components";
import { Aside, Steps } from "@astrojs/starlight/components";

EmDash exposes a REST API at `/_emdash/api/` for content management, media uploads, and schema operations.

Expand Down Expand Up @@ -319,29 +319,170 @@ provider-only assets.

### Create Media

```http
POST /_emdash/api/media
Content-Type: application/json
```
Media uploads require a token with the `media:write` scope. The default maximum file size is 50
MB. Set [`maxUploadSize`](/reference/configuration/#maxuploadsize) to change the limit.

#### Request Body
Use one of the following upload methods:

```json
{
"filename": "photo.jpg",
"mimeType": "image/jpeg",
"size": 102400,
"width": 1920,
"height": 1080,
"storageKey": "uploads/photo.jpg"
}
- Use a direct multipart upload when the client can send the file through EmDash.
- Use the upload target flow for direct uploads to S3-compatible storage. Local storage and native
R2 return an EmDash upload URL instead.

#### Direct multipart upload

Send the file in the `file` field of a multipart request:

```bash
curl --request POST \
--header "Authorization: Bearer $EMDASH_TOKEN" \
--form "file=@./photo.jpg;type=image/jpeg" \
https://example.com/_emdash/api/media
```

<Aside>
This endpoint records media metadata after upload. Use the upload endpoint or signed URLs to
upload the actual file.
`curl` adds the multipart boundary to the `Content-Type` header. Do not set that header manually.

The endpoint also accepts the following optional multipart fields:

| Field | Description |
| ----------- | ------------------------------------------------------------------ |
| `width` | Image width in pixels |
| `height` | Image height in pixels |
| `fieldId` | Field whose configured MIME type allowlist applies to the upload |
| `thumbnail` | Downscaled image used to generate a low-quality image placeholder |

A new upload returns `201 Created`. If the same file already exists, EmDash returns the existing
media item with `deduplicated: true`. Direct multipart uploads are ready immediately and do not
use the confirmation endpoint.

#### Upload target flow

The upload target flow keeps the media item in `pending` state until the final confirmation.
Pending media does not appear in the standard media list or media library.

<Steps>

1. Request an upload target

```http
POST /_emdash/api/media/upload-url
Authorization: Bearer <token>
Content-Type: application/json

{
"filename": "photo.jpg",
"contentType": "image/jpeg",
"size": 102400
}
```

`filename`, `contentType`, and `size` are required. The request also accepts the following
optional fields:

| Field | Description |
| ------------- | --------------------------------------------------------------------------- |
| `contentHash` | `sha1:` plus 40 lowercase hexadecimal characters, used to find a match |
| `fieldId` | Field whose configured MIME type allowlist applies to the upload |

The response contains the URL, method, and headers for the file upload. `uploadUrl` is an
absolute signed URL when the storage adapter supports one. Otherwise, it is a root-relative
EmDash endpoint.

```json
{
"success": true,
"data": {
"uploadUrl": "/_emdash/api/media/01M0AFKJS0RJM3WV69QHAY7YA1/upload",
"method": "PUT",
"headers": {
"Content-Type": "image/jpeg",
"X-EmDash-Request": "1"
},
"mediaId": "01M0AFKJS0RJM3WV69QHAY7YA1",
"storageKey": "01M0AFKJS0K2YF0222NP6ENYWX.jpg",
"expiresAt": "2026-08-18T14:05:09.920Z"
}
}
```

If `contentHash` matches an existing media item with the same MIME type and size, the response
contains `existing: true`, `mediaId`, `storageKey`, and `url` instead of an upload target. Use the
returned media item and stop. Do not upload or confirm the file.

2. Upload the file to `uploadUrl`

Start with the returned `method` and `headers`. Resolve a relative `uploadUrl` against the
EmDash site URL.

For a same-origin EmDash target, include the Bearer token in the upload request. For a signed URL
on another origin, send only the returned upload headers.

A same-origin upload returns the following response. A signed URL returns the storage provider's
response instead.

```json
{
"success": true,
"data": {
"uploaded": true,
"size": 102400
}
}
```

3. Confirm the upload

Confirming checks the stored file and changes the media item from `pending` to `ready`. `size`,
`width`, and `height` are optional, but EmDash validates them when supplied.

```http
POST /_emdash/api/media/01M0AFKJS0RJM3WV69QHAY7YA1/confirm
Authorization: Bearer <token>
Content-Type: application/json

{
"size": 102400,
"width": 1920,
"height": 1080
}
```

The response contains the ready media item:

```json
{
"success": true,
"data": {
"item": {
"id": "01M0AFKJS0RJM3WV69QHAY7YA1",
"filename": "photo.jpg",
"status": "ready",
"url": "/_emdash/api/media/file/01M0AFKJS0K2YF0222NP6ENYWX.jpg"
}
}
}
```

</Steps>

<Aside type="caution">
Do not send the EmDash Bearer token to an upload URL on another origin. A signed URL already
authorizes the storage upload. Sending the token would disclose it to the storage host.
</Aside>

#### Upload errors

| Status | Code | Cause |
| ------ | ---------------------- | ------------------------------------------------------------------------ |
| `400` | `NO_FILE` | The multipart request has no `file` field, or the upload body is missing |
| `400` | `INVALID_TYPE` | The MIME type is not allowed or does not match the pending media item |
| `400` | `VALIDATION_ERROR` | Upload metadata is missing, invalid, or exceeds the configured size limit |
| `400` | `FILE_NOT_FOUND` | Confirmation cannot find the uploaded object |
| `400` | `UPLOAD_SIZE_MISMATCH` | The declared, uploaded, and confirmed sizes do not match |
| `400` | `INVALID_STATE` | The media item is not pending |
| `404` | `NOT_FOUND` | The media item does not exist |
| `409` | `INVALID_STATE` | The pending media item changed during confirmation |
| `413` | `PAYLOAD_TOO_LARGE` | The direct or same-origin upload is too large |

### Update Media

```http
Expand Down
Loading