Skip to main content
Get started

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.

sh
node --version  # v24 or later
corepack enable

Create a project

Scaffold a new project with create-root, then install its dependencies:

sh
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/.

sh
pnpm create @blinkk/root --template=cms my-site

Run the dev server

Start the dev server from the project directory:

sh
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

  1. Create a project in the Firebase console, or add Firebase to an existing Google Cloud project.

  2. Create a Firestore database in Native mode.

  3. Under Authentication, enable the Google sign-in provider. If your site will serve on a custom domain, add it to the authorized domains.

  4. Under Project settings, register a web app and copy its firebaseConfig values 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):

sh
pnpm add @blinkk/root-cms firebase-admin

Then add the plugin to root.config.ts:

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:

sh
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:

sh
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:

ts
// @/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:

sh
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:

tsx
// @/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:

sh
# 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:

sh
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

1
2
3
4
5
6
7
8
9
10
11
12
Breakpoint: