Skip to content

Internal Media Storage

The internal media storage in Sveltia CMS uses the repository’s file system to store and manage media assets such as images and files. It provides a simple and effective way to handle media uploads directly within the CMS interface. Some additional features, such as image optimization and file size limits, are also available to enhance the media management experience.

Considerations

The internal storage (Git repository) may not be suitable for a large number of media files or very large files, as it can lead to performance issues with Git operations. It’s particularly true for the GitHub backend that does not support Git LFS (Large File Storage) at this time. In such cases, consider using external storage.

Requirements

No special requirements are needed to use the internal media storage, as it works with any backend supported by Sveltia CMS.

Configuring Folder Paths

You can configure the folder paths for storing and accessing media files in the internal media storage at three levels: top-level, collection-level, and field-level. The settings at each level override the ones at the previous level.

Top-Level Configuration

Define the internal media storage settings in your config.yml file using the media_folder and public_folder options at the root level.

yaml
media_folder: /static/uploads
public_folder: /uploads
toml
media_folder = "/static/uploads"
public_folder = "/uploads"
json
{
  "media_folder": "/static/uploads",
  "public_folder": "/uploads"
}
js
{
  media_folder: "/static/uploads",
  public_folder: "/uploads",
}

Media Folder

The media_folder option specifies the folder in the repository where media files will be stored. Check your framework’s static assets handling to choose an appropriate folder.

Common static folder names

Here’s a quick reference for some popular frameworks:

Framework / SSGStatic Folder Name
Eleventy, GitBook, Jekyll/ (root)
Pelican/content
MkDocs, Docsify/docs
Astro, Next.js, Nuxt, Remix, UmiJS, VitePress/public
Hexo, Slate/source
mdBook/src
Docusaurus, Fresh, Gatsby, Hugo, SvelteKit, Zola/static
VuePress/.vuepress/public

If you’re unsure about your framework’s static files folder, please refer to its official documentation.

A few notes about this option:

  • It must be an absolute path relative to the root of the repository.
  • Although the leading slash can be omitted, it is recommended to include it for clarity.
  • To use the repository’s root folder, set this option to a slash (/), a period (.), or an empty string ('').

If you only want to use an external media storage provider and do not need the internal media storage, you can omit the media_folder option to disable it. Otherwise, this option is required.

Note that some of the stock photo providers may still require a media_folder to function properly because they don’t allow direct linking to their CDN URLs, requiring the images to be copied to the local repository instead.

Public Folder

The public_folder option defines the public URL path that corresponds to the media_folder. The leading slash is required in this option. If public_folder is not specified, it will default to the value of media_folder.

With the above configuration, if a media file is stored in /static/uploads/image.jpg and your site is hosted at https://example.com, the public URL to access the image would be https://example.com/uploads/image.jpg.

Breaking change from Netlify/Decap CMS

Sveltia CMS does not support absolute URLs in the public_folder option. Use relative paths starting with a slash (/) instead.

Collection-Level Configuration

You can override the internal media storage settings for each collection by specifying the media_folder and public_folder options in the collection configuration.

yaml
collections:
  - name: products
    label: Products
    folder: content/products
    media_folder: /static/uploads/products
    public_folder: /uploads/products
toml
[[collections]]
name = "products"
label = "Products"
folder = "content/products"
media_folder = "/static/uploads/products"
public_folder = "/uploads/products"
json
{
  "collections": [
    {
      "name": "products",
      "label": "Products",
      "folder": "content/products",
      "media_folder": "/static/uploads/products",
      "public_folder": "/uploads/products"
    }
  ]
}
js
{
  collections: [
    {
      name: "products",
      label: "Products",
      folder: "content/products",
      media_folder: "/static/uploads/products",
      public_folder: "/uploads/products",
    },
  ],
}

If public_folder is not specified, it will default to the value of the collection-level media_folder.

Absolute vs. Relative Paths

The collection-level and field-level media_folder option must be starting with a slash (/) to indicate an absolute path from the root of the repository, while a leading slash can be omitted in the top-level media_folder option.

If you use a relative path, Sveltia CMS will treat it as relative to the collection folder (and path, if defined). See the Using entry-relative folders section below for details.

We recommend using absolute paths for better clarity and to avoid confusion, unless you specifically want to organize media files within the content folders.

Note for Netlify/Decap CMS users

The absolute path setup is not documented in the official Netlify/Decap CMS documentation, but it has been supported at least since 2020. Sveltia CMS continues to support this behavior for compatibility and better usability.

Using Placeholders

The following placeholder variables can be used in the media_folder and public_folder options, in addition to slug template tags:

  • {{dirname}}: The name of the directory containing the entry file, relative to the collection folder.
  • {{filename}}: The entry file name without the extension. (Not the media file name.)
  • {{extension}}: The entry file extension. (Not the media file extension.)
  • {{media_folder}}: Refers to the top-level media_folder setting.
  • {{public_folder}}: Refers to the top-level public_folder setting.

The following example is the same as the previous one, but using placeholders:

yaml
collections:
  - name: products
    label: Products
    folder: content/products
    media_folder: '{{media_folder}}/products'
    public_folder: '{{public_folder}}/products'
toml
[[collections]]
name = "products"
label = "Products"
folder = "content/products"
media_folder = "{{media_folder}}/products"
public_folder = "{{public_folder}}/products"
json
{
  "collections": [
    {
      "name": "products",
      "label": "Products",
      "folder": "content/products",
      "media_folder": "{{media_folder}}/products",
      "public_folder": "{{public_folder}}/products"
    }
  ]
}
js
{
  collections: [
    {
      name: "products",
      label: "Products",
      folder: "content/products",
      media_folder: "{{media_folder}}/products",
      public_folder: "{{public_folder}}/products",
    },
  ],
}

Using Entry-Relative Folders

Some frameworks and static site generators support organizing content and media files together in the same folder. One example is Hugo’s page bundles, where each content entry can have its own folder containing the content file and associated media files.

Assets stored in entry-relative folders are only accessible by the associated entry and not available for other entries. Therefore, Sveltia CMS automatically deletes these assets when the associated entry is deleted. When you’re working with a local repository, the empty enclosing folder is also deleted.

To configure Sveltia CMS to use entry-relative paths for media files, set the media_folder and public_folder options to empty strings ('') in your collection configuration. This tells Sveltia CMS to look for media files in the same folder as the content files.

yaml
collections:
  - name: posts
    label: Blog Posts
    folder: /content/posts
    path: '{{slug}}/index'
    media_folder: ''
    public_folder: ''
    fields:
      - { name: title, label: Title }
      - { name: cover, label: Cover Image, widget: image }
      - { name: body, label: Body, widget: richtext }
toml
[[collections]]
name = "posts"
label = "Blog Posts"
folder = "/content/posts"
path = "{{slug}}/index"
media_folder = ""
public_folder = ""

[[collections.fields]]
name = "title"
label = "Title"

[[collections.fields]]
name = "cover"
label = "Cover Image"
widget = "image"

[[collections.fields]]
name = "body"
label = "Body"
widget = "richtext"
json
{
  "collections": [
    {
      "name": "posts",
      "label": "Blog Posts",
      "folder": "/content/posts",
      "path": "{{slug}}/index",
      "media_folder": "",
      "public_folder": "",
      "fields": [
        { "name": "title", "label": "Title" },
        { "name": "cover", "label": "Cover Image", "widget": "image" },
        { "name": "body", "label": "Body", "widget": "richtext" }
      ]
    }
  ]
}
js
{
  collections: [
    {
      name: "posts",
      label: "Blog Posts",
      folder: "/content/posts",
      path: "{{slug}}/index",
      media_folder: "",
      public_folder: "",
      fields: [
        { name: "title", label: "Title" },
        { name: "cover", label: "Cover Image", widget: "image" },
        { name: "body", label: "Body", widget: "richtext" },
      ],
    },
  ],
}

This configuration allows you to structure your content and media files like this:

.
└─ content/
   └─ posts/
      └─ my-first-post/
         ├─ index.md
         └─ image1.jpg

And the cover image field in the index.md file will omit the folder path when referencing the image:

yaml
---
title: My First Post
cover: image1.jpg
---
Content goes here...

If you want to organize media files in a subfolder within each entry folder, you can specify the subfolder name in the media_folder and public_folder options.

yaml
collections:
  - name: posts
    label: Blog Posts
    folder: /content/posts
    path: '{{slug}}/index'
    media_folder: 'images'
    public_folder: 'images'
toml
[[collections]]
name = "posts"
label = "Blog Posts"
folder = "/content/posts"
path = "{{slug}}/index"
media_folder = "images"
public_folder = "images"
json
{
  "collections": [
    {
      "name": "posts",
      "label": "Blog Posts",
      "folder": "/content/posts",
      "path": "{{slug}}/index",
      "media_folder": "images",
      "public_folder": "images"
    }
  ]
}
js
{
  collections: [
    {
      name: "posts",
      label: "Blog Posts",
      folder: "/content/posts",
      path: "{{slug}}/index",
      media_folder: "images",
      public_folder: "images",
    },
  ],
}

Then the folder structure would look like this:

.
└─ content/
   └─ posts/
      └─ my-first-post/
         ├─ index.md
         └─ images/
            └─ image1.jpg

And the cover image field in the index.md file would reference the image like this:

yaml
---
title: My First Post
cover: images/image1.jpg
---
Content goes here...

File-Level Configuration

Each file in a file collection can also have its own media folder settings by specifying the media_folder and public_folder options in the file configuration, which override both the top-level and collection-level settings.

yaml
collections:
  - name: pages
    label: Pages
    files:
      - name: about
        label: About Page
        file: content/pages/about.md
        media_folder: /static/uploads/about
        public_folder: /uploads/about
toml
[[collections]]
name = "pages"
label = "Pages"

[[collections.files]]
name = "about"
label = "About Page"
file = "content/pages/about.md"
media_folder = "/static/uploads/about"
public_folder = "/uploads/about"
json
{
  "collections": [
    {
      "name": "pages",
      "label": "Pages",
      "files": [
        {
          "name": "about",
          "label": "About Page",
          "file": "content/pages/about.md",
          "media_folder": "/static/uploads/about",
          "public_folder": "/uploads/about"
        }
      ]
    }
  ]
}
js
{
  collections: [
    {
      name: "pages",
      label: "Pages",
      files: [
        {
          name: "about",
          label: "About Page",
          file: "content/pages/about.md",
          media_folder: "/static/uploads/about",
          public_folder: "/uploads/about",
        },
      ],
    },
  ],
}

The same placeholder variables mentioned above can be used in field-level media_folder and public_folder options.

Field-Level Configuration

You can also configure media storage settings for individual File or Image fields within a collection. This allows you to specify different media folders for different fields, overriding both the top-level and collection-level settings.

yaml
fields:
  - name: thumbnail
    label: Thumbnail Image
    widget: image
    media_folder: /static/uploads/thumbnails
    public_folder: /uploads/thumbnails
toml
[[fields]]
name = "thumbnail"
label = "Thumbnail Image"
widget = "image"
media_folder = "/static/uploads/thumbnails"
public_folder = "/uploads/thumbnails"
json
{
  "fields": [
    {
      "name": "thumbnail",
      "label": "Thumbnail Image",
      "widget": "image",
      "media_folder": "/static/uploads/thumbnails",
      "public_folder": "/uploads/thumbnails"
    }
  ]
}
js
{
  fields: [
    {
      name: "thumbnail",
      label: "Thumbnail Image",
      widget: "image",
      media_folder: "/static/uploads/thumbnails",
      public_folder: "/uploads/thumbnails",
    },
  ],
}

The same placeholder variables mentioned above can be used in field-level media_folder and public_folder options.

Field-level media_folder and public_folder options can also be set to empty strings ('') or subfolder names to use entry-relative paths, just like in the collection-level configuration.

Additional Features

There are several additional features available in the internal media storage to enhance your media management experience.

Image Optimization

Ever wanted to prevent end-users from adding huge images to your repository? The built-in image optimizer in Sveltia CMS makes developers’ lives easier with a simple configuration like this:

yaml
media_libraries:
  default:
    config:
      transformations:
        raster_image: # original format
          format: webp # new format, only `webp` is supported
          quality: 85 # default: 85
          width: 2048 # default: original size
          height: 2048 # default: original size
        svg:
          optimize: true
toml
[media_libraries.default]
[media_libraries.default.config]
[media_libraries.default.config.transformations]
[media_libraries.default.config.transformations.raster_image]
format = "webp"
quality = 85
width = 2048
height = 2048

[media_libraries.default.config.transformations.svg]
optimize = true
json
{
  "media_libraries": {
    "default": {
      "config": {
        "transformations": {
          "raster_image": {
            "format": "webp",
            "quality": 85,
            "width": 2048,
            "height": 2048
          },
          "svg": {
            "optimize": true
          }
        }
      }
    }
  }
}
js
{
  media_libraries: {
    default: {
      config: {
        transformations: {
          raster_image: {
            format: "webp",
            quality: 85,
            width: 2048,
            height: 2048,
          },
          svg: {
            optimize: true,
          },
        },
      },
    },
  },
}

Then, whenever a user selects images to upload, those images are automatically optimized, all within the browser. Raster images such as JPEG and PNG are converted to WebP format and resized if necessary. SVG images are minified using the SVGO library.

In case you’re not aware, WebP offers better compression than conventional formats and is now widely supported across major browsers. So there is no reason not to use WebP on the web.

  • raster_image applies to any supported raster image format: avif, bmp, gif, jpeg, png and webp. If you like, you can use a specific format as key instead of raster_image.
  • The width and height options are the maximum width and height, respectively. If an image is larger than the specified dimension, it will be scaled down. Smaller images will not be resized.
  • File processing is a bit slow on Safari because native WebP encoding is not supported and the jSquash library is used instead.
  • AVIF conversion is not supported because no browser has native AVIF encoding support (Chromium won’t fix it) and the third-party library (and AVIF encoding in general) is very slow.
  • This feature is not intended for creating image variants in different formats and sizes. It should be done with a framework during the build process. Popular frameworks like Astro, Eleventy, Hugo, Next.js and SvelteKit have built-in image processing capabilities.
  • Exif metadata is stripped from raster images to reduce file size. If you want to keep it, upload the original files without optimization and use the framework to process them later.

Future Plans

We may add more transformation options in the future.

File Size Limits

If you want to restrict the maximum file size for uploads in the internal media storage, you can set the max_file_size option (in bytes) in the media_libraries configuration at the top level, collection level, or field level. The default value is Infinity, meaning there is no limit.

For example, to set a maximum file size of 1 MB for all uploads in the internal media storage, add the following to your config.yml:

yaml
media_libraries:
  default:
    config:
      max_file_size: 1024000
toml
[media_libraries.default]
[media_libraries.default.config]
max_file_size = 1024000
json
{
  "media_libraries": {
    "default": {
      "config": {
        "max_file_size": 1024000
      }
    }
  }
}
js
{
  media_libraries: {
    default: {
      config: {
        max_file_size: 1024000,
      },
    },
  },
}

Slugification of Filenames

Some frameworks and static site generators have restrictions on filenames, such as not allowing spaces or special characters. To ensure compatibility, you can enable filename slugification in the internal media storage by setting the slugify_filename option to true in the media_libraries configuration.

yaml
media_libraries:
  default:
    config:
      slugify_filename: true
toml
[media_libraries.default]
[media_libraries.default.config]
slugify_filename = true
json
{
  "media_libraries": {
    "default": {
      "config": {
        "slugify_filename": true
      }
    }
  }
}
js
{
  media_libraries: {
    default: {
      config: {
        slugify_filename: true,
      },
    },
  },
}

Once enabled, any uploaded file will have its filename converted to a URL-friendly format, according to the global slug options.

Accessing the Storage

There are two main ways to use the internal media storage in Sveltia CMS:

File and Image Fields

When editing content entries, you can use File and Image fields to upload and select media assets directly within the entry editor. Click the Browse button to open the media picker, where you can select existing assets or upload new ones. These fields also support drag-and-drop functionality for easy uploads.

Standalone Asset Library

You can access the Asset Library from the main navigation menu in the CMS interface. Here, you can view, upload, and manage all your media assets in one place. You can view assets in a grid or list format, search for specific files, and view asset details such as file size, dimensions and a list of entries using the asset.

Released under the MIT License.