How Keystatic organises your content

Keystatic has two concepts or structures to organise data: collections and singletons.

Those are defined in the Keystatic configuration.

You get a lot of control and flexibility with where your content gets generated, both at the collection or singleton level, and at the field level for certain field types, like images.

Content path

You can define where Keystatic should store collection entries and singletons via the path property in the collection/singleton top-level options.

// Keystatic config
export default config({
  collections: {
    posts: collection({
      label: 'Posts',
      path: 'content/posts/*/',
      // ...
    })
  },
  singletons: {
    settings: singleton({
      label: 'Settings',
      path: 'content/posts/',
      // ...
   })
  }
})

The optional trailing slash / on that path has an impact on the content structure - read below for more details on collection paths and singleton paths.

Path wildcard for collections

The path property for collections must contain a * wildcard.

It represents the slug of an entry.

This wildcard gives you flexibility and control over your where your content is being output.

Nested folder output example

👉

path: 'packages/design-system/*/docs/'

Imagine a Design System inside a monorepo:

root
├── packages
  ├── design-system
    ├── button
      └── src
    ├── dropdown
      └── src
└── apps
  └── docs(keystatic)

Your Keystatic site lives in apps/docs, but you want your documentation entries in packages/design-system/{component-name}/docs/, to collocate them with each component.

The following path will let you do exactly that:

path: 'packages/design-system/*/docs/'

Nested slug example

👉

path: 'content/posts/**'

There may be situations where you need the slug of an entry to be following a multi-folder structure.

Say you want the same collection to support this following tree structure:

content
├── posts
  ├── en  
      └── post-1.mdoc
  ├── fr  
      └── post-1.mdoc

You can enable this by using the ** wildcard in your path:

path: 'content/posts/**'

Since Keystatic slugs can contain / characters, you construct a multi-folder structure with slug field values like: en/post-1 and fr/post-1.


Collections

The default path value, if not specified, will be {collection-name}/*/.

Collection paths ending with a trailing slash /

If the path ends with a trailing slash /, each entry will be created in its own directory named after the slug:

collection-name
└── slug
    ├── index.yaml
    └── other.mdoc

Say you create two entries in the posts collection, where the path is set to 'content/posts/*/'.

Since there is a trailing slash in the path, the generated output will look like so:

content
└── posts
    ├── my-first-post
        ├── index.yaml
        ├── other.mdoc
    └── my-second-post
        ├── index.yaml
        └── other.mdoc

Collection paths ending without a trailing slash

If the path does not end with a trailing slash, entries' index files will be created immediately inside the collection directory:

collection-name
├── slug.yaml
└── slug
    └── other.mdoc

Say you create two entries in the posts collection, where the path is set to 'content/posts/*'.

Since there is no trailing slash in the path, the generated output will look like so:

content
└── posts
    ├── my-first-post.yaml
    └── my-first-post
        └── other.mdoc
    ├── my-second-post.yaml
    └── my-second-post
        └── other.mdoc

Singletons

The path property for singletons does not contain a * wildcard.

If not specified, the default path value for singletons will be {singleton-name}/.

Singleton paths ending with a trailing slash /

If the path ends with a trailing slash /, the singleton's content ill be created in its own directory named after the slug:

singleton-name
└── slug
    ├── index.yaml
    └── other.mdoc

Singleton paths ending without a trailing slash

If the path does not end with a trailing slash, the singleton's index file will be created immediately inside the singleton directory:

singleton-name
├── slug.yaml
└── slug
    └── other.mdoc

Individual fields output

Right now, only images allow you to decide where content should be generated, independently of the collection/singleton level path settings.

⚡️

The reason for this is certain frameworks (like Next.js) need your images to be in a specific directory (like /public) to be easily accessible.

Instead of generating all your content inside the public directory to satisfy this requirement, Keystatic lets you define a specific path for your images only.

Here's how you define where an image gets generated for a given collection or singleton:

// In the context of a `posts` collection...
coverImage: fields.image({
  label: "Cover Image",
  directory: "public/images/posts",
}),

Regardless of where the posts entries are created, the coverImage image will be generated in public/images/posts/{post-slug}.