Skip to content

Slugs and File Paths ​

An entry’s slug is its filename, and the file path is where that file is stored under the collection’s folder. Both can be customized with the options described on this page.

Entry Slugs ​

Sveltia CMS provides several ways to customize the slug (filename) of an entry in a collection.

Global Slug Options ​

The slug option defined at the top level of the configuration file applies to all collections, including the names of entry folders created or renamed in a collection with nested entries. It also applies to the names of uploaded asset files and new asset folders when filename slugification is enabled. The default settings are as follows:

yaml
slug:
  encoding: unicode
  clean_accents: false
  sanitize_replacement: '-'
  trim: true
  lowercase: true
  timezone: utc
toml
[slug]
encoding = "unicode"
clean_accents = false
sanitize_replacement = "-"
trim = true
lowercase = true
timezone = "utc"
json
{
  "slug": {
    "encoding": "unicode",
    "clean_accents": false,
    "sanitize_replacement": "-",
    "trim": true,
    "lowercase": true,
    "timezone": "utc"
  }
}
js
{
  slug: {
    encoding: "unicode",
    clean_accents: false,
    sanitize_replacement: "-",
    trim: true,
    lowercase: true,
    timezone: "utc",
  },
}

The available options are:

  • encoding: Specifies the encoding method for slugs. Supported values are unicode (default) and ascii.
    • unicode: Allows Unicode characters in slugs, preserving non-Latin scripts.
    • ascii: Sanitizes slugs to ASCII characters only. The allowed characters are 0-9, a-z, A-Z, hyphen (-), underscore (_) and tilde (~). Other characters are replaced with the value specified in the sanitize_replacement option.
  • clean_accents: A boolean value indicating whether to remove accents from characters in slugs. If enabled, accented characters are converted to their unaccented equivalents (e.g. é becomes e). Also, certain characters like German umlauts are transliterated to their ASCII equivalents (e.g. ß becomes ss). The default value is false.
  • sanitize_replacement: A string used to substitute invalid characters. The default value is a hyphen (-).
    • The replacement can’t contain a character that slugs can’t have, such as a slash (/) or a question mark (?), because it would end up in every slug.
    • The only exception is a plain space. It keeps the words of a filename apart for people and tools that read filenames rather than URLs, e.g. Sourdough focaccia.md for an Obsidian vault that links to entries by filename. Combine it with lowercase: false to keep the original casing. When the replacement contains a space, users can also type spaces when editing a slug.
    • Filenames with spaces aren’t URL-friendly. If you use the {{slug}} template tag in the preview_path option, make sure your site builds the same URL.
  • maxlength: An integer specifying the maximum number of characters in a generated slug. If the generated slug exceeds this length, it will be truncated. Any dots and sanitize_replacement characters left at the end are then removed, though the replacement characters are kept if trim is false. This is useful for CI/CD services or filesystems that impose filename length restrictions. The default value is undefined. Without it, a generated slug is limited to 200 bytes in UTF-8 instead, like Decap CMS, so the filename stays within the 255-byte limit of most filesystems and the Editorial Workflow branch name stays valid. The limit is lower if the collection name is very long. A Chinese, Japanese or Korean character takes 3 bytes, so a slug written in one of these languages can have about 66 characters. Existing entries are never renamed.
  • trim: A boolean value indicating whether to trim leading and trailing sanitize_replacement characters from the slug. The default value is true. Leading and trailing dots are always removed, whatever this option is set to: a leading dot would make a hidden file, and a Git branch name can’t end with a dot.
  • lowercase: A boolean value indicating whether to convert the slug to lowercase. The default value is true. Changing this to false will preserve the original casing of the title or identifier field.
  • timezone: A string specifying the timezone to use when generating date-based slugs with template tags like {{day}} and {{hour}}. The default value is utc. You can set this to local to use the local timezone of the user. It applies to the entry date taken from a DateTime field as well, unless the field stores a date only.

Deprecation Notice

The collection-level slug_length option has been deprecated in favor of the maxlength global slug option described above. The slug_length option will be removed in Sveltia CMS v1.0.0. If you are upgrading from an older version, update your configuration accordingly.

How Slugs Are Generated ​

By default, Sveltia CMS uses the title field as the slug (filename) for entries in a collection.

If a collection only has the Markdown body field, an entry slug will be generated from a header in the body, if one exists. This aims to support a typical VitePress or Docusaurus setup. If no title or header is found, a part of a random UUID will be used to ensure uniqueness.

Specifying an Identifier Field ​

If you want to use a different field as the entry identifier for generating slugs and filenames, you can specify it using the identifier_field option in the collection definition. This is useful when your entries have a unique identifier field other than title, such as name or id. For example, to use a product_name field as the identifier:

yaml
collections:
  - name: products
    label: Products
    folder: /content/products
    identifier_field: product_name
    fields:
      - { name: product_name, label: Product Name }
      - { name: description, label: Description, widget: richtext }
toml
[[collections]]
name = "products"
label = "Products"
folder = "/content/products"
identifier_field = "product_name"

[[collections.fields]]
name = "product_name"
label = "Product Name"

[[collections.fields]]
name = "description"
label = "Description"
widget = "richtext"
json
{
  "collections": [
    {
      "name": "products",
      "label": "Products",
      "folder": "/content/products",
      "identifier_field": "product_name",
      "fields": [
        { "name": "product_name", "label": "Product Name" },
        { "name": "description", "label": "Description", "widget": "richtext" }
      ]
    }
  ]
}
js
{
  collections: [
    {
      name: "products",
      label: "Products",
      folder: "/content/products",
      identifier_field: "product_name",
      fields: [
        { name: "product_name", label: "Product Name" },
        { name: "description", label: "Description", widget: "richtext" },
      ],
    },
  ],
}

Defining Entry Slugs ​

The slug option allows you to define a custom template for generating entry slugs using various template tags and field names.

For example, to create slugs that include the year and month of the entry date along with the entry slug, you can use the following configuration:

yaml
collections:
  - name: posts
    label: Blog Posts
    folder: /content/posts
    slug: '{{year}}-{{month}}-{{slug}}'
toml
[[collections]]
name = "posts"
label = "Blog Posts"
folder = "/content/posts"
slug = "{{year}}-{{month}}-{{slug}}"
json
{
  "collections": [
    {
      "name": "posts",
      "label": "Blog Posts",
      "folder": "/content/posts",
      "slug": "{{year}}-{{month}}-{{slug}}"
    }
  ]
}
js
{
  collections: [
    {
      name: "posts",
      label: "Blog Posts",
      folder: "/content/posts",
      slug: "{{year}}-{{month}}-{{slug}}",
    },
  ],
}

Any field name defined in the collection’s fields option can be used as a template tag in the slug option. For example, if you have a date field in the collection, you can use {{date}} in the slug option to include the date in the slug. For nested fields, use dot notation, e.g. {{author.name}}.

If a field’s name matches one of the predefined template tags, such as slug, year or uuid, you need to prefix it with fields., like {{fields.slug}}, to avoid confusion with the tag itself.

You can use string transformations with these template tags as well. For example, to create slugs that include the full date in YYYY-MM-DD format along with a custom slug field, you can use the following configuration:

yaml
collections:
  - name: posts
    label: Blog Posts
    folder: /content/posts
    slug: "{{date | date('YYYY-MM-DD')}}-{{fields.slug}}"
toml
[[collections]]
name = "posts"
label = "Blog Posts"
folder = "/content/posts"
slug = "{{date | date('YYYY-MM-DD')}}-{{fields.slug}}"
json
{
  "collections": [
    {
      "name": "posts",
      "label": "Blog Posts",
      "folder": "/content/posts",
      "slug": "{{date | date('YYYY-MM-DD')}}-{{fields.slug}}"
    }
  ]
}
js
{
  collections: [
    {
      name: "posts",
      label: "Blog Posts",
      folder: "/content/posts",
      slug: "{{date | date('YYYY-MM-DD')}}-{{fields.slug}}",
    },
  ],
}

TIP

The slug option value should not contain slashes (/). If you need to create a nested folder structure for entries, use the path option instead.

The slug option can also be an object, whose template option specifies the template, along with the options to make slugs editable.

Slug Template Tags ​

The following template tags are supported in the slug option:

  • {{slug}}: The slugified version of the entry’s title field (or the field defined with the identifier_field option).
  • {{year}}: 4-digit year of the entry date.
  • {{month}}: 2-digit month of the entry date.
  • {{day}}: 2-digit day of the entry date.
  • {{hour}}: 2-digit hour of the entry date.
  • {{minute}}: 2-digit minute of the entry date.
  • {{second}}: 2-digit second of the entry date.

The entry date is the value of the first DateTime field named date, publishDate or publish_date, so a backdated post is named after its own date. In a multilingual collection, the value in the default locale is used. If the collection has no such field, or it’s left empty, the date and time the entry is first saved are used instead. The slug is only generated when the entry is first saved, so changing the date later doesn’t rename the file.

By default, the entry date is based on the UTC timezone for compatibility with Netlify/Decap CMS. To use the local timezone of the user instead, set the timezone option to local in the global slug options. A DateTime field that stores a date only, without a time, gives that date as is in either case.

Additionally, the following unique identifier tags are available. These tags generate random values for each entry, ensuring uniqueness. This is particularly useful when the entry title may change later or when the title contains characters that are not suitable for filenames, such as non-Latin scripts.

  • {{uuid}}: A random UUID v4, e.g. 4fc0917c-8aea-4ad5-a476-392bdcf3b642.
  • {{uuid_short}}: The last 12 characters of a random UUID v4, e.g. 392bdcf3b642.
  • {{uuid_shorter}}: The first 8 characters of a random UUID v4, e.g. 4fc0917c.

Making Slugs Editable ​

Entry slugs are shown and edited in the Slug panel of the Content Editor’s sidebar. By default, users can edit the slug both while creating an entry and once it has been saved:

  • In a new entry, the panel shows the slug the entry will be saved with, which follows the slug template as the entry is edited. The pencil button lets users change it, and emptying it goes back to the template.
  • In a saved entry, the pencil button renames the entry. Renaming moves the entry’s file and rewrites every reference to it when the entry is saved. See Slug Panel for details.

To control this, set the slug option to an object instead of a template string. The object accepts the following options:

  • template: The slug template, which is what a string slug option specifies. The default is the identifier field, like {{title}}.
  • editable: Whether users can edit the slug. true (default) allows it both when an entry is created and once it has been saved, and false allows neither. An array picks the stages: [create] for new entries only, or [update] for saved entries only.
  • hint: A short description shown in the Slug panel, e.g. to explain what the slug is used for.
  • pattern: A regular expression the slug has to match, along with an error message, e.g. ['^[a-z-]+$', 'Use lowercase letters and hyphens only']. It works like the pattern field option and applies to a slug typed in by the user.
  • i18n: Whether each locale has a slug of its own. See Localizing Entry Slugs.
  • auto_increment: Whether to number new entries automatically instead of filling the template. See Numbering Slugs Automatically.

For example, to let users give a new blog post a slug of its own while keeping saved posts at their original URLs:

yaml
collections:
  - name: posts
    label: Blog Posts
    folder: /content/posts
    slug:
      template: '{{year}}-{{month}}-{{day}}-{{slug}}'
      editable: [create]
toml
[[collections]]
name = "posts"
label = "Blog Posts"
folder = "/content/posts"

[collections.slug]
template = "{{year}}-{{month}}-{{day}}-{{slug}}"
editable = ["create"]
json
{
  "collections": [
    {
      "name": "posts",
      "label": "Blog Posts",
      "folder": "/content/posts",
      "slug": {
        "template": "{{year}}-{{month}}-{{day}}-{{slug}}",
        "editable": ["create"]
      }
    }
  ]
}
js
{
  collections: [
    {
      name: "posts",
      label: "Blog Posts",
      folder: "/content/posts",
      slug: {
        template: "{{year}}-{{month}}-{{day}}-{{slug}}",
        editable: ["create"],
      },
    },
  ],
}

Locking entry URLs

Renaming an entry changes its URL on your live site, unless your framework redirects the old URL. If your editors shouldn’t change the slug once an entry has been published, set editable to [create], or to false to have the slug always generated from the template.

Having Users Type the Slug ​

When the slug can’t be derived from the entry’s content, e.g. when the filename is the only place a value is stored, you can have users type the slug themselves. Set the editable option to allow editing on creation, without a template. The slug is then required. The Slug panel opens by itself when a new entry is created, and the entry can’t be saved until a slug has been entered.

For example, a collection of languages whose filenames are BCP 47 language codes, such as en.json or pt-br.json, can guide users with the hint and pattern options:

yaml
collections:
  - name: languages
    label: Languages
    folder: /content/languages
    format: json
    slug:
      editable: true
      hint: A BCP 47 language code, like en or pt-br
      pattern: ['^[a-z]{2,3}(-[a-z0-9]+)*$', 'Must be a BCP 47 language code']
    fields:
      - { name: native_name, label: Native Name }
toml
[[collections]]
name = "languages"
label = "Languages"
folder = "/content/languages"
format = "json"

[collections.slug]
editable = true
hint = "A BCP 47 language code, like en or pt-br"
pattern = ["^[a-z]{2,3}(-[a-z0-9]+)*$", "Must be a BCP 47 language code"]

[[collections.fields]]
name = "native_name"
label = "Native Name"
json
{
  "collections": [
    {
      "name": "languages",
      "label": "Languages",
      "folder": "/content/languages",
      "format": "json",
      "slug": {
        "editable": true,
        "hint": "A BCP 47 language code, like en or pt-br",
        "pattern": ["^[a-z]{2,3}(-[a-z0-9]+)*$", "Must be a BCP 47 language code"]
      },
      "fields": [{ "name": "native_name", "label": "Native Name" }]
    }
  ]
}
js
{
  collections: [
    {
      name: "languages",
      label: "Languages",
      folder: "/content/languages",
      format: "json",
      slug: {
        editable: true,
        hint: "A BCP 47 language code, like en or pt-br",
        pattern: ["^[a-z]{2,3}(-[a-z0-9]+)*$", "Must be a BCP 47 language code"],
      },
      fields: [{ name: "native_name", label: "Native Name" }],
    },
  ],
}

Without the editable option, a collection without a template generates the slug from the identifier field as usual, and users can still change it in the Slug panel if they like.

A slug can also be given to a new entry with the _slug query parameter when linking to the Content Editor.

Numbering Slugs Automatically ​

For entries named after a sequential number, such as 1.md, 2.md and 3.md, set the auto_increment option to true. A new entry’s slug is then the integer after the highest one among the slugs of the collection’s entries, like an AUTO_INCREMENT column in a relational database. Unlike the Number field’s auto_increment option, it doesn’t need a field to hold the number.

  • The first entry is numbered 1. Only slugs made of digits count, so an entry named about doesn’t affect the numbering.
  • The unpublished entries with Editorial Workflow count too, as do the slugs of every locale.
  • The number is assigned when the entry is saved, so the Slug panel shows the next number while a new entry is being edited.
  • When the slug is editable on creation, users can type a slug of their own in the Slug panel, which takes over from the number. The pattern option applies to it.
  • With localized slugs, every locale shares the number unless a slug is typed in for it.
  • The template option is ignored, and a warning is shown if it’s set.

For example, a collection of articles numbered automatically, whose slugs can only be changed to another number:

yaml
collections:
  - name: articles
    label: Articles
    folder: /content/articles
    slug:
      auto_increment: true
      hint: A sequential number used as the article slug, e.g. 1
      pattern: ['^[0-9]+$', 'Must contain only numbers']
    fields:
      - { name: title, label: Title }
toml
[[collections]]
name = "articles"
label = "Articles"
folder = "/content/articles"

[collections.slug]
auto_increment = true
hint = "A sequential number used as the article slug, e.g. 1"
pattern = ["^[0-9]+$", "Must contain only numbers"]

[[collections.fields]]
name = "title"
label = "Title"
json
{
  "collections": [
    {
      "name": "articles",
      "label": "Articles",
      "folder": "/content/articles",
      "slug": {
        "auto_increment": true,
        "hint": "A sequential number used as the article slug, e.g. 1",
        "pattern": ["^[0-9]+$", "Must contain only numbers"]
      },
      "fields": [{ "name": "title", "label": "Title" }]
    }
  ]
}
js
{
  collections: [
    {
      name: "articles",
      label: "Articles",
      folder: "/content/articles",
      slug: {
        auto_increment: true,
        hint: "A sequential number used as the article slug, e.g. 1",
        pattern: ["^[0-9]+$", "Must contain only numbers"],
      },
      fields: [{ name: "title", label: "Title" }],
    },
  ],
}

WARNING

As with the Number field option, Sveltia CMS doesn’t keep a counter. The next number is worked out in the browser from the entries that exist when an entry is saved. This means:

  • If the entry with the highest number is deleted, the next new entry is given the same number.
  • Two users saving new entries at the same moment may get the same number.
  • Any slug made of digits also counts, so a date-like slug such as 20260114 makes the numbering continue from there.

The option is ignored in a collection that stores all its entries in a single file, whose entries have no slugs.

Deprecation Notice

The special {{fields._slug}} and {{fields._slug | localize}} slug template tags have been deprecated in favor of the editable and i18n options described above, and will be removed in Sveltia CMS v1.0.0. slug: '{{fields._slug}}' is equivalent to slug: { editable: true }, and slug: '{{fields._slug | localize}}' is equivalent to slug: { editable: true, i18n: true }.

Also, in earlier versions, setting the collection’s delete option to false prevented a saved entry’s slug from being edited. This is no longer the case; use editable: [create] or editable: false instead.

File Paths ​

Sveltia CMS provides a couple of options to customize the file paths of entries in a collection.

Using Subfolders ​

By default, Sveltia CMS saves entries directly under the specified folder using the slug as the filename. However, you can organize entries into subfolders using the path option.

Just like the slug option described above, the path option can use template tags to create dynamic folder structures. The slug template tags and string transformations can be used in the path option, along with any field names defined in the collection’s fields option. For nested fields, use dot notation, e.g. {{author.name}}.

For example, to save blog posts in subfolders based on the year and month of the entry date, you can use the following configuration:

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

With the above configuration, a blog post created on June 15, 2025, with the title “My First Post” will be saved at content/posts/2025/06/my-first-post.md.

Creating Page Bundles ​

You can create nested structures like Hugo’s page bundles, or Zola’s asset colocation, which uses the same index.md convention, using the path, media_folder and public_folder options together. For example, to create a leaf bundle for each blog post, you can use the following configuration:

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

With the above configuration, a blog post with the title “My First Post” will be saved at content/posts/my-first-post/index.md, and its media files will be stored in the same folder.

Since the collection has the path option, the media_folder: '' and public_folder: '' lines can be omitted when the global media_folder option is defined: in that case, the collection-level media_folder defaults to an empty string.

The same options can be combined with the nested option to build a tree of page bundles, where each entry has both its own media folder and its own child entries. See Nesting Page Bundles.

Constructing File Paths ​

A folder collection’s file path is determined by multiple factors: the i18n, folder, path, slug and extension options. The configuration can be complex, especially with i18n support, so let’s break it down.

  • The i18n global or collection option (optional)
    • It can be configured to add internationalization (i18n) support to your site.
    • The structure and omit_default_locale_from_file_path options affect the entry file path.
  • The folder collection option (required)
    • It specifies the folder where the collection entries are stored, relative to the repository’s root directory.
    • It can contain slashes to create a nested folder structure.
    • With i18n enabled, it can contain the {{locale}} placeholder as a folder name to say where the locale folder goes, which takes precedence over the structure i18n option. See Custom Locale Folder Placement.
  • The path collection option (optional)
    • It defaults to {{slug}}, which is the slug collection option value.
    • It can contain template tags.
    • It can also contain slashes to create a nested folder structure.
  • The slug collection option (optional)
    • It defaults to {{title}}, which is the entry’s title field value’s slugified version.
    • It can contain template tags but cannot contain slashes.
  • The extension collection option (optional)
    • It defaults to md.

Looking at the above options, the entry file path can be constructed as follows:

  • With i18n disabled:
    yaml
    /<folder>/<path>.<extension>
  • With the single_file i18n structure:
    yaml
    /<folder>/<path>.<extension>
  • With the multiple_files i18n structure:
    yaml
    /<folder>/<path>.<locale>.<extension>
    When the omit_default_locale_from_file_path i18n option is set to true, the path depends on the locale:
    yaml
    /<folder>/<path>.<extension> # default locale
    /<folder>/<path>.<locale>.<extension> # other locales
  • With the multiple_folders i18n structure:
    yaml
    /<folder>/<locale>/<path>.<extension>
    When the omit_default_locale_from_file_path i18n option is set to true, the path depends on the locale:
    yaml
    /<folder>/<path>.<extension> # default locale
    /<folder>/<locale>/<path>.<extension> # other locales
  • With the multiple_root_folders i18n structure:
    yaml
    /<locale>/<folder>/<path>.<extension>
    When the omit_default_locale_from_file_path i18n option is set to true, the path depends on the locale:
    yaml
    /<folder>/<path>.<extension> # default locale
    /<locale>/<folder>/<path>.<extension> # other locales
  • With the {{locale}} placeholder in the folder option, e.g. content/{{locale}}/posts, whatever the i18n structure:
    yaml
    /<folder with the locale filled in>/<path>.<extension>
    When the omit_default_locale_from_file_path i18n option is set to true, the path depends on the locale:
    yaml
    /<folder without the locale>/<path>.<extension> # default locale
    /<folder with the locale filled in>/<path>.<extension> # other locales