Media Library Plugin
A full-featured media library plugin for Svelar/SvelteKit with file uploads, image conversions, collections, disk storage (local or S3), and pre-built UI components for uploading, previewing, and browsing media.
Package: @beeblock/svelar-media
Install:
npx svelar plugin:install @beeblock/svelar-media
Imports:
// Plugin registration
import { SvelarMediaPlugin } from '@beeblock/svelar-media/server';
// Core API
import { Media, MediaService, MediaCollection, HasMedia } from '@beeblock/svelar-media';
// Conversions
import { ConversionBuilder, ConversionWorker, ImageConverter } from '@beeblock/svelar-media/conversions';
// Server-side (controllers, validation)
import { MediaController, MediaRequest } from '@beeblock/svelar-media/server';
// UI components
import { MediaUploader, MediaGallery, MediaPreview } from '@beeblock/svelar-media/ui';
// Types
import type { MediaRecord, MediaCollectionConfig, ConversionDefinition, DiskType, CustomProperties } from '@beeblock/svelar-media';
Quick Start
1. Register the Plugin
// src/lib/plugins.ts
import { SvelarMediaPlugin } from '@beeblock/svelar-media/server';
export const mediaPlugin = new SvelarMediaPlugin({
disk: 'local',
storagePath: 'storage/media',
maxFileSize: 10, // MB
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'],
conversions: true,
prefix: '/api',
});
2. Add Media to a Model
import { Model } from '@beeblock/svelar/database';
import { HasMedia } from '@beeblock/svelar-media';
class Post extends HasMedia(Model) {
static table = 'posts';
registerMediaCollections() {
this.addMediaCollection('images')
.acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp'])
.maxFileSize(5);
this.addMediaCollection('documents')
.acceptsMimeTypes(['application/pdf'])
.singleFile();
}
registerMediaConversions() {
this.addMediaConversion('thumb')
.width(200)
.height(200)
.sharpen()
.performOnCollections('images');
this.addMediaConversion('preview')
.width(800)
.nonQueued();
}
}
3. Upload Media
// In a server route or controller
const post = await Post.find(1);
// From a File object
await post.addMedia(file)
.usingName('hero-image')
.usingFileName('hero.jpg')
.withCustomProperties({ alt: 'Hero image' })
.toMediaCollection('images');
// From a URL
await post.addMediaFromUrl('https://example.com/photo.jpg')
.usingName('external-photo')
.toMediaCollection('images');
Configuration
The SvelarMediaPlugin constructor accepts the following options:
| Option | Type | Default | Description |
|---|---|---|---|
disk |
'local' | 's3' |
'local' |
Default storage disk |
storagePath |
string |
'storage/media' |
Local storage path |
maxFileSize |
number |
10 |
Max file size in MB |
allowedMimeTypes |
string[] |
['image/*', 'application/pdf'] |
Allowed MIME types |
conversions |
boolean |
true |
Enable image conversions |
prefix |
string |
'/api' |
API route prefix |
s3 |
S3Config |
undefined |
S3 configuration (bucket, region, etc.) |
Core API
HasMedia Mixin
The HasMedia mixin adds media management methods to any Svelar Model:
class Product extends HasMedia(Model) {
static table = 'products';
}
Instance methods:
| Method | Returns | Description |
|---|---|---|
addMediaCollection(name) |
MediaCollection |
Register a named media collection |
addMediaConversion(name) |
ConversionBuilder |
Register a named image conversion |
addMedia(file) |
MediaAdder |
Start adding a File/Buffer/ArrayBuffer |
addMediaFromUrl(url) |
MediaUrlAdder |
Start adding media from a URL |
getMedia(collection?) |
Promise<Media[]> |
Get all media, optionally filtered by collection |
getFirstMedia(collection?) |
Promise<Media | null> |
Get the first media item in a collection |
getFirstMediaUrl(collection?, conversion?) |
Promise<string> |
Get URL of the first media item |
clearMediaCollection(collection) |
Promise<void> |
Remove all media from a collection |
getMediaCount(collection?) |
Promise<number> |
Count media items |
MediaAdder (Fluent Builder)
Returned by model.addMedia(file):
await post.addMedia(file)
.usingName('my-photo')
.usingFileName('photo.jpg')
.withMimeType('image/jpeg')
.withCustomProperties({ caption: 'A beautiful sunset' })
.storingOn('s3')
.withOrder(1)
.toMediaCollection('images');
| Method | Description |
|---|---|
.usingName(name) |
Set the media name |
.usingFileName(fileName) |
Set the file name |
.withMimeType(mimeType) |
Set the MIME type |
.withCustomProperties(props) |
Attach custom key-value metadata |
.storingOn(disk) |
Choose the storage disk ('local' or 's3') |
.withOrder(order) |
Set the display order |
.toMediaCollection(collection) |
Save to a named collection (required, finalizes the upload) |
MediaUrlAdder
Returned by model.addMediaFromUrl(url):
await post.addMediaFromUrl('https://cdn.example.com/photo.jpg')
.usingName('cdn-photo')
.toMediaCollection('images');
Same fluent methods as MediaAdder.
MediaCollection
Configure collection constraints:
this.addMediaCollection('avatar')
.singleFile() // Only one file allowed
.acceptsMimeTypes(['image/jpeg', 'image/png']) // Restrict MIME types
.maxFileSize(2) // Max size in MB
.useDisk('local'); // Default disk for this collection
Media Class
Represents a stored media item:
const media = await post.getFirstMedia('images');
media.id; // number
media.name; // string
media.fileName; // string
media.mimeType; // string
media.size; // number (bytes)
media.collection; // string
media.disk; // 'local' | 's3'
media.customProperties; // Record<string, unknown>
media.orderColumn; // number
media.getUrl(); // Full URL to the original file
media.getUrl('thumb'); // Full URL to a conversion
media.getPath(); // File system path
media.toJSON(); // Serializable object
MediaService
Low-level service for direct media operations:
import { MediaService } from '@beeblock/svelar-media';
const service = new MediaService(config);
await service.store(file, modelType, modelId, collection, options);
await service.delete(mediaId);
await service.getForModel(modelType, modelId, collection);
Image Conversions
ConversionBuilder
Define conversions with a fluent API:
this.addMediaConversion('thumb')
.width(200)
.height(200)
.sharpen()
.quality(80)
.format('webp')
.nonQueued() // Process immediately (not via queue)
.performOnCollections('images'); // Only apply to specific collections
| Method | Description |
|---|---|
.width(px) |
Set output width |
.height(px) |
Set output height |
.quality(n) |
Set compression quality (1-100) |
.format(fmt) |
Convert to format ('webp', 'png', 'jpeg') |
.sharpen() |
Apply sharpening |
.nonQueued() |
Process synchronously instead of via queue |
.performOnCollections(...names) |
Restrict to specific collections |
ImageConverter
Performs the actual image transformation using Sharp:
import { ImageConverter } from '@beeblock/svelar-media/conversions';
const converter = new ImageConverter();
const outputBuffer = await converter.convert(inputBuffer, {
width: 200,
height: 200,
quality: 80,
format: 'webp',
sharpen: true,
});
ConversionWorker
Processes conversion jobs (used internally or via the scheduler):
import { ConversionWorker } from '@beeblock/svelar-media/conversions';
const worker = new ConversionWorker(config);
await worker.processMedia(mediaId);
Server-Side
MediaController
Handles media upload, retrieval, and deletion API routes:
// src/routes/api/media/+server.ts
import { MediaController } from '@beeblock/svelar-media/server';
const controller = new MediaController(mediaPluginConfig);
export const GET = async (event) => controller.index(event);
export const POST = async (event) => controller.store(event);
// src/routes/api/media/[id]/+server.ts
export const GET = async (event) => controller.show(event);
export const DELETE = async (event) => controller.destroy(event);
MediaRequest
Validates and parses incoming media upload requests:
import { MediaRequest } from '@beeblock/svelar-media/server';
const parsed = await MediaRequest.parse(event.request);
// parsed.file, parsed.collection, parsed.modelType, parsed.modelId, etc.
UI Components
MediaUploader
Drag-and-drop file uploader with progress:
<script lang="ts">
import { MediaUploader } from '@beeblock/svelar-media/ui';
function handleUpload(media: any) {
console.log('Uploaded:', media);
}
</script>
<MediaUploader
collection="images"
modelType="posts"
modelId={1}
accept="image/*"
multiple={true}
maxFiles={5}
maxSize={5}
onUpload={handleUpload}
/>
MediaGallery
Display a grid of media items with preview and actions:
<script lang="ts">
import { MediaGallery } from '@beeblock/svelar-media/ui';
let media = $state([]);
</script>
<MediaGallery
items={media}
columns={3}
selectable={true}
onSelect={(item) => console.log('Selected:', item)}
onDelete={(item) => console.log('Delete:', item)}
/>
MediaPreview
Single media item preview (image, PDF, etc.):
<script lang="ts">
import { MediaPreview } from '@beeblock/svelar-media/ui';
</script>
<MediaPreview
media={mediaItem}
conversion="thumb"
width={200}
height={200}
/>
Migration SQL
Run this migration to create the required media table:
CREATE TABLE IF NOT EXISTS media (
id INTEGER PRIMARY KEY AUTOINCREMENT,
model_type TEXT NOT NULL,
model_id INTEGER NOT NULL,
collection TEXT NOT NULL DEFAULT 'default',
name TEXT NOT NULL,
file_name TEXT NOT NULL,
mime_type TEXT,
disk TEXT NOT NULL DEFAULT 'local',
size INTEGER DEFAULT 0,
custom_properties TEXT DEFAULT '{}',
order_column INTEGER DEFAULT 0,
conversions TEXT DEFAULT '{}',
created_at TEXT,
updated_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_media_model ON media(model_type, model_id);
CREATE INDEX IF NOT EXISTS idx_media_collection ON media(model_type, model_id, collection);
Full Working Example
<!-- src/routes/posts/[id]/media/+page.svelte -->
<script lang="ts">
import { MediaUploader, MediaGallery } from '@beeblock/svelar-media/ui';
import { apiFetch } from '@beeblock/svelar/http';
interface Props {
data: { post: any; media: any[] };
}
let { data }: Props = $props();
let media = $state(data.media);
async function handleUpload(newMedia: any) {
media = [...media, newMedia];
}
async function handleDelete(item: any) {
await apiFetch(`/api/media/${item.id}`, { method: 'DELETE' });
media = media.filter((m) => m.id !== item.id);
}
</script>
<h1>Post Media</h1>
<MediaUploader
collection="images"
modelType="posts"
modelId={data.post.id}
accept="image/*"
multiple={true}
onUpload={handleUpload}
/>
<MediaGallery
items={media}
columns={4}
onDelete={handleDelete}
/>
// src/routes/api/media/+server.ts
import { MediaController } from '@beeblock/svelar-media/server';
const controller = new MediaController();
export const GET = async (event) => controller.index(event);
export const POST = async (event) => controller.store(event);
// src/routes/api/media/[id]/+server.ts
import { MediaController } from '@beeblock/svelar-media/server';
const controller = new MediaController();
export const GET = async (event) => controller.show(event);
export const DELETE = async (event) => controller.destroy(event);