Media Management
Upload images, videos, and files; serve them in your frontend; and connect a production storage provider.
Airdraft has a built-in media library that handles file uploads, metadata, and serving. By default it stores files on local disk — great for development. For production, connect a cloud storage provider.
Uploading from the dashboard
- In the sidebar, click Media.
- Drag and drop files onto the upload zone, or click Upload to browse.
- The file is stored and a media key (e.g.
images/my-photo-abc1.jpg) is returned. - You can add alt text, a caption, and tags from the media detail view.
When editing an entry with a media field, click the field to open the media picker and choose an existing file or upload a new one.
Storage providers
Local disk (default — development only)
Out of the box, Airdraft stores uploads on the local file system. This is suitable for development but will lose files on serverless deployments or container restarts.
No configuration needed for local storage.
Warning: Do not use local storage in production. Files are not persisted across deployments on Vercel, Netlify, or any stateless platform.
Cloud storage (production)
For production, connect a cloud provider using the @airdraft/media-adapter-files-sdk package, which supports 30+ providers including S3, R2, GCS, Cloudinary, Vercel Blob, and Supabase Storage.
Install
npm install @airdraft/media-adapter-files-sdk files-sdk
Configure (self-hosted)
// airdraft.config.ts
import { defineConfig, LocalAdapter } from '@airdraft/core'
import { withMedia } from '@airdraft/plugin-media'
import { FilesSdkMediaAdapter } from '@airdraft/media-adapter-files-sdk'
import Files from 'files-sdk'
const adapter = new LocalAdapter({ root: './content' }) // content adapter (for sidecar metadata)
const files = new Files({ apiKey: process.env.FILES_SDK_API_KEY! })
export default defineConfig({
adapter,
plugins: [
withMedia({
adapter: new FilesSdkMediaAdapter({ files }), // ← media binary storage
storageAdapter: adapter, // ← content adapter for sidecar metadata
prefix: 'uploads',
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml'],
maxSize: 5 * 1024 * 1024,
}),
],
})
Key distinction:
adapteris the media binary storage (where files are saved).storageAdapteris the content adapter used to store sidecar metadata (alt text, dimensions) alongside entries.
Configure (Airdraft Cloud)
In the dashboard, go to Settings → Storage and enter your provider credentials. Airdraft Cloud supports R2, S3, GCS, Cloudinary, and Vercel Blob natively.
GitHub media storage
Store media as commits in a GitHub repository. Useful if you're already using GitHub for content sync and don't want a separate storage provider.
npm install @airdraft/media-adapter-github
import { GitHubMediaAdapter } from '@airdraft/media-adapter-github'
const mediaAdapter = new GitHubMediaAdapter({
appId: process.env.GITCMS_APP_ID!,
privateKey: process.env.GITCMS_PRIVATE_KEY!,
installationId: process.env.GITCMS_INSTALLATION_ID!,
repo: process.env.GITCMS_REPO!, // 'owner/repo'
branch: process.env.GITCMS_BRANCH ?? 'main',
})
// Default 10 MB upload limit per file
Or use withAutoMedia with storageBackend: 'github' to reuse your existing GitHub App env vars:
const media = await withAutoMedia({
storageAdapter: adapter,
storageBackend: 'github',
})
Using media in your frontend
Media fields store a key — a path like images/photo-abc1.jpg. Your frontend resolves this to a URL using the base URL of your storage provider.
With @airdraft/react-content (recommended)
@airdraft/plugin-media injects companion fields on every read: {field}_url (string) and {field}_media (full MediaItem object). The display components from @airdraft/react-content read these automatically:
import { ImageField, MediaField, MediaListField } from '@airdraft/react-content'
// Single image — reads entry.data.cover_url or entry.data.cover_media.url
<ImageField entry={entry} field="cover" />
// MIME-aware: renders <img>, <video>, <audio>, or <a> based on type
<MediaField entry={entry} field="attachment" />
// Multiple media files (field with multiple: true)
// Reads entry.data.gallery_medias
<MediaListField entry={entry} field="gallery" />
Manual URL resolution
Use @airdraft/content pure functions when you need the URL in logic rather than JSX:
import { resolveMediaUrl, resolveMediaUrls, resolveImageDimensions } from '@airdraft/content'
const url = resolveMediaUrl('cover', entry.data)
const dims = resolveImageDimensions('cover', entry.data)
// → { width: 1200, height: 630 }
// For next/image + OG metadata:
return { openGraph: { images: [{ url, ...dims }] } }
Signed URLs (private files)
If your storage is private (e.g. a private S3 bucket), fetch a short-lived signed URL:
const { url } = await cms.media.url('images/photo-abc1.jpg')
// → https://my-bucket.s3.amazonaws.com/images/photo-abc1.jpg?X-Amz-Signature=...
Uploading from your app (React)
Use the useMedia hook to upload files programmatically:
'use client'
import { useMedia } from '@airdraft/react'
export function UploadButton() {
const { uploadAll, uploading } = useMedia()
async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const files = Array.from(e.target.files ?? [])
const results = await uploadAll(files)
// results → [{ key, url, mimeType, ... }, ...]
}
return (
<label>
<input type="file" multiple onChange={handleChange} hidden />
<span>{uploading ? 'Uploading…' : 'Upload files'}</span>
</label>
)
}
uploadAll batches uploads 3 at a time to avoid overwhelming the server.
Listing and searching media
// HTTP client
const result = await client.media.list({ prefix: 'images/', limit: 20 })
// result.items → MediaItem[]
// result.cursor → pagination cursor for the next page
const nextPage = await client.media.list({ cursor: result.cursor })
Resources
@airdraft/plugin-mediaon npm —withMedia,withAutoMedia@airdraft/media-adapter-localon npm — local disk storage@airdraft/media-adapter-files-sdkon npm — S3/R2/GCS/Cloudinary/Vercel Blob@airdraft/media-adapter-githubon npm — GitHub repo-backed storage@airdraft/react-contenton npm —ImageField,MediaField,MediaListField@airdraft/contenton npm —resolveMediaUrl,resolveImageDimensions- GitHub — aevrHQ/airdraft