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. 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 (-).
  • maxlength: An integer specifying the maximum length of the slug. If the generated slug exceeds this length, it will be truncated. This is useful for CI/CD services or filesystems that impose filename length restrictions. The default value is undefined, meaning there is no length limit.
  • trim: A boolean value indicating whether to trim leading and trailing sanitize_replacement characters from the slug. The default value is true.
  • 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.

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 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 creation 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.

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 creation date.
  • {{month}}: 2-digit month of the entry creation date.
  • {{day}}: 2-digit day of the entry creation date.
  • {{hour}}: 2-digit hour of the entry creation date.
  • {{minute}}: 2-digit minute of the entry creation date.
  • {{second}}: 2-digit second of the entry creation date.

By default, the entry creation date is based on the UTC timezone for backward 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.

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

By default, an entry’s slug is generated from the title field or the template defined in the slug option, and users never see it while writing. To have users choose the slug themselves instead, set the slug option to the special {{fields._slug}} tag.

With that option in place, a required Slug field appears above the other fields in the Edit Pane. It looks like a standard string field, but its value becomes the entry slug. The field starts out empty, so the entry can’t be saved until a slug has been entered, and slashes and whitespace are rejected.

Only while creating an entry

The Slug field is shown only while an entry is being created, including when an existing entry is duplicated. Once the entry has been saved, the field disappears.

A saved entry can still be renamed, but only with the Slug Editor in the 3-dot menu of the Content Editor. Renaming moves the entry’s file and rewrites every reference to it, so it’s deliberately kept out of the Edit Pane, where it could otherwise be changed by accident in the middle of routine editing.

In an i18n-enabled collection, {{fields._slug}} makes the slug editable in the default locale only, and the remaining locales show the same value as read-only. To let users enter a different slug for each locale, set the slug option to {{fields._slug | localize}} 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 creation, 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.

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.
  • 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
    /<locale>/<folder>/<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