Skip to content

Media

A media root holds the images and files editors upload. Add one to a workspace with Config.media() and set the workspace's mediaDir to the folder the files are written to. Link to uploads with the Image, File and Link fields.

import {Config} from 'alinea'

export const main = Config.workspace('My site', {
  source: 'content',
  mediaDir: 'public/media',
  roots: {
    pages: Config.root('Pages'),
    media: Config.media()
  }
})

Editors upload files in the media root of the dashboard and organize them in folders.

What's stored

Every upload is saved in two places:

  • The file itself goes to mediaDir, named after the original file plus the id of its entry: public/media/landscape.2V4c3kRS.jpg.

  • A media entry is stored with your content, in the media root: content/media/landscape.json. It holds the title, the file location, the extension and size, a content hash and, for images, the width and height, the average color, a thumbhash placeholder, the focal point and the alt text. Its metadata records who uploaded the file and who last changed it, and when.

Folders are entries too. Links store the id of the media entry, so moving or renaming a file in the dashboard doesn't break them.

Urls

Your site serves uploads at /admin/file/<folders>/<name>.<extension>, below the adminPath of your config. withAlinea forwards those requests to your Alinea handler, which serves the public copy of the file. When a file is moved or renamed, its old url keeps working. Set mediaUrl on the workspace to add a prefix, for example when two workspaces could have files with the same name.

So keep mediaDir inside your public folder, and don't change it once there are uploads: entries store file locations relative to it.

Options

Config.media(options) takes the same options as Config.root, with media-specific behaviour:

  • i18n: translate the alt text of files. Files themselves aren't localised: every locale uses the same file, and editors enter an alt text per locale. Image links return the alt text in the locale of the entry you queried. Add a fallback function to choose which locales to try when a translation is missing: fallback: locale => ['en'].

  • children: folders that always exist.

  • icon, orderChildrenBy and view, as for other roots.

The label of a media root is always "Media". A workspace can have several media roots; uploads that don't target a specific folder go to the first one.

File types

A media root holds media files: an upload becomes a MediaFile entry with an alt text. Use Config.file to define your own types of files, with fields of their own, and place them in any root, just like other types. A file type gets the data of the upload (location, extension, size, hash and, for images, the dimensions, preview and focus point) next to its fields.

schema/Library.ts
import {Config, Field} from 'alinea'

export const Photo = Config.file('Photo', {
  accept: 'image/*',
  fields: {
    alt: Field.localised(Field.text('Alt text')),
    credit: Field.text('Credit')
  }
})

export const Logo = Config.file('Logo', {accept: 'image/*'})
export const Download = Config.file('Download', {accept: '.pdf,.docx'})

export const Folder = Config.type('Folder', {
  contains: ['Folder', 'Photo', 'Logo', 'Download'],
  fields: {}
})

Config.root('Library', {
  contains: ['Folder', 'Photo', 'Download'],
  i18n: {locales: ['en', 'nl'], translate: 'fields'}
})
  • accept lists the files of the type, like the accept attribute of a file input: extensions (.pdf), media types (application/pdf) and groups of them (image/*). Without it the type takes every file.

  • Editors can upload wherever the folder they are in, or the root at the top level, contains a file type. An upload becomes the first of those types that accepts the file. When several accept it, the upload dialog asks which one to use, and files none of them accepts are left out.

  • Image and file fields pick files of every file type. Queries select the data of a file with ImageLink and FileLink, and your own fields with the fields of the file type.

  • File types follow the rules of media files: uploading publishes them, they have no drafts, and deleting an entry deletes its file.

  • In a root that translates fields, Field.localised fields of files hold a value per locale, as the alt text of media files does.

The built-in MediaFile type takes tabs and fields of your own as well: define it in the schema with Config.mediaFile. Files uploaded before have them too, and queries naming MediaFile find it.

cms.tsx
import {Config, Field} from 'alinea'
import {createCMS} from 'alinea/next'

export const cms = createCMS({
  schema: {
    ...schema,
    MediaFile: Config.mediaFile({
      tabs: {
        analytics: Field.tab('Analytics', {view: '@/views/MediaAnalytics'})
      }
    })
  },
  workspaces
})

Limits and production

  • Set maxUploadSize in your config (in bytes) to refuse larger uploads.

  • Committing uploads to Git works well for images and documents, less so for large videos. When you deploy with your own backend, uploads can go to S3-compatible storage instead, see Self-hosted.