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:
- Browse: Click the upload container to pick files with the standard browser selector.
- Drag & drop: Drag files onto the Media page or a Media Picker field preview to open the uploader.
- Clipboard paste: Copy a file or screenshot, then press
Cmd+V/Ctrl+Von 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
altorcaptionare 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.urlRender 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.remotePatternsinnext.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/imageand add your storage domain toimage.domainsinnuxt.config.ts.