Repository navigation
Releases: plain-cms/plain
Release list
v1.20.3 — plugin screens update the moment you deploy
A plugin's admin screen could keep running old code for hours after its deploy went green, and only a manual hard refresh fixed it.
Cache-busting stamps every asset URL the build controls, but it walked HTML and admin/*.js only. A plugin's admin module is reached neither way — the admin imports it on demand from the URL in api/site.json — so that URL carried no version, and browsers and CDNs held on to the old copy for the whole of its TTL.
Those URLs now carry the same content hash as every other asset. api/site.json is not cached, so a deploy changes the URL and the new module loads on the next ordinary reload.
Nothing to do beyond merging the update PR. If you write plugins with admin screens, this is worth taking.
v1.20.2 — engine tests stop asserting your files
A patch on v1.20.1, which made tests/ engine-owned.
One of those newly-shipped tests, skills.test.js, checked that every repo path a skill mentions exists — and its list of root files included CLAUDE.md, site.config.json and README.md. Those are yours, not the engine's, and a site is free not to have one. niche.careers has no CLAUDE.md, so it failed a test it never asked for.
The test already drew this line for content/, data/ and media/. Now the root gets the same treatment: it reads engine.json and asserts only the files the engine actually installs.
If you are on 1.20.1, take this one. If you are on 1.20.0 or older, upgrade straight here.
v1.20.1 — plugin admin screens, and upgrades that carry their own tests
Three versions' work — the last release was v1.18.0.
Plugins can add admin screens (1.19.0)
A plugin may now contribute its own screen to the admin. Declare it in
plugin.json and export a render function from admin.js; the build lists it
in api/site.json and the admin imports it on demand.
The screen receives { h, siteInfo, dataScreen, apiFetch, options } — and
deliberately not the user's token. dataScreen gives a plugin the same
fetch/render/error shape the built-in backend screens use, so a read-only view
over your own API is a few lines rather than a rewrite.
apiFetch — the write half (1.20.0)
dataScreen reads. apiFetch writes: it resolves a named entry from
services in site.config.json, attaches the stored bearer token, and turns a
401/403 into a message that says what to do about it. Endpoints stay in config
and secrets stay out of it (§5.1); credentials remain BYOK in localStorage
(§8.3).
tests/ is engine-owned (1.20.1)
The goldens assert what build.js emits, so shipping one without the other
strands a site the moment engine output changes — which is exactly what
1.19.0's new api/site.json key did. Upgrades now carry the code and the
fixtures asserting it together.
An ownership change can't deliver itself: the updater that runs an upgrade is
the one already installed, and to it every newly-claimed file looks like a
local edit. So:
tools/update.jstreats a path missing from the installed manifest as newly
engine-owned, takes upstream's copy, and lists it apart from routine updates
so the diff is reviewed. This is the rule for future ownership changes.migrations/001-adopt-engine-tests.jscovers this one, running from upstream
where the installed updater can't help. It replaces only paths upstream owns,
so a test you wrote yourself survives.update.ymlno longer regenerates goldens before running them. That step was
load-bearing only whiletests/was unowned; a real disagreement between
build.jsand its own fixtures should fail, not be written over.
Also fixed: admin-demo.test.js had been failing since it was written — its
temp dir sat outside the repo, so Node read the copied ESM as CommonJS. The
suite is green at 123/123.
Upgrading
Merge the PR your site's updater opens. If it was installed before 1.20.1,
expect migration 001 to adopt the engine's tests/ tree — the diff is in the
PR, and reverting rolls it back.
If you have deliberately customised a test at an upstream path, it will be
replaced. Read that part of the diff before merging.
v1.18.0 — try the editor, no account needed
You should be able to see what a CMS is before you create an account for it. Until now, /admin/ opened on a screen asking for a GitHub token — so nobody could look at plain's editor without first deciding to try plain.
Set "demo": true in the site block of site.config.json and that screen leads with Try the editor instead. /admin/?demo=1 skips it entirely and drops a visitor straight into the dashboard.
Try it here: the editor, with no account — write a post, publish it, watch the build pill, open History and restore an older version.
It is not a mock-up
Every call the admin makes to GitHub goes through one function (gh() in admin/js/github.js), so the demo is a stand-in for exactly that one function. admin/js/demo.js answers the same REST paths from a repository that lives in the browser tab, seeded from the site's own published JSON API — api/site.json for the config and menu, api/<collection>/index.json for one Markdown file per item, since those already carry every item's raw body. Images the content references are listed and previewed straight from the published site.
So nothing is faked at the UI level: edits rewrite files, saves make commits, the publish pill runs, History lists versions and Restore restores them. It is the real admin, with a different repository behind it.
| Per tab | State lives in sessionStorage. A new tab starts clean, and a demo visitor never disturbs a real sign-in on the same device. |
| Free | No GitHub API calls, no keys, nothing to rate-limit — the demo reads static JSON your site already publishes. |
| Never in doubt | A standing strip across every screen says it's a demo, with Start over, Exit demo, and the way to run it for real. |
| Off by default | Sites without the flag behave exactly as before, and demo.js is never even fetched. |
Also in this release
- Cache-busting now versions dynamic
import()URLs, not just static ones. - The media library stops printing "0 KB" for a file whose size it doesn't know.
Upgrading
Engine-owned (admin/, config.defaults.json), so an existing site picks it up on its next update PR — click Update available in the admin, or wait for the weekly workflow. Then add one line of config to switch it on. No new dependencies, no output changes, no migration.
v1.17.0 — write a post from your terminal, in one prompt
Your site is a folder of files, so an AI coding agent can run it. Open your repo in Claude Code and say:
"Write a post about the three things I learned launching this site, then publish it."
This release ships three skills in .claude/skills/, which Claude Code picks up automatically:
| Skill | What it knows how to do |
|---|---|
| plain-post | write, edit, publish, unpublish, or translate a post or page |
| plain-site | images, the menu, the footer, settings, redirects, themes, plugins, languages, deploys |
| plain-extend | add a field or a whole content type, a landing page, a plugin, a starter |
They're recipes, not magic. plain-post tells the agent to read your collection's fields out of site.config.json before writing a line, so the frontmatter is right the first time; to keep filenames as slugs, because the filename is the URL; to add a redirect whenever a URL changes; and to run the tests and a build before anything is committed. If the content is wrong, the build stops with the file, the line, the problem, and the fix — so a bad post never reaches your readers.
Publishing is still your call. The permission allowlist in .claude/settings.json covers building and inspecting — never git commit or git push. A test asserts that boundary so it doesn't get widened by accident.
Using a different agent? The skills are plain Markdown — point any tool at .claude/skills/ and it gets the same instructions.
Upgrading
Both paths are engine-owned, so an existing site picks them up as new files on its next update PR — click Update available in the admin, or wait for the weekly workflow. Nothing else changes: no new dependencies, no output changes, no migration.
Also since v1.13.0
These shipped on main but were never tagged here:
- v1.16.0 — the editor stops shouting empty fields at you; hidden fields stay hidden and the preview looks like the page
- v1.15.0 — new
static-rootplugin: root-level files (site-verification,.well-known/) from astatic/folder - v1.14.0 — plugin update notifications and one-click plugin updates in the admin
Plus, on main since: deep-linkable searches with ?q=.
v1.13.0 — GitHub-login auth for the backend
The admin's Feedback and Insights tabs now authenticate to your backend with the GitHub login you're already signed in with — no separate admin secret to create, paste, or rotate.
Pairs with plain-cms/backend's new GitHub-verify mode: set Admin:Repo = owner/name on the backend, and it authorizes any caller whose GitHub token has push access to that repo (checked live against the GitHub API). The tabs send auth.token automatically — no prompt.
- Backward compatible: a manually-pasted token still wins if present (static-token backends keep working); an open backend just ignores the extra header.
- Only
admin/js/backend-data.jschanged.
See the backend README's Admin auth section for the three modes (GitHub / static token / open) and the security note.
v1.12.0 — Feedback & Insights in the admin
Two new owner-only admin tabs that read what your backend collects.
Features
- Feedback tab — reads
GET /feedback, the messages yourfeedbackwidget posts, as a list (message · email · page · time). - Insights tab — reads
GET /reports(page views + CTA clicks) and renders stat tiles + a per-page/per-CTA table. - Both are gated by the backend's admin token (BYOK, kept in
localStorage, sent only to your backend). Tabs appear only when the relevant plugin is enabled and a backend is connected on the Backend screen.
Changed
- sales-analytics → v2.0.0 is now a pure tracking plugin: the standalone
/insights/dashboard page and its CSS are removed — the report view moved into the admin's Insights tab. Tracking beacons (POST /events) are unchanged. Sites keep collecting; the reports just render in the admin now. - Core budget cap raised 2,500 → 2,600 (spec C3) to fit the two screens.
New engine file admin/js/backend-data.js; plugins/sales-analytics/{index.js,client.css} removed.
v1.11.3 — sign-in remembers the repository
Sign-out now keeps the repository (and branch) in localStorage and drops only the token, so the sign-in screen prefills the repository — no more re-typing owner/name on every login. One-line change in admin/js/github.js. Pairs with v1.11.2: a lapsed token bounces you to sign-in with the repo already filled in.
v1.11.2 — admin auto sign-out on expired token
Admin auth fix.
Fix
- A GitHub 401 ("Bad credentials") — an expired OAuth-worker session or a revoked/mistyped token — now signs the writer out automatically and returns them to the sign-in screen, instead of dead-ending an action (e.g. Save) on an error toast. The central
gh()call clears the dead token on any 401 andapp.jsroutes to sign-in, so every GitHub call is covered.
Only admin/js/{github,app}.js changed.
v1.11.1 — Create-backend button fix
Patch release fixing the Create backend repo button introduced in v1.11.0.
Fix
- The button called the GitHub contents API with the admin's sign-in token, which fails with "Resource not accessible by integration" for fine-grained PATs and OAuth-app / GitHub-App tokens — they can write commits but can't create repositories. That covers most admin sign-ins (anyone using the OAuth worker).
- It's now a plain link to GitHub's create-from-template page (
github.com/plain-cms/backend/generate?owner=…&name=…), which opens under the writer's own browser GitHub session — full account permissions, works for every token type, no JS.
Only admin/js/{backend,github}.js changed.