Skip to content

Editorial Workflow

This is an advanced remote workflow designed for teams that require a review process before changes are merged into the configured branch. Editors can submit changes for review, and designated reviewers can approve or request modifications.

Use Cases

  • Teams of content creators and editors working on collaborative projects.
  • Projects that require a formal review and approval process for content changes.
  • Situations where content quality and consistency are critical, necessitating oversight.
  • Workflows that involve multiple stages of review, such as draft, review, and publish.

Requirements

The GitHub or GitLab backend must be used.

Future Plans

Support for the Gitea/Forgejo backend may be added in the future.

Configuration

Add the publish_mode option to the top level of your CMS configuration file:

yaml
publish_mode: editorial_workflow
toml
publish_mode = "editorial_workflow"
json
{
  "publish_mode": "editorial_workflow"
}
js
{
  publish_mode: 'editorial_workflow',
}

How It Works

Nothing an editor does in the CMS touches your configured branch until the change is published. Each entry with unsaved work lives on its own branch with an open pull request, so making a change and releasing it are two separate steps.

Editor actionWhat happens in Git
Save a new entryA branch named cms/[COLLECTION_NAME]/[SLUG] is created off the configured branch, the entry files are committed to it, and a pull request is opened
Save an existing draftAnother commit is added to the same branch
Change the statusThe pull request’s label is updated
Delete a published entryA pull request is opened that removes the entry files
PublishThe pull request is merged and its branch is deleted
DiscardThe pull request is closed without merging and its branch is deleted

On GitLab the same applies, with merge requests in place of pull requests.

Saving and Sending for Review

Saving an entry doesn’t hand it to anyone — it stays a draft until someone moves it on. So when you save an entry that’s still in the Draft status, the CMS asks what you want to do next:

  • Send for Review moves the entry to In Review straight away, ready for someone to look at.
  • Later leaves it as a draft. You can send it whenever you like, using the status button in the entry editor or by dragging its card between columns on the Editorial Workflow page.

The prompt only appears while an entry is still a draft. Saving one that’s already In Review or Ready leaves its status alone.

Statuses

An unpublished entry moves through three stages, shown as columns on the Editorial Workflow page and as a status button in the entry editor:

StatusLabelMeaning
Draftsveltia-cms/draftWork in progress
In Reviewsveltia-cms/pending_reviewReady for someone to look at
Readysveltia-cms/pending_publishApproved and ready to be merged

A pending deletion carries a fourth label, sveltia-cms/pending_deletion. It isn’t a stage — there’s no review to move it through, only the deletion itself to carry out or call off — so it doesn’t appear as a column. See Deleting Entries.

An entry in the Draft status is kept as a draft pull request or draft merge request, so it can’t be merged by accident. Moving the entry to In Review or Ready marks it ready for review.

GitHub and GitLab record this differently: GitHub has a dedicated draft flag, while GitLab marks a draft with a Draft: prefix on the merge request title. Sveltia CMS adds and removes that prefix for you, so if you edit a merge request title by hand, keep the prefix intact while the entry is in the Draft status.

Custom Label Prefix

Labels are written with the sveltia-cms/ prefix by default. You can change it with the cms_label_prefix option in the backend section:

yaml
backend:
  name: github
  repo: user/repo
  cms_label_prefix: my-cms/
toml
[backend]
name = "github"
repo = "user/repo"
cms_label_prefix = "my-cms/"
json
{
  "backend": {
    "name": "github",
    "repo": "user/repo",
    "cms_label_prefix": "my-cms/"
  }
}
js
{
  backend: {
    name: 'github',
    repo: 'user/repo',
    cms_label_prefix: 'my-cms/',
  },
}

Migrating from Netlify/Decap CMS

Sveltia CMS reads the netlify-cms/ and decap-cms/ prefixes as well as your configured one, so pull requests created by Netlify CMS or Decap CMS show up straight away. Labels are always written with your configured prefix, so an imported pull request is migrated the first time its status changes.

Squash Merges

You can squash all the commits in a pull/merge request into a single commit when it’s merged by adding the squash_merges option to the backend section. Otherwise, a merge commit is created. This is supported with both the GitHub and GitLab backends.

yaml
backend:
  name: github
  repo: user/repo
  squash_merges: true
toml
[backend]
name = "github"
repo = "user/repo"
squash_merges = true
json
{
  "backend": {
    "name": "github",
    "repo": "user/repo",
    "squash_merges": true
  }
}
js
{
  backend: {
    name: 'github',
    repo: 'user/repo',
    squash_merges: true,
  },
}

See the GitHub or GitLab documentation for more information about squash merging.

Editorial Workflow Page

A board with a column for each status is available from the top navigation. Drag a card from one column to another to change an entry’s status, or use the status button in the entry editor. Each card also offers the actions available at that stage, and clicking the card opens the entry in the editor.

Entry List

Unpublished entries appear in the entry list alongside published ones, each with a badge showing its status:

  • An entry that updates a published one replaces it in the list, so you see the pending version rather than what’s currently live.
  • An entry that has never been published is listed separately under an Unpublished Entries heading, above the published entries.

Deleting Entries

Deletion goes through review like any other change, so removing an entry from the configured branch is a two-step process. What the Delete button does depends on whether the entry has ever been published.

Deleting a Published Entry

Deleting a published entry opens a pull request that removes its files. The entry stays in the configured branch until that pull request is published. Until then it appears in the entry list and on the Editorial Workflow page with a Pending Deletion badge.

Because there’s nothing to review or edit, a pending deletion doesn’t move through the three stages. It carries the sveltia-cms/pending_deletion label rather than one of the stage labels, so it’s never mistaken for content waiting to go live — including by another CMS reading the same repository. It’s listed in its own section below the board, and its card offers two actions:

  • Cancel closes the pull request and leaves the entry in place.
  • Delete merges the pull request, which removes the entry.

Opening a pending deletion in the entry editor shows its content for reference only. The fields are read-only and there’s no Save button, because the only things left to do are carrying the deletion out or calling it off.

Different from Decap CMS

Decap CMS has a separate Unpublish action, and its Delete button removes the entry from the configured branch immediately. Sveltia CMS has no Unpublish action: deleting a published entry is the unpublish process, so making the change and releasing it stay separate, the same as with any edit. See issue #770.

Deleting an Unpublished Entry

  • If the entry has never been published, deleting it closes its pull request. Nothing is left behind, because nothing was ever merged into the configured branch.
  • If the entry updates a published one, the button is labelled Discard instead. Discarding closes the pull request and restores the published version, which stays in the configured branch. The entry itself isn’t deleted.

Restricting Publishing and Deletion

Two collection options let you limit what editors can do. Both are set on the collection, not on the backend:

  • publish: false hides the publishing controls, so editors can move an entry through the review stages but someone else has to publish it.
  • delete: false prevents entries from being deleted. Discarding unpublished changes is still allowed, because that leaves the published version untouched.
yaml
collections:
  - name: posts
    folder: content/posts
    publish: false
    delete: false
toml
[[collections]]
name = "posts"
folder = "content/posts"
publish = false
delete = false
json
{
  "collections": [
    {
      "name": "posts",
      "folder": "content/posts",
      "publish": false,
      "delete": false
    }
  ]
}
js
{
  collections: [
    {
      name: 'posts',
      folder: 'content/posts',
      publish: false,
      delete: false,
    },
  ],
}

Event Hooks

Editorial Workflow adds four event types on top of preSave and postSave:

EventWhen it fires
prePublishBefore a pull request is merged
postPublishAfter a pull request has been merged
preUnpublishBefore a published entry is removed from the configured branch
postUnpublishAfter a published entry has been removed from the configured branch

The preUnpublish and postUnpublish hooks fire when a deletion is published, not when it’s requested — that’s the point at which the entry actually leaves the configured branch. Publishing a deletion fires these instead of prePublish and postPublish, because nothing is being published.