Meet the beet
Themes: make the CMS your own
Custom themes are here. Developers can create themes that reskin the CMS to match a brand or a team's preferences, from colors and type to spacing. Each user can pick a theme of their choosing or create their own.
Asset library
The CMS has a new asset library for organizing a project's images and files. Upload files and organize them into folders, then pick them from any image or file field.
Folders can sync from Figma and Google Drive. When an asset is replaced, either by manually uploading a new version or by a sync, every doc that uses it is updated automatically.
Translations manager (experimental)
The translations manager has been rebuilt from the ground up to work more closely with the docs they support. Translations now have draft and published versions like docs do, so changes to translations go live when the doc is published instead of as soon as they're saved. It's experimental for now, so it's off by default.
Locales can also fall back to one another. When a string isn't translated for a locale, the translation from the next locale in its i18n.fallbacks chain is used instead.
To try it, set experiments: {v2TranslationsManager: true} in your cmsPlugin() config. Existing translations are migrated automatically the first time you start the dev server or build your site with it on. See Localization for details.
Human-friendly diffs
Reviewing changes before you publish is now much easier. Instead of a raw JSON diff, the CMS lists what changed field by field, labeled with the names from your schema, like "Hero › Title". Text edits are highlighted word by word, replaced images are shown side by side, and moving an item in a list shows up as a reorder rather than as every item changing.
New defaults
Several features that were opt-in in v3 are now on by default:
-
Dependency graph: tracks references between docs, so that fetching a doc can also resolve the docs it references.
-
Module preloading: Root.js adds
<link rel="modulepreload">tags for the shared JS chunks that a page's scripts import, so the browser can fetch them in parallel.
The legacy EditorJS rich text editor has also been removed. Rich text fields use the Lexical editor, which has been the default since v2, and stored content doesn't need to change.
Each new default can be turned off. See the v4 migration guide for details.
Also new since v3.0
If you haven't upgraded in a while, here's what else landed during the v3.x releases:
-
Collaboration: comments on individual fields, seeing who's editing each field, a task manager with @mentions (experimental), and email notifications.
-
Publish checks: required checks that must pass before a doc is published, configured per collection.
-
Root AI: tools for releases and for Google Docs, Sheets and Slides, "Edit with AI" for images, and change proposals for reviewing AI edits before they're applied.
-
An agent-ready CLI:
root-cmscommands built for AI agents, and aroot-cms setupwizard that sets up Google Cloud for a new project. -
Editing: compare viewports side by side in the preview, undo and redo field edits, and crop images to fixed aspect ratios.
-
Framework: parallel static builds with
root build --threads, brotli compression, and security headers.
Routes and data fetching
createRoute() is now the recommended way to set up routes that are connected to the CMS. Give it a collection, and it loads the doc for each URL and handles preview mode, locales, translations, 404s and caching for you.
To load more content for a page, like a shared header or a list of posts, use a batch request. Instead of a separate RPC to Firestore for each read, a batch request fetches the page's docs, queries and data sources in parallel. With the new translations manager, it then fetches the translations for every doc in the batch, for the page's locale only. In createRoute(), add reads with the batchRequest option:
// routes/[...slug].tsx
import {createRoute} from '@blinkk/root-cms';
export default function Page(props) {
// props.doc, props.header and props.posts are ready to render.
}
export const {handle} = createRoute({
collection: 'Pages',
batchRequest: (req) => {
req.addDoc('Global/header');
req.addQuery('posts', 'BlogPosts', {limit: 3});
},
preRenderHook: (props, ctx) => ({
...props,
header: ctx.batchResponse.getDoc('Global/header'),
posts: ctx.batchResponse.getQuery('posts'),
}),
});
Outside of a route, use cmsClient.createBatchRequest(). See the @blinkk/root-cms API reference for all of the options.
Installation
Root.js v4.0 is available now on npm and is installable using the package manager of your choice. It requires Node.js 24 or later.
pnpm add @blinkk/root@latest @blinkk/root-cms@latest
Migration notes
Most projects only need to update their packages. If you relied on one of the old defaults, read the v4 migration guide.
To have an AI coding agent, such as Claude Code, do the upgrade for you, start it in your project's folder and paste this prompt:
Upgrade this project to Root.js v4. Follow the instructions at
https://rootjs.dev/skills/root-v4-upgrade.md
The agent works on a new branch and saves a snapshot of your pages before it changes anything. It updates your packages and suggests other changes worth making for v4, like moving CMS routes to createRoute(). Then it compares your pages before and after, runs your build and tests, and offers to file GitHub issues for anything left over.
Thank you!
Huge thank you to everyone who filed bugs, requested features and tested new releases throughout v3. We have lots more planned for v4.
Issues, ideas, and PRs are always welcome at github.com/blinkk/rootjs.