# Assets

# Assets

Madori includes a filesystem asset manager for uploading, organising, and browsing files. The public asset route reads the configured `assetsPath`; the `/assets/...` rewrite and `/api/public/assets/...` route work for custom roots as well as the default path. The Asset Manager provides a Control Panel interface and REST API.

The system supports images, documents, videos, audio, fonts, and archives, subject to upload safety checks. Active HTML and script extensions are rejected. Images get thumbnail previews; other files display type-appropriate icons.

---

## Configuration Reference

### Storage Configuration

Asset storage is configured in `madori.config.ts`:

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `assetsPath` | `string` | `./public/assets` | Directory where uploaded assets are stored |

```ts
// madori.config.ts
const config: MadoriConfigInput = {
  assetsPath: './public/assets',
}
```

Assets are served through Madori's safe file route at `/assets/...` (and `/api/public/assets/...`), including files uploaded after startup. Requests are containment-checked; metadata sidecars are hidden. Active file types and SVG downloads use attachment disposition and a sandbox CSP.

### Asset Field Configuration

Use the `asset` field type in blueprints to let editors pick files:

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `max_files` | `number` | `1` | Maximum number of files. `1` = single file mode; `0` = unlimited |
| `min_files` | `number` | `0` | Minimum number of selected files |

```yaml
- handle: hero_image
  field:
    type: asset
    display: Hero Image
    options:
      max_files: 1
```

### Asset Metadata Storage

Metadata is stored alongside files as `.meta.yaml`:

```yaml
# public/assets/images/hero.jpg.meta.yaml
alt: "Homepage hero banner"
```

| Property | Type | Description |
|----------|------|-------------|
| `alt` | `string` | Alt text for accessibility |

### API Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/assets` | List assets (optional `?directory=` param) |
| POST | `/api/assets/upload` | Upload a single file (`file` field) |
| POST | `/api/assets/upload-multiple` | Upload multiple files (`files` field) |
| PATCH | `/api/assets/{path}` | Update asset metadata (alt text, filename) |
| DELETE | `/api/assets/{path}` | Delete a single file |
| POST | `/api/assets/move` | Move/rename a file |
| POST | `/api/assets/bulk-move` | Move multiple files to a folder |
| POST | `/api/assets/bulk-delete` | Delete multiple files |
| POST | `/api/assets/directories` | Create a directory |
| POST | `/api/assets/directories/delete` | Delete a directory |
| POST | `/api/assets/directories/rename` | Rename a directory |

### Display Modes

Assets are displayed based on their MIME type:

| MIME Type Pattern | Display Mode | Visual |
|-------------------|--------------|--------|
| `image/*` | Thumbnail | Image preview |
| `application/pdf` | Icon | Document icon |
| `video/*` | Icon | Video icon |
| `audio/*` | Icon | Music icon |
| `application/zip` | Icon | Archive icon |
| Other | Icon | Generic file icon |

---

## Usage Examples

### Uploading Files via the Control Panel

1. Navigate to **Assets** in the CP sidebar
2. Drag and drop files onto the page, or click **Upload** and select files
3. Multiple files can be uploaded simultaneously — each shows individual progress
4. Uploaded files appear in the grid immediately upon completion

### Creating Folders

1. In the Asset Manager, click **Create Folder**
2. Enter a folder name (e.g. "Blog Images")
3. Navigate into the folder by clicking it
4. Use the breadcrumb trail to go back to parent folders

### Editing Asset Metadata

1. Click any asset in the grid to open its detail panel
2. Edit the **Alt text** field (important for image accessibility)
3. Edit the **Filename** if needed
4. Changes persist immediately without page reload

### Using Asset Fields in Blueprints

**Single image:**

```yaml
- handle: featured_image
  field:
    type: asset
    display: Featured Image
    required: true
    options:
      max_files: 1
```

**Multiple images (gallery):**

```yaml
- handle: gallery
  field:
    type: asset
    display: Photo Gallery
    options:
      max_files: 10
```

**Unlimited attachments:**

```yaml
- handle: attachments
  field:
    type: asset
    display: Attachments
```

### Programmatic Upload

```ts
const formData = new FormData()
formData.append('file', fileBlob, 'photo.jpg')

const response = await fetch('/api/assets/upload', {
  method: 'POST',
  body: formData,
})

const { data } = await response.json()
// data.path contains the asset reference returned by the API
```

### Updating Metadata via API

```ts
await fetch('/api/assets/images/hero.jpg', {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    alt: 'Updated alt text for hero image',
  }),
})
```

### Bulk Operations

Select multiple assets in the Control Panel (Shift/Cmd-click), then:

- **Move** — choose a destination folder and move all selected files
- **Delete** — confirm once to delete all selected files

Via API:

```ts
// Bulk move
await fetch('/api/assets/bulk-move', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    paths: ['/assets/photo1.jpg', '/assets/photo2.jpg'],
    destination: '/assets/archive/',
  }),
})

// Bulk delete
await fetch('/api/assets/bulk-delete', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    paths: ['/assets/old-file.pdf', '/assets/unused.png'],
  }),
})
```

---

## Common Patterns

### Folder Organisation Strategy

Organise assets by purpose or content type:

```
public/assets/
├── blog/           # Blog post images
├── pages/          # Page-specific assets
├── branding/       # Logos, favicons, brand assets
├── downloads/      # Downloadable documents
└── uploads/        # User-uploaded content
```

Or organise by date for high-volume sites:

```
public/assets/
├── 2026/
│   ├── 01/
│   ├── 02/
│   └── 03/
```

### Image Optimisation

Upload images at the dimensions you need. For responsive images, consider uploading multiple sizes:

```
public/assets/hero/
├── hero-1920.jpg    # Desktop
├── hero-1024.jpg    # Tablet
└── hero-640.jpg     # Mobile
```

Use Next.js Image component with the asset paths:

```tsx
import Image from 'next/image'

<Image src="/assets/hero/hero-1920.jpg" alt="Hero banner" width={1920} height={1080} />
```

### Asset Picker in Forms

When an editor uses an asset field, the picker modal allows:

- Browsing existing folders
- Searching by filename
- Uploading a new file directly into the picker
- Selecting and confirming the choice

The selected asset path is stored in the entry data.

### Alt Text Best Practices

Always add alt text to images for accessibility:

- Describe what the image shows, not what it is ("Team meeting in conference room" not "photo.jpg")
- Keep it concise — aim for under 125 characters
- Use empty alt text for purely decorative images
- Include relevant details that aren't available in surrounding text

### Backing Up Assets

Since assets live on the filesystem, back them up alongside your content:

```bash
# Rsync to backup location
rsync -avz public/assets/ /backups/assets/

# Or include in Git (for smaller sites)
git add public/assets/
git commit -m "Add new blog images"
```

For large sites with many assets, consider excluding them from Git and using a separate backup strategy.

### Referencing Assets in Templates

Assets are referenced by their public `/assets/` URL (the configured root may be outside `public/`):

```tsx
// File at public/assets/logo.svg → accessible at /assets/logo.svg
<img src="/assets/logo.svg" alt="Site logo" />

// File at public/assets/blog/post-image.jpg → accessible at /assets/blog/post-image.jpg
<img src="/assets/blog/post-image.jpg" alt="Blog illustration" />
```