Skip to main content

Root CMS Setup

Root.js supports an optional first-party plugin called Root CMS to give you a fully functional content editing and publishing experience right out of the box.

Root CMS uses Firebase under the hood for authentication, Firestore, cloud storage, and security rules.

Creating a Firebase project

Follow the steps below to set up a Firebase project for Root CMS:

  1. Create a new project using the Firebase console

  2. Navigate to the Firestore page and ensure the database is set to run in "Native mode"

  3. Within the Firestore project settings, create a new web application and take note of the "firestoreConfig" object for the next section (Install plugin)

  4. Navigate to the Authentication page and set up a Google Sign-in as a Provider

  5. If you plan to host your site on a custom domain, add your domain as a trusted domain on the Authentication page

If you plan to use Google Sheets related features of Root CMS, you will also need to set up a GAPI client id and enable the appropriate APIs:

  1. Create an OAuth Client ID (web application) and add it to the cms plugin config under the key gapi.clientId

  2. Enable the Google Sheets API

Install plugin

Install the npm package (and peer dependencies):

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

Next, add the plugin to your root.config.ts file:

ts
// @/root.config.ts

import {defineConfig} from '@blinkk/root';
import {cmsPlugin} from '@blinkk/root-cms/plugin';

export default defineConfig({
  domain: 'https://example.com',
  plugins: [
    cmsPlugin({
      id: 'project-id',
      name: 'My CMS Project',
      firebaseConfig: {
        /* add this value from the firebase console */
      },
    }),
  ],
});

When you start the server with "root dev", you should be able to access the CMS by navigating to http://localhost:4007/cms/.

Update Security Rules

Once your Firebase project is created and the cmsPlugin is installed, you'll need to lock down Firestore so that only designated users can read and write to the database. You can do that in one of two ways:

Run "root-cms init"

The "root-cms init" command takes care of updating your security rules and updates the share settings of your project to add yourself as an ADMIN.

bash
root-cms init --admin=you@example.com

Update security rules manually

You can also manually update your security rules by navigating to the Firestore section of the Firebase console and pasting the following security rules in:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }

    match /Projects/{project} {
      allow write:
        if isSignedIn() && userIsAdmin();
      allow read:
        if isSignedIn() && userCanRead();

      match /{collection}/{document=**} {
        allow write:
          if isSignedIn() && userCanWrite();
        allow read:
          if isSignedIn() && userCanRead();
      }

      function isSignedIn() {
        return request.auth != null;
      }

      function getRoles() {
        return get(/databases/$(database)/documents/Projects/$(project)).data.roles;
      }

      function userCanRead() {
        let roles = getRoles();
        let email = request.auth.token.email;
        let domain = '*@' + email.split('@')[1];
        return (roles[email] in ['ADMIN', 'EDITOR', 'VIEWER']) || (roles[domain] in ['ADMIN', 'EDITOR', 'VIEWER']);
      }

      function userCanWrite() {
        let roles = getRoles();
        let email = request.auth.token.email;
        let domain = '*@' + email.split('@')[1];
        return (roles[email] in ['ADMIN', 'EDITOR']) || (roles[domain] in ['ADMIN', 'EDITOR']);
      }

      function userIsAdmin() {
        let roles = getRoles();
        let email = request.auth.token.email;
        let domain = '*@' + email.split('@')[1];
        return (roles[email] == 'ADMIN') || (roles[domain] == 'ADMIN');
      }
    }
  }
}

Add a schema

Once Firebase is set up and the Root CMS plugin is installed, the next step is to define schemas for your content collections. This is done by adding .schema.ts files in a folder called collections/.

Example:

ts
// @/collections/BlogPosts.schema.ts

import {schema} from '@blinkk/root-cms';

export default schema.collection({
  name: 'BlogPosts',
  url: '/blog/[slug]',
  preview: {
    title: 'meta.title',
    image: 'meta.image',
  },
  fields: [
    schema.object({
      id: 'meta',
      label: 'Meta',
      fields: [
        schema.string({
          id: 'title',
          label: 'Title',
          translate: true,
          default: 'Lorem ipsum',
        }),
        schema.string({
          id: 'description',
          label: 'Description',
          help: 'Description for SEO and social shares.',
          translate: true,
          variant: 'textarea',
        }),
        schema.image({
          id: 'image',
          label: 'Image',
          help: 'Meta image for social shares. Recommended size: 1400x800.',
        }),
      ],
    }),
    schema.object({
      id: 'content',
      label: 'Content',
      fields: [
        schema.richtext({
          id: 'body',
          label: 'Blog content body',
          translate: true,
        }),
      ],
    }),
  ],
});

After creating the .schema.ts file, you should be able to see the collection appear in the CMS at http://localhost:4007/cms/. Navigate to the new collection to create your first content doc.

For more information about schemas, check out the Schemas guide.

Fetch data within a route

From within a route .tsx file, you can fetch data from a content doc using getStaticProps() (SSG) or handle() (SSR) to pass as a prop to your Page component.

Example:

tsx
// @/routes/blog/[slug].tsx

import {Handler, HandlerContext, Request} from '@blinkk/root';
import {RootCMSClient} from '@blinkk/root-cms';
import {BlogPostsDoc} from '@/root-cms';

interface PageProps {
  slug: string;
  doc: BlogPostsDoc;
}

export default function Page(props: PageProps) {
  return (
    <div>
      <h1>Slug: {props.slug}</h1>
      <pre><code>{JSON.stringify(props.doc)}</code></pre>
    </div>
  );
}

/** SSR handler. */
export const handle: Handler = async (req: Request) => {
  const ctx = req.handlerContext as HandlerContext<PageProps>;
  const slug = ctx.params.slug;
  const mode = String(req.query.preview) === 'true' ? 'draft' : 'published';

  const cmsClient = new RootCMSClient(req.rootConfig);
  const doc = await cmsClient.getDoc('BlogPosts', slug, {mode});
  if (!doc) {
    return ctx.render404();
  }
  const props: PageProps = {slug, doc};
  return ctx.render(props);
};

For more examples, check out the Data Fetching guide.

Set up cron jobs

A few Root CMS features (e.g. scheduled publishing, version history) require a cron job to hit the /cms/api/cron.run URL endpoint to work.

There are two ways to set up cron jobs on App Engine / Firebase / Google Cloud Platform:

Firebase Functions

If your app is deployed to Firebase Functions, you can simply export the "cron" function from the "@blinkk/root-cms/functions" package.

Example:

// @/index.ts

import {server} from '@blinkk/root/functions';
import {cron} from '@blinkk/root-cms/functions';

export const www = {
  server: server({
    mode: 'production',
  }),
  cron: cron(),
};

Google Cloud Scheduler

If your app runs on App Engine or Google Cloud Run (or any other hosting infrastructure), you can set up a cron job on Google Cloud Scheduler to call the /cms/api/cron.run endpoint on some interval.

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