Git CMS

Git CMS

Store Markdown content in a Git repository. Edit it in the Pluto admin. It works with local Git out of the box, and with GitHub to publish a deployed site. No database.

The Git CMS is the backend in @plutocms/core. It needs no database. Content is Markdown files with YAML frontmatter in a Git repository. Editors change the files in the Pluto admin.

Pluto works out of the box with local Git. In development, each save in the admin is a commit in the local repository of your project. You push when you are ready. Local Git publishing works in development only: to publish from a deployed site, connect GitHub. A deployed site reads the files live through the GitHub API, with a cache.

GitHub is the only Git provider for a deployed site today. Support for other Git providers is planned.

How it works

  • Each save is a commit: in the local repository in development, in the GitHub repository on a deployed site. On GitHub, the commit carries the name of the editor.
  • On a deployed site, users with push access to the repository are editors. They sign in with a GitHub App. In development, you are signed in as a local user.
  • The public site reads published posts at run time. A save publishes in seconds, with no new build.
  • A draft is a post with status: draft in its frontmatter.
  • A save that changes nothing makes no commit.

On unless a backend layer is present

plutoGithub.enabled is 'auto' by default. It also accepts true and false.

ValueResult
'auto'The CMS is on, unless the app has a backend layer. With a backend layer, the CMS is off and the module logs nothing.
trueThe CMS is on. With a backend layer, the module logs the error below and stays off.
falseThe CMS is off.

A backend layer is one of these:

  • A layer with $meta.name === 'supabase'.
  • The @nuxtjs/supabase module.
  • A layer with $meta.plutoBackend: true. See Create a layer.

When the CMS is off, the module adds nothing: no runtime config key, handler, plugin, page, middleware, or import.

The standalone CMS cannot run with the supabase layer. Remove plutoGithub.enabled or the supabase layer.

Start fast

The quickest start is the template. You need no GitHub App on your machine. After a deploy, the admin creates the App for you: see the Quickstart.

Terminal
npm create pluto@latest my-blog -t blog --backend git

Or use the CMS in an existing app. Install @plutocms/core first. extends does not install it.

npm install @plutocms/core

Then extend only @plutocms/core. The app needs no other layer, and it sets no plutoGithub option:

nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@plutocms/core']
})

In development, the CMS reads and writes the local content/ folder, and commits each save to the local Git repository. In production, it uses the GitHub repository that Pluto finds. See Reference. Posts live in content/posts/<slug>.md.

File layout

A file is <dir>/<type.source>/<slug>.md. The built-in post type uses source: 'posts'. With the default dir, a post is content/posts/<slug>.md.

  • The item id is the slug. It is the file name. It is not a frontmatter key.
  • A slug uses lowercase letters, numbers, and single dashes.
  • The first richtext field is the Markdown body.
  • Every other field, except the slug field, is a frontmatter key.
  • An update keeps the frontmatter keys that the type does not know.
  • A file with no status key counts as the default status. For posts, that is draft.
  • An update whose new file text equals the stored text makes no commit. The updated timestamp alone is not a change. The call returns the stored item with the same _version.

Nested content types

A content type can store its items in nested folders. Set files: { nested: true } on the type. A docs site needs this. The docs layer uses it.

files: {
  nested: true, // ids are paths: '1.start/2.install'
  exclude: ['index', 'drafts/'], // an id, or a folder with a trailing slash
  frontmatter: { read, write } // pure functions: (frontmatter) => frontmatter
}
  • The id is the path under the type folder, without .md. Each segment is a slug with an optional numeric prefix, such as 2.install. An id has at most 6 segments and 255 characters.
  • source: '' means the content dir itself. Only a nested type can use it.
  • The slug field holds the whole id. A new path in the slug field creates the item in a folder. An update with a new path moves the file in one commit. A move onto a taken path gives 409.
  • The default sort of a nested type is compareContentIds. In each segment, index comes first. Then the numeric prefix sorts as a number. Then the name decides. Set defaultSort: { field: 'id', direction: 'asc' } for a nested type that also has timestamps.
  • frontmatter.read runs on a copy after Pluto parses a file. Use it to add flat keys that the fields read. frontmatter.write runs before Pluto writes a file. Use it to fold the flat keys back. Keys that the type does not know must stay.
  • Every store has listTree. It reads every .md file under a folder. The GitHub store does this with one GraphQL call. All stores skip dot folders and dot files.
  • The admin pages of a nested item use a catch-all route: /admin/content/<type>/<id path>. contentIdPath(id) encodes an id for a URL and keeps the /.
  • The public read routes also read a nested item: the id can hold slashes. See Public read.

What is in this section

Quickstart

Set up the Git CMS in 10 minutes. Pluto creates the GitHub App.

Reference

Options, environment variables, and private key formats.

GitHub App

Create the App by hand, and how the sign-in works.

Troubleshooting

Fix the setup checks and the sign-in errors.

Editing

The Markdown editor, drafts, nested types, and conflicts.

Public read

Routes and composables for the public site.

Security

Sessions, CSRF checks, and sanitized HTML.

Planned

The source lists these for a later version: revision history, settings stored in the repository, a password provider, scheduled posts, and configurable collections.