Skip to content

Content model

Your content model lives in code, in the config you pass to createCMS: a schema of types and their fields, and workspaces with roots that decide where entries can be created. Editors fill it in through the dashboard, and every entry is saved as a JSON file in your repository.

cms.ts
import {Config, Field} from 'alinea'
import {createCMS} from 'alinea/next'

const Page = Config.document('Page', {
  fields: {
    body: Field.richText('Body')
  }
})

const Blog = Config.document('Blog', {
  contains: ['BlogPost'],
  fields: {}
})

const BlogPost = Config.document('Blog post', {
  fields: {
    publishDate: Field.date('Publish date'),
    cover: Field.image('Cover image'),
    body: Field.richText('Body')
  }
})

export const cms = createCMS({
  schema: {Page, Blog, BlogPost},
  workspaces: {
    main: Config.workspace('My site', {
      source: 'content',
      mediaDir: 'public/media',
      roots: {
        pages: Config.root('Pages', {contains: ['Page', 'Blog']}),
        media: Config.media()
      }
    })
  },
  baseUrl: {
    development: 'http://localhost:3000',
    production: 'https://example.com'
  },
  handlerUrl: '/api/cms'
})

The building blocks

  • Types describe one kind of entry, such as a page or a blog post, with its fields and dashboard settings. Documents are types with a title, path and SEO metadata built in.

  • The schema lists every type by name. The name (BlogPost) is what gets stored in content files and what you refer to in contains.

  • Fields hold the data: text, rich text, links, images, lists of blocks and more.

  • Workspaces group content, for example one per website, each with its own content directory.

  • Roots are the top-level sections of a workspace's content tree, like "Pages" or "Settings". Entries nest below them, and a root can be translated.

  • Media roots hold uploaded images and files.

Once content is saved you read it back with queries, which are typed from the same schema.

Where content is stored

Every entry is a JSON file in the workspace's source directory. The folder structure follows the content tree: a root is a folder, an entry is a file named after its path, and the children of an entry live in a folder with the same name.

content
├ pages               // the "pages" root
│ ├ index.json        // entry with path "index", url "/"
│ ├ blog.json         // url "/blog"
│ ╰ blog              // children of blog.json
│   ├ hello-world.json
│   ╰ hello-world.draft.json
╰ media               // the media root
  ├ landscape.json    // metadata of an uploaded file
  ╰ documents.json    // a media folder

public/media          // mediaDir: the uploaded files themselves
╰ landscape.2V4c3kRS.jpg

A few details that are good to know:

  • Drafts and archived versions sit next to the published file as <path>.draft.json and <path>.archived.json.

  • In a root with i18n each locale gets its own folder: content/pages/en/..., content/pages/fr/....

  • With more than one workspace, each source directory must be named after its workspace key and share the same parent folder, for example content/main and content/blog.

  • An entry file holds the field values plus a few internal properties such as _id, _type and _index (its position among its siblings). The path isn't stored separately: it's the file name.

Because content is plain files, it's versioned, reviewed and deployed with your code. alinea dev and alinea build compile your config and index the content into the @alinea/generated package in node_modules, which your site queries at runtime. That package is regenerated on every run, so don't edit or commit it.

In this section