Dyrecteddyrected
Guides

File Uploads

Accept file uploads, store them locally or in S3/Cloudinary, and display images.

File uploads in Dyrected come down to two decisions: where files are stored and which collection holds them. Once those are set, you get an Admin UI media manager and a programmatic upload API for free. This guide walks through both, starting with local storage for development.

Configure a storage adapter

A storage adapter tells Dyrected where uploaded files live. For local development, LocalStorageAdapter writes them to a folder in your project and serves them from a URL prefix:

// dyrected.config.ts  (same for both frameworks)
import { LocalStorageAdapter } from '@dyrected/storage-local'

export default defineConfig({
  storage: new LocalStorageAdapter({ 
    uploadDir: './public/uploads',
    staticUrlPrefix: '/uploads'
  }),
  // For production use S3 — see Configuring Storage guide
})

See Configuring Storage for S3, R2, and Cloudinary setup.

Define an upload collection

An upload collection is a normal collection with an upload config. That config controls which file types are accepted, the size limit, and any resized versions to generate. Add your own fields alongside it — here, an alt text field:

// dyrected.config.ts  (same for both frameworks)
{
  slug: 'media',
  upload: {
    allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
    maxFileSize: 5 * 1024 * 1024,
    imageSizes: [
      { name: 'thumbnail', width: 300, height: 300, fit: 'cover' },
      { name: 'card',      width: 800 },
    ],
  },
  fields: [
    { name: 'alt', label: 'Alt', type: 'text' },
  ],
}

Upload via the Admin UI

Open /admin and your media collection now has a media manager and file picker. (Any upload collection gets this — it uses that collection's own labels, so a collection you name sermons shows as "Sermons," not "Media.")

There are three ways to add a file:

  1. Browse: Click the upload container to pick files with the standard browser selector.
  2. Drag & drop: Drag files onto the Media page or a Media Picker field preview to open the uploader.
  3. Clipboard paste: Copy a file or screenshot, then press Cmd+V / Ctrl+V on the page to load it into the uploader.

Media library and picker

The Media Library page and the form-level Media Picker share one grid-based selector:

  • Single and multiple pickers use the same thumbnail/card treatment.
  • The attachment detail dialog scrolls and keeps large previews inside the viewport.
  • Metadata fields like alt or caption are editable from the media detail view.

Client-side image cropping

The Media Library and the form-level MediaPicker both support cropping in the browser:

  • Hover an image thumbnail to reveal a crop (scissors) button, shown for raster images like PNG, JPG, and WebP, but not SVGs or videos.
  • Click the crop button on an image (or in the attachment details pane on the main media page) to open the crop dialog.
  • Pick an aspect ratio preset (Free-form, 1:1, 4:3, 3:2, 16:9) and adjust the cropping handles.
  • Click Save to apply the crop. The cropped version is uploaded as a new media document and the selection automatically updates to point to the cropped version. All image sizes are generated server-side automatically after upload.

Upload programmatically

Beyond the Admin UI, you can upload from your own code — a custom uploader, a migration script, or a form. Build a FormData with the file and any metadata fields, then hand it to the collection's upload() method:

import { createClient } from '@dyrected/sdk'

const client = createClient({
  baseUrl: process.env.NEXT_PUBLIC_DYRECTED_URL!,
  apiKey: process.env.DYRECTED_API_KEY!,
})

const form = new FormData()
form.append('file', file)
form.append('alt', 'My awesome image')

const media = await client.collection('media').upload(form)
// Using raw fetch in a Client Component or Server Action
const form = new FormData()
form.append('file', file)
form.append('alt', file.name)

const res = await fetch(`${process.env.NEXT_PUBLIC_DYRECTED_URL}/api/collections/media`, {
  method: 'POST',
  headers: { 'x-api-key': process.env.DYRECTED_API_KEY! },
  body: form,
})
const media = await res.json()
// Using $fetch in a composable or component
const form = new FormData()
form.append('file', file)
form.append('alt', file.name)

const media = await $fetch('/api/collections/media', {
  method: 'POST',
  headers: { 'x-api-key': useRuntimeConfig().public.dyrectedApiKey },
  body: form,
})

Attach media to another collection

// dyrected.config.ts  (same for both frameworks)
{
  slug: 'posts',
  fields: [
    { name: 'title', label: 'Title',      type: 'text' },
    { name: 'coverImage', label: 'Cover Image', type: 'relationship', relationTo: 'media' },
  ],
}

Fetch with depth: 1 to inline the media document:

// docs[0].coverImage.url        → full storage URL
// docs[0].coverImage.sizes.thumbnail.url

Render images

import Image from 'next/image'

export function PostCard({ post }: { post: any }) {
  const cover = post.coverImage
  if (!cover) return null

  return (
    <Image
      src={cover.sizes?.card?.url ?? cover.url}
      alt={cover.alt ?? ''}
      width={800}
      height={450}
    />
  )
}

Add external storage domains to images.remotePatterns in next.config.ts.

<!-- components/PostCard.vue -->
<script setup lang="ts">
defineProps<{ post: any }>()
</script>

<template>
  <img
    v-if="post.coverImage"
    :src="post.coverImage.sizes?.card?.url ?? post.coverImage.url"
    :alt="post.coverImage.alt ?? ''"
    width="800"
    height="450"
  />
</template>

For Nuxt.js Image optimisation, use <NuxtImg> from @nuxt/image and add your storage domain to image.domains in nuxt.config.ts.

On this page