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 four levels: top-level, collection-level, file-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.
media_folder: /public/uploads
public_folder: /uploadsmedia_folder = "/public/uploads"
public_folder = "/uploads"{
"media_folder": "/public/uploads",
"public_folder": "/uploads"
}{
media_folder: "/public/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 various frameworks:
| Framework / SSG | Static Folder Name |
|---|---|
| Eleventy, Jekyll, Lume | / (root) |
| Pelican, Quartz | /content |
| Docsify, MkDocs | /docs |
| Rspress, VitePress | /docs/public¹ |
| Nikola | /files |
| Angular, Astro, Fumadocs, HonoX, Next.js, Nextra, Nuxt, Qwik, React Router (Remix), SolidStart, TanStack Start, UmiJS, Vite | /public |
| Hexo, Middleman | /source |
| Bridgetown, mdBook | /src |
| Analog | /src/public |
| Docusaurus, Fresh, Gatsby, Gridsome, Hugo, Nuxt 2, SvelteKit, Zola | /static |
| VuePress | /docs/.vuepress/public¹ |
¹ The public folder is placed under the source folder, which is often named docs. If your site’s Markdown files are in the root folder, use /public (VitePress) or /.vuepress/public (VuePress) instead.
Some frameworks process HTML or YAML files in these folders instead of copying them as is, so the admin folder needs extra configuration:
- Eleventy: Add
eleventyConfig.addPassthroughCopy('admin')to the configuration file. Otherwise,index.htmlis rendered as a template andconfig.ymlis not copied at all. - Hexo: Add
skip_render: admin/**to_config.ymlso the theme layout is not applied to the admin page. - Lume: Add
site.copy('admin')to_config.ts. - Middleman: Add
page '/admin/*', layout: falsetoconfig.rbso the site layout is not applied to the admin page. - Pelican: Add
'admin'toSTATIC_PATHS, and toARTICLE_EXCLUDESandPAGE_EXCLUDES, so the admin page is copied instead of being read as an article. - Sphinx: Put the admin folder in a folder listed in the
html_extra_pathoption, such as_extra, so it’s copied to the root of the output folder.
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 ('').
Public Folder
The public_folder option defines the public URL path that corresponds to the media_folder. The leading slash is added automatically if omitted, but it is recommended to include it for clarity. If public_folder is not specified, it will default to the value of media_folder with a leading slash.
With the above configuration, if a media file is stored in /public/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.
Disabling Internal Media Storage
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.
You can also disable the internal media storage for specific fields.
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.
collections:
- name: products
label: Products
folder: content/products
media_folder: /public/uploads/products
public_folder: /uploads/products[[collections]]
name = "products"
label = "Products"
folder = "content/products"
media_folder = "/public/uploads/products"
public_folder = "/uploads/products"{
"collections": [
{
"name": "products",
"label": "Products",
"folder": "content/products",
"media_folder": "/public/uploads/products",
"public_folder": "/uploads/products"
}
]
}{
collections: [
{
name: "products",
label: "Products",
folder: "content/products",
media_folder: "/public/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 start 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 collectionfolder.{{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-levelmedia_foldersetting.{{public_folder}}: Refers to the top-levelpublic_foldersetting.
The following example is the same as the previous one, but using placeholders:
collections:
- name: products
label: Products
folder: content/products
media_folder: '{{media_folder}}/products'
public_folder: '{{public_folder}}/products'[[collections]]
name = "products"
label = "Products"
folder = "content/products"
media_folder = "{{media_folder}}/products"
public_folder = "{{public_folder}}/products"{
"collections": [
{
"name": "products",
"label": "Products",
"folder": "content/products",
"media_folder": "{{media_folder}}/products",
"public_folder": "{{public_folder}}/products"
}
]
}{
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.
In a nested collection, the folders below an entry hold entries of their own, so the media in them is left alone: deleting a page never touches the media of the pages beneath it.
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.
This only makes each entry’s media its own if the entry has a folder of its own to keep it in. The path option gives it one, as in the example below, and so does the subfolders mode of a nested collection. Without either, entries are files sharing one folder, so a relative media_folder resolves to the collection folder and the media is shared as well.
TIP
When a collection has the path option but no media_folder option of its own, its media_folder defaults to an empty string, and public_folder follows it, so the entry-relative setup below is implied. This applies when the global media_folder option is defined. Set the collection-level media_folder explicitly to store the media elsewhere.
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 }[[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"{
"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" }
]
}
]
}{
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.jpgAnd the cover image field in the index.md file will omit the folder path when referencing the image:
---
title: My First Post
cover: image1.jpg
---
Content goes here...With i18n enabled, an entry-relative file is stored once, not once per locale.
The multiple_files and single_file structures keep every locale’s content in the folder holding the entry, so the media already sits beside all of them. This is how Hugo’s page bundles are usually organized:
content/posts/my-first-post/index.en.md # cover: image1.jpg
content/posts/my-first-post/index.de.md # cover: image1.jpg
content/posts/my-first-post/image1.jpgThe multiple_folders and multiple_root_folders structures give each locale a folder of its own. The file is saved in the default locale’s folder, and every locale’s entry refers to it by the same relative path, so the media is never duplicated:
content/posts/en/my-first-post/index.md # cover: image1.jpg
content/posts/de/my-first-post/index.md # cover: image1.jpg
content/posts/en/my-first-post/image1.jpgSveltia CMS resolves that path across locales, so the image appears in the content editor whichever locale the user is editing. Your framework may not: taken literally from the German entry’s own folder, image1.jpg points at a file that isn’t there. Hugo resolves it for page bundles, where a bundle inherits the resources of its translated pages, as long as the two are linked as translations. If your framework has no such mechanism, prefer the multiple_files structure, which keeps the media next to every locale’s file.
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.
collections:
- name: posts
label: Blog Posts
folder: /content/posts
path: '{{slug}}/index'
media_folder: 'images'
public_folder: 'images'[[collections]]
name = "posts"
label = "Blog Posts"
folder = "/content/posts"
path = "{{slug}}/index"
media_folder = "images"
public_folder = "images"{
"collections": [
{
"name": "posts",
"label": "Blog Posts",
"folder": "/content/posts",
"path": "{{slug}}/index",
"media_folder": "images",
"public_folder": "images"
}
]
}{
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.jpgAnd the cover image field in the index.md file would reference the image like this:
---
title: My First Post
cover: images/image1.jpg
---
Content goes here...Because these assets belong to the entry, they follow it: renaming an entry, or filing it under a different parent in a nested collection, moves the whole folder in the same commit.
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.
collections:
- name: pages
label: Pages
files:
- name: about
label: About Page
file: content/pages/about.md
media_folder: /public/uploads/about
public_folder: /uploads/about[[collections]]
name = "pages"
label = "Pages"
[[collections.files]]
name = "about"
label = "About Page"
file = "content/pages/about.md"
media_folder = "/public/uploads/about"
public_folder = "/uploads/about"{
"collections": [
{
"name": "pages",
"label": "Pages",
"files": [
{
"name": "about",
"label": "About Page",
"file": "content/pages/about.md",
"media_folder": "/public/uploads/about",
"public_folder": "/uploads/about"
}
]
}
]
}{
collections: [
{
name: "pages",
label: "Pages",
files: [
{
name: "about",
label: "About Page",
file: "content/pages/about.md",
media_folder: "/public/uploads/about",
public_folder: "/uploads/about",
},
],
},
],
}The same placeholder variables mentioned above can be used in file-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.
fields:
- name: thumbnail
label: Thumbnail Image
widget: image
media_folder: /public/uploads/thumbnails
public_folder: /uploads/thumbnails[[fields]]
name = "thumbnail"
label = "Thumbnail Image"
widget = "image"
media_folder = "/public/uploads/thumbnails"
public_folder = "/uploads/thumbnails"{
"fields": [
{
"name": "thumbnail",
"label": "Thumbnail Image",
"widget": "image",
"media_folder": "/public/uploads/thumbnails",
"public_folder": "/uploads/thumbnails"
}
]
}{
fields: [
{
name: "thumbnail",
label: "Thumbnail Image",
widget: "image",
media_folder: "/public/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.
Disabling Internal Media Storage for a Field
If you have enabled an external media storage provider and want to disable the internal media storage for a specific field, you can add the media_libraries option with the default library set to false in the field configuration. This will prevent the media picker from showing the internal media library and only allow selecting from the external provider.
fields:
- name: cover
label: Cover Image
widget: image
media_libraries:
default: false[[fields]]
name = "cover"
label = "Cover Image"
widget = "image"
[fields.media_libraries]
default = false{
"fields": [
{
"name": "cover",
"label": "Cover Image",
"widget": "image",
"media_libraries": {
"default": false
}
}
]
}{
fields: [
{
name: "cover",
label: "Cover Image",
widget: "image",
media_libraries: {
default: false,
},
},
],
}Asset Collections
In addition to the global media folder and collection-specific media folders, Sveltia CMS supports defining separate asset collections that can be used across multiple content collections. This allows you to organize your media assets in a more structured way and reuse them in different contexts.
To define an asset collection, add a new entry to the asset_collections array in your config.yml file. Each asset collection has the following properties:
name: Unique identifier for the asset collection. Required and must be unique across all collections.label: Human-readable name for the asset collection, displayed in the media picker. If omitted, it defaults to the value ofname.icon: Optional icon for the asset collection, which can be a string representing a Material Symbols icon name.media_folder: The folder in the repository where media files for this collection will be stored. Required and must be an absolute path relative to the root of the repository. A leading slash can be omitted, but it is recommended to include it for clarity.public_folder: The public URL path that corresponds to themedia_folder. If omitted, it defaults to the value ofmedia_folder.readonly: Set totrueto make the asset collection read-only, so assets can’t be uploaded to, changed in or deleted from it. Optional. See Making Content Read-Only.
Configure your asset collections like this:
asset_collections:
- name: images
label: Images
icon: image
media_folder: /public/uploads/images
public_folder: /uploads/images
- name: logos
label: Logos
icon: brand_family
media_folder: /public/uploads/logos
public_folder: /uploads/logos
- name: documents
label: Documents
icon: description
media_folder: /public/uploads/documents
public_folder: /uploads/documents[[asset_collections]]
name = "images"
label = "Images"
icon = "image"
media_folder = "/public/uploads/images"
public_folder = "/uploads/images"
[[asset_collections]]
name = "logos"
label = "Logos"
icon = "brand_family"
media_folder = "/public/uploads/logos"
public_folder = "/uploads/logos"
[[asset_collections]]
name = "documents"
label = "Documents"
icon = "description"
media_folder = "/public/uploads/documents"
public_folder = "/uploads/documents"{
"asset_collections": [
{
"name": "images",
"label": "Images",
"icon": "image",
"media_folder": "/public/uploads/images",
"public_folder": "/uploads/images"
},
{
"name": "logos",
"label": "Logos",
"icon": "brand_family",
"media_folder": "/public/uploads/logos",
"public_folder": "/uploads/logos"
},
{
"name": "documents",
"label": "Documents",
"icon": "description",
"media_folder": "/public/uploads/documents",
"public_folder": "/uploads/documents"
}
]
}{
asset_collections: [
{
name: "images",
label: "Images",
icon: "image",
media_folder: "/public/uploads/images",
public_folder: "/uploads/images",
},
{
name: "logos",
label: "Logos",
icon: "brand_family",
media_folder: "/public/uploads/logos",
public_folder: "/uploads/logos",
},
{
name: "documents",
label: "Documents",
icon: "description",
media_folder: "/public/uploads/documents",
public_folder: "/uploads/documents",
},
],
}Additional Features
For backward compatibility, the additional media storage features can be configured specifically for the internal media storage. This configuration goes in the media_libraries option, under the default → config key, which applies only to the internal media storage provider, as opposed to the all key that also applies to files uploaded to external storage providers. Options set here override the same options under the all key at the same level.
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[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{
"media_libraries": {
"default": {
"config": {
"transformations": {
"raster_image": {
"format": "webp",
"quality": 85,
"width": 2048,
"height": 2048
},
"svg": {
"optimize": true
}
}
}
}
}
}{
media_libraries: {
default: {
config: {
transformations: {
raster_image: {
format: "webp",
quality: 85,
width: 2048,
height: 2048,
},
svg: {
optimize: true,
},
},
},
},
},
}media_libraries:
default:
config:
max_file_size: 1024000[media_libraries.default]
[media_libraries.default.config]
max_file_size = 1024000{
"media_libraries": {
"default": {
"config": {
"max_file_size": 1024000
}
}
}
}{
media_libraries: {
default: {
config: {
max_file_size: 1024000,
},
},
},
}media_libraries:
default:
config:
slugify_filename: true[media_libraries.default]
[media_libraries.default.config]
slugify_filename = true{
"media_libraries": {
"default": {
"config": {
"slugify_filename": true
}
}
}
}{
media_libraries: {
default: {
config: {
slugify_filename: true,
},
},
},
}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, users can use File and Image fields to upload and select media assets directly within the entry editor. Clicking the Browse button opens the media picker, where users can select existing assets or upload new ones, browsing the subfolders of the media folder and creating new ones as needed. These fields also support drag-and-drop functionality for easy uploads.
Standalone Asset Library
The Asset Library is accessible from the main navigation menu in the CMS interface. Here, users can view, upload, and manage all media assets in one place. Assets can be viewed in a grid or list format, and users can browse and organize them in subfolders, search for specific files and view asset details such as file size, dimensions and a list of entries using the asset.