Getting started
Overview
Root.js is a web platform for building content-driven websites. It pairs a TypeScript web framework with a built-in CMS, so developers, writers and translators can work on the same site.
-
The framework renders TSX on the server, either on demand (SSR) or ahead of time as static HTML (SSG). It's built on Vite and Preact, and it ships no client-side JavaScript unless you add it.
-
The CMS adds visual editing with live preview, localization, releases and publishing workflows. Content is stored in your own Firebase project, and content models are defined in code.
The CMS is optional. You can start with the framework and add the CMS at any time.
Requirements
-
Node.js 24 (the current LTS) or later.
-
A package manager. We recommend pnpm, which you can enable with Corepack.
-
For the CMS: a Google Cloud project with Firebase, and the gcloud CLI.
node --version # v24 or later
corepack enable
Create a project
Scaffold a new project with create-root, then install its dependencies:
pnpm create @blinkk/root my-site
cd my-site
pnpm install
By default, the project is created from the starter template. To start from another example, pass --template:
-
starter: the framework with a layout, components and App Engine deploy scripts. -
minimal: the smallest possible project. -
cms: the framework with the CMS set up. -
blog: a localized blog that uses the CMS. -
basepath: a site served from a sub-path, e.g./about/.
pnpm create @blinkk/root --template=cms my-site
Run the dev server
Start the dev server from the project directory:
pnpm dev
The site runs at http://localhost:4007, and pages reload as you edit them. To use a different port, set the PORT environment variable.
Set up the CMS
The CMS runs inside your site at /cms/ and stores content in Firestore. If you started from the cms or blog template, the plugin is already installed, but the template points at a sample Firebase project, so follow the steps below to connect your own.
1. Create a Firebase project
-
Create a project in the Firebase console, or add Firebase to an existing Google Cloud project.
-
Create a Firestore database in Native mode.
-
Under Authentication, enable the Google sign-in provider. If your site will serve on a custom domain, add it to the authorized domains.
-
Under Project settings, register a web app and copy its
firebaseConfigvalues for the next step.
2. Add the CMS plugin
Install the CMS and the Firebase Admin SDK (skip this if you used the cms template):
pnpm add @blinkk/root-cms firebase-admin
Then add the plugin to root.config.ts:
// @/root.config.ts
import {defineConfig} from '@blinkk/root';
import {cmsPlugin} from '@blinkk/root-cms/plugin';
export default defineConfig({
domain: 'https://example.com',
server: {
// Signs the CMS session cookie. Generate a random value with:
// node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
sessionCookieSecret: process.env.SESSION_COOKIE_SECRET,
},
plugins: [
cmsPlugin({
id: 'my-site',
name: 'My Site',
// From Project settings > Your apps in the Firebase console.
firebaseConfig: {
apiKey: '...',
authDomain: 'my-project.firebaseapp.com',
projectId: 'my-project',
storageBucket: 'my-project.appspot.com',
},
}),
],
});
The id namespaces your content in Firestore, so several sites can share one Firebase project. See CMS configuration for every option.
3. Sign in to Google Cloud
The server reads and writes Firestore with application default credentials. For local development, sign in with the gcloud CLI:
gcloud auth login
gcloud auth application-default login
4. Secure the database and add yourself as an admin
Run init-firebase to apply the CMS's Firestore security rules and give your account the ADMIN role:
pnpm exec root-cms init-firebase --admin=you@example.com
This replaces any existing Firestore rules in the project. To review the rules or apply them by hand, see Security rules.
5. Add a collection
Content models are .schema.ts files in the collections/ folder. Each file defines a collection of docs that share the same fields:
// @/collections/Pages.schema.ts
import {schema} from '@blinkk/root-cms';
export default schema.collection({
name: 'Pages',
description: 'Landing pages',
url: '/[...slug]',
preview: {
title: 'meta.title',
image: 'meta.image',
},
fields: [
schema.object({
id: 'meta',
label: 'Meta',
fields: [
schema.string({id: 'title', label: 'Title', translate: true}),
schema.string({
id: 'description',
label: 'Description',
translate: true,
variant: 'textarea',
}),
schema.image({id: 'image', label: 'Image'}),
],
}),
schema.object({
id: 'content',
label: 'Content',
fields: [
schema.richtext({id: 'body', label: 'Body', translate: true}),
],
}),
],
});
Generate TypeScript types for your schemas, then restart the dev server and open http://localhost:4007/cms/ to create your first doc:
pnpm exec root-cms generate-types
Render CMS content
Routes read content with RootCMSClient. This route renders any doc in the Pages collection at its slug, and shows drafts when the URL has ?preview=true:
// @/routes/[[...slug]].tsx
import {Handler, HandlerContext} from '@blinkk/root';
import {RootCMSClient} from '@blinkk/root-cms';
import {RichText} from '@blinkk/root-cms/richtext';
import {PagesDoc} from '@/root-cms';
interface PageProps {
doc: PagesDoc;
}
export default function Page(props: PageProps) {
const fields = props.doc.fields || {};
return (
<main>
<h1>{fields.meta?.title}</h1>
<RichText data={fields.content?.body} />
</main>
);
}
export const handle: Handler = async (req) => {
const ctx = req.handlerContext as HandlerContext<PageProps>;
const slug = ctx.params.slug || 'index';
// Editors preview drafts by adding ?preview=true to the URL.
const mode = String(req.query.preview) === 'true' ? 'draft' : 'published';
const cmsClient = new RootCMSClient(req.rootConfig);
const doc = await cmsClient.getDoc<PagesDoc>('Pages', slug, {mode});
if (!doc) {
return ctx.render404();
}
return ctx.render({doc});
};
Learn more in Schemas and Data fetching.
Share environment variables
API keys and other secrets belong in a .env file, which shouldn't be committed. To share them with your team, root secrets stores them in Google Cloud Secret Manager and keeps each developer's .env in sync:
# Create a manifest (.root.secrets.json) and commit it.
pnpm exec root secrets init --gcp-project=my-project --gsm-key=my-site-env
# Upload the values in your .env file.
pnpm exec root secrets push
# Teammates download them into their own .env.
pnpm exec root secrets sync
Once a project has a manifest, root dev syncs the secrets when it starts. See the CLI reference for every command.
Work with AI agents
Root.js includes skills that teach AI coding agents, such as Claude Code, how to read and edit your CMS content from the command line. Install them into your project:
pnpm exec root-cms skill.install
With the skills installed, an agent can look up docs with root-cms client.call, and propose content changes as a YAML file that your team reviews in a pull request before it's applied with root-cms proposal.apply.
Next steps
-
Project structure: where routes, components and content models live.
-
Routes: file-based routing, data fetching and SSR.
-
Deployment: ship your site as static HTML or with a server.
-
Schemas: model your content.