In Laioutr, media (images, videos) are used in sections and blocks—hero banners, product tiles, content blocks, and so on. Editors configure these in Cockpit (Studio) by picking assets from a media library. The library is not a single source: it is an abstraction over one or more backends. Each connected backend (e.g. Shopify Files, Shopware Media, your own DAM or CMS) is exposed as a media library provider. The editor selects a library (e.g. “Shopify”), browses or searches assets, and picks one; the chosen asset is stored as Laioutr’s canonical Media type (image or video with sources, alt, optional placeholder) and rendered on the frontend.
This gives you:
The abstraction lives in @laioutr-core/frontend-core and @laioutr-core/core-types. Your Nuxt app must use frontend-core so the media library API and registry are available. Apps like @laioutr-app/shopify and @laioutr-app/shopware ship with a built-in provider; you can add more by implementing and registering your own.
POST /api/laioutr/media-libraries). The response is the metadata of each provider (name, label, iconSrc), which Cockpit uses to show library tabs or a dropdown.POST /api/laioutr/media-list with library, offset, limit, and optional search and sort. The server looks up the provider by name and calls provider.list(args). The provider calls your backend (e.g. Shopify Files API, Shopware Media API), maps results to Laioutr’s Media type plus a previewUrl (and optional fileName), and returns items, total, offset, limit.POST /api/laioutr/media-upload (multipart form with library and files). The server validates the library, calls provider.upload({ files }), and returns the same shape as list (new items). The provider uploads to your backend and returns the created assets as ProviderStudioMediaItem.So: you implement a provider that talks to your asset system and speaks the Media type and ProviderStudioMedia contract; the rest (API routes, Cockpit UI, storage of media in props) is handled by the platform.
Assets are stored and passed around as Media from @laioutr-core/core-types/common. It is a discriminated union:
provider is the Nuxt Image provider name (e.g. shopify, raw); src is the URL or identifier your frontend uses to resolve the image or video. Your provider must return Media objects that match this shape so the frontend can render them correctly.
A MediaLibraryProvider extends MediaLibraryMeta and implements:
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Unique id (e.g. 'shopify', 'shopware', 'my-dam'). Used in API calls and in project context. |
| label | string | yes | Display name in Cockpit (e.g. “Shopify”, “Shopware”). |
| iconSrc | string | no | URL to an icon for the library (e.g. /app-shopify/shopify-logo.svg). |
| list | function | yes | list(args) => Promise |
| upload | function | no | upload(args) => Promise |
ProviderStudioMediaArgs (for list):
ProviderStudioMediaResponse:
UploadMediaArgs (for upload): files – array of ProviderStudioMediaFile (name, mimeType, size, filepath, optional url, getStream() to read the file). Your upload implementation should upload each file to your backend and return the same response shape as list (the newly created items).
Create a server-side module that builds a MediaLibraryProvider and registers it with defineMediaLibraryProvider. You can use @laioutr-core/core-types/media-library for types and defineMediaLibraryProvider from frontend-core (auto-imported when the app uses frontend-core).
Example: list-only provider (e.g. a read-only DAM):
// e.g. server/media-libraries/my-dam.ts
import { defineMediaLibraryProvider } from '#imports';
import type { ProviderStudioMediaItem } from '@laioutr-core/core-types/media-library';
import type { Media, MediaImage } from '@laioutr-core/core-types/common';
export default defineMediaLibraryProvider({
name: 'my-dam',
label: 'My DAM',
iconSrc: '/app-my-dam/logo.svg',
list: async ({ limit, offset, search, sort }) => {
const api = getMyDamClient();
const response = await api.getAssets({ limit, offset, q: search, sort });
const items: ProviderStudioMediaItem[] = response.items.map((asset) => ({
media: mapToMedia(asset),
previewUrl: asset.thumbnailUrl ?? asset.url,
fileName: asset.fileName,
}));
return {
items,
total: response.total,
offset,
limit,
};
},
});
function mapToMedia(asset: any): Media {
const image: MediaImage = {
type: 'image',
sources: [
{
provider: 'my-dam',
src: asset.id,
width: asset.width,
height: asset.height,
responsive: 'static',
},
],
alt: asset.alt ?? '',
};
return image;
}
Example: provider with upload (e.g. Shopify-style):
export default defineMediaLibraryProvider({
name: 'my-backend',
label: 'My Backend',
list: async (args) => { /* ... */ },
upload: async ({ files }) => {
const results = await Promise.all(
files.map(async (file) => {
const stream = file.getStream();
const uploaded = await myBackendClient.upload(stream, { name: file.name, mimeType: file.mimeType });
return {
media: mapToMedia(uploaded),
previewUrl: uploaded.previewUrl ?? '',
fileName: uploaded.name,
};
})
);
return { items: results, total: results.length, offset: 0, limit: results.length };
},
});
defineMediaLibraryProvider(provider) returns a function that, when run, registers the provider with the global mediaLibraryRegistry. So the default export of your file is that function; when Nitro loads it as a server plugin, it runs and registers the provider.
Your app (Nuxt module) must register the media library provider so the frontend app loads it. In registerLaioutrApp, pass the path to your provider file in mediaLibraryProviders. The kit will add it as a server plugin.
// In your app's module or registerLaioutrApp call
export default defineNuxtConfig({
laioutr: {
apps: [
{
name: 'my-app',
version: '1.0.0',
mediaLibraryProviders: [resolveRuntimeModule('./server/media-libraries/my-dam')],
// ... other app config
},
],
},
});
If you use registerLaioutrApp from @laioutr-core/kit (e.g. in a custom app package):
registerLaioutrApp({
name: 'my-app',
version: '1.0.0',
mediaLibraryProviders: [resolveRuntimeModule('./server/media-libraries/my-dam')],
// ...
});
After that, the provider is registered when the frontend app starts; media-libraries and media-list (and media-upload if you implemented upload) will use it.
Your list (and upload) must return Media that the frontend can render. For images:
'raw' for plain URLs), src (URL or id), and optionally width, height, responsive, focalPoint.For video, use type: 'video' and sources as MediaSourceVideo. If your backend has thumbnails, you can set preview on the video media.
Ensure your frontend has a Nuxt Image provider for the provider value you use (e.g. a custom provider that resolves src to your DAM URL), or use raw and pass a full URL as src.
The frontend-core module registers these handlers; you don’t implement them yourself:
| Endpoint | Purpose |
|---|---|
| POST /api/laioutr/media-libraries | Returns MediaLibraryMeta (name, label, iconSrc) for all registered providers. Cockpit uses this to show available libraries. |
| POST /api/laioutr/media-list | Body: ProviderStudioMediaArgs. Returns ProviderStudioMediaResponse. Cockpit uses this to show the asset list in the picker. |
| POST /api/laioutr/media-upload | Multipart form: library + files. Returns ProviderStudioMediaResponse. Called when the user uploads; returns 400 if the provider has no upload. |
Currencies
Laioutr’s currencies support lets customers switch between multiple currencies on their frontend. It includes a currency switcher component and optional informational links.
Multi-language Support
Laioutr’s multi-language support lets customers switch between multiple languages and regions on their frontend. It includes a language switcher component, a dark mode toggle, and optional informational links.