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:
| Field | Purpose |
|---|---|
| Branch name | Git branch name. Default: cms/edit-YYYY-MM-DD. |
| Workspace title | A 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:
- Creates the Git branch from your base branch.
- 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-updatewhose head is undercms/*(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:
- Writes this deployment’s pointer file (
cms/pointers/<build-id>.jsonwith{ "branch": "<name>", "buildId": "<build-id>" }on the pointer ref or base branch — see Multiple deploys) so the public site reads content from that branch. - Revalidates Next.js caches — visitors see fresh content immediately, no redeploy needed.
- 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
mergedstatus. - 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:
- Validates required fields (and reference min/max rules). Invalid entries are not submitted.
- Writes the JSON file — via the GitHub API in production, or directly to the local filesystem in development. New entries in
draftare promoted tochangedon first successful save so they can appear on public pages once the workspace branch is published from the header. - 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):
| Field | Purpose |
|---|---|
git.baseBranch | Default branch (e.g. main). Feature branches are created from here; PRs target this branch. |
git.publishedPointerBranch | Optional. 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.
Environment variables (Git-related)#
| Variable | Description |
|---|---|
GITHUB_REPO_OWNER | GitHub username or organization |
GITHUB_REPO_NAME | Repository name |
CMS_GITHUB_TOKEN | Static token for GitHub API reads/writes in production. Required for private repos. |
CMS_BRANCH | Optional 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_ID | Together 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.ts → git. 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.