Show navigationHide navigation

Editorial workflow

OctoCMS uses Git branches to separate in-progress edits from live content. The workflow has three stages: create a branch (your workspace), publish (go live), and merge (clean up on GitHub).

Step 1: Create a branch#

Click the branch chip in the header (it shows the current branch, e.g. main) to open the branch menu anytime — you do not need to open or edit content first. Choose Create new branch. You will be asked for:

FieldPurpose
Branch nameGit branch name. Default: cms/edit-YYYY-MM-DD.
Workspace titleA short label for this batch of edits. Used as the draft pull-request title on GitHub.
Description (optional)Longer context. Included in the pull-request body.

When you confirm, the CMS:

  1. Creates the Git branch from your base branch.
  2. Opens a draft pull request on GitHub (labeled cms-update). If that fails (for example a transient API error) the branch is still created — you can open the PR manually on GitHub.

You must have an active branch before saving in production. If you try to save without one, the CMS shows a "Create a working branch" dialog first.

Step 2: Edit and save#

Every Save commits to the active feature branch. Work on as many entries as you need — each save is a separate commit.

To switch branches, open the header menu again. It lists:

  • Base branch (e.g. main) — read-only browsing; you cannot save while this is selected.
  • CMS branches — open pull requests labeled cms-update whose head is under cms/* (title and PR link come from GitHub). The pointer branch (git.publishedPointerBranch) is hidden so you do not select it by mistake.

The branch menu only changes where your saves go. The public site follows the last Publish for this deployment (per-build pointer file — see Multiple deploys).

History panel#

The entry editor sidebar includes a History card below Entry details that shows the last 5 commits to touch this entry's JSON file. Each row links directly to the commit on GitHub, and a See all commits link opens the full commit history for the file on the base branch.

The panel is lazy: it only loads once the card scrolls into view, and never blocks editor interactivity. In local development the panel shows "No commits yet." because commit history is only meaningful against GitHub.

Diff view#

The editor's top bar has an Edit / Diff segmented control. Switch to Diff to see, per field, exactly what this branch would contribute to the base branch if it were merged. See Diff view for the full reference.

Step 3: Publish#

When the content on a branch should go live on the public site, open the branch menu in the header (branch chip) and click Publish next to that branch. There is no separate Publish control on the entry editor — that action updates the deployment pointer only from the branch menu.

Publishing does three things:

  1. Writes this deployment’s pointer file (cms/pointers/<build-id>.json with { "branch": "<name>", "buildId": "<build-id>" } on the pointer ref or base branch — see Multiple deploys) so the public site reads content from that branch.
  2. Revalidates Next.js caches — visitors see fresh content immediately, no redeploy needed.
  3. Marks the GitHub pull request as Ready for review (best effort).

Publish does not merge the PR. Your branch stays open and you can continue editing. Click Publish again after further changes to push them live.

Step 4: Merge the pull request#

Once you are satisfied with the published content, go to GitHub and merge the pull request. This is a normal GitHub merge — review the diff, request approvals if your repo requires them, then merge.

After the merge:

  • Edited entries return to the baseline merged status.
  • The public site continues serving content normally (content is already on the base branch after merge).
  • You can delete the feature branch on GitHub as usual.

Merging is separate from publishing. Content goes live when you Publish, not when you merge. Merging simply moves your commits into the base branch so the feature branch can be cleaned up.


How saving works#

When you click Save on an entry the CMS:

  1. Validates required fields (and reference min/max rules). Invalid entries are not submitted.
  2. Writes the JSON file — via the GitHub API in production, or directly to the local filesystem in development. New entries in draft are promoted to changed on first successful save so they can appear on public pages once the workspace branch is published from the header.
  3. Triggers cache revalidation so the public site sees fresh content without a full redeploy.

Branch settings#

Git branch names are configured in cms/octocms.config.ts under git (not environment variables):

FieldPurpose
git.baseBranchDefault branch (e.g. main). Feature branches are created from here; PRs target this branch.
git.publishedPointerBranchOptional. When set (e.g. cms/publish-pointer), per-build pointer files under cms/pointers/ are read and written only on this branch so Publish does not need to commit to a protected base branch. When omitted, those files live on git.baseBranch.

Dev mode#

In development, saves write to the local filesystem — no GitHub API calls for content. The branch menu in the header is still available so you can open Create new branch and exercise the same UI as production. Creating a branch, switching branches, and Publish still call the GitHub API (createBranch, publishBranch, etc.); configure CMS_GITHUB_TOKEN and repo env vars if you want those actions to succeed locally, otherwise expect clear errors from GitHub. Use your normal git workflow when you prefer to create branches from the terminal.

VariableDescription
GITHUB_REPO_OWNERGitHub username or organization
GITHUB_REPO_NAMERepository name
CMS_GITHUB_TOKENStatic token for GitHub API reads/writes in production. Required for private repos.
CMS_BRANCHOptional default public branch for this deploy when no valid pointer override exists (must exist on GitHub; otherwise git.baseBranch is used).
VERCEL_DEPLOYMENT_ID / VERCEL_BUILD_ID / GITHUB_RUN_ID / BUILD_IDTogether they define the build id used in cms/pointers/<id>.json. See Multiple deploys.

Repo identity stays in environment variables; branch policy stays in cms/octocms.config.tsgit. Per-build public branch overrides are documented in Multiple deploys.

Cache revalidation#

  • Save uses on-demand revalidation for immediate freshness.
  • Publish calls the same revalidation path so public routes pick up the new pointer immediately.
  • For instant updates after external merges, configure a GitHub webhook to POST /api/revalidate/blog (or whichever tag) as needed.

What not to do#

  • Do not hand-edit cms/__generated__/ — reserved for generated artifacts.
  • Do not edit content JSON files manually if they contain reference UUIDs or media IDs — use the CMS editor to keep those consistent.

Branch history file (cms/branch-history.json)#

This JSON file maps branch names to workspace metadata: title, optional description, createdAt, and entries (paths to content JSON files touched on that branch). It is merged from the base branch when you create a new working branch. Parallel PRs that both change the same branch key can produce merge conflicts like any shared JSON file; unrelated branch keys usually merge cleanly.