Skip to main content
CMS

Notifications and integrations

The CMS records every change as an action, which your server can react to, e.g. to email a team when a page is published. It can also send strings to an external translation service and import the results.

Actions

Each action has a name like doc.publish, doc.save, release.publish or doc.comment.add, the email of the user who performed it (by, or system for scheduled jobs), a timestamp and metadata such as the docId. Actions are stored at Projects/<id>/ActionLogs in Firestore and listed in the CMS's action log.

The onAction option is called on the server with every action. It isn't awaited, so use it for quick, fire-and-forget work:

ts
// @/root.config.ts

cmsPlugin({
  // ...
  onAction: (action) => {
    if (action.action === 'doc.publish') {
      console.log(`${action.by} published ${action.metadata?.docId}`);
    }
  },
}),

Notification services

For notifications, register services with the notifications option. Each service has an id, a label and an async onAction(ctx, action) handler, and each one decides for itself which actions to handle. ctx has the rootConfig, a cmsClient and the user who performed the action. For example, to post to a Slack webhook:

ts
// @/root.config.ts

cmsPlugin({
  // ...
  notifications: [
    {
      id: 'slack',
      label: 'Slack',
      onAction: async (ctx, action) => {
        if (action.action !== 'doc.publish') {
          return;
        }
        await fetch(process.env.SLACK_WEBHOOK_URL!, {
          method: 'POST',
          headers: {'Content-Type': 'application/json'},
          body: JSON.stringify({
            text: `${action.by} published ${action.metadata?.docId}`,
          }),
        });
        return {status: 'success'};
      },
    },
  ],
}),

A handler can return {status: 'success' | 'info' | 'error', message} to describe what it did. Errors are logged and don't affect the action or the other services.

Email notifications

emailNotifications() is a built-in service that emails a list of people when matching actions happen. actions takes patterns where * matches anything, and to can be a list or a function that returns the recipients for an action:

ts
// @/root.config.ts

import {
  cmsPlugin,
  commentEmailNotifications,
  emailNotifications,
} from '@blinkk/root-cms/plugin';

cmsPlugin({
  // ...
  notifications: [
    emailNotifications({
      actions: ['doc.publish', 'release.*'],
      to: ['web-team@example.com'],
      template: {
        subject: '[My Site] {action} by {by}',
        body: '{by} ran {action} on {metadata.docId}.',
      },
      emailService: 'https://my-email-service.appspot.com',
    }),
    commentEmailNotifications({
      emailService: 'https://my-email-service.appspot.com',
    }),
  ],
}),

Templates fill {placeholder} values from the action, including nested ones like {metadata.docId}, and can set subject, a plain-text body and html. Without a template, a default email describes the action. For full control, pass a function that returns the email. filter can skip actions (e.g. ones by a bot account), and transformData changes the data the template sees.

commentEmailNotifications() emails the people @mentioned in a field comment and everyone who has already commented in the thread, when a comment is added, resolved or reopened. Links in the email point to rootConfig.domain unless you set cmsUrl.

Delivering email

Emails are queued in Firestore at Projects/<id>/Emails and sent by the Root.js email service, a small App Engine app (apps/root-services in the Root.js repo) that reads the queue and sends with the App Engine Mail API. Deploy it to App Engine in the Google Cloud project that has your Firestore database, and set emailService to its URL so new emails are sent right away.

Emails are sent from noreply@<project-id>.appspotmail.com by default. A different from address must be an authorized sender for the Mail API. Set expireAfterMinutes to skip emails that couldn't be sent in time instead of sending them late.

Your own server code can send email through the same queue with cmsClient.sendEmail({to, subject, body, htmlBody}).

Translation services

To connect a translation platform like Crowdin or Smartling, register a service with the translations option. It appears in the Import and Export menus of a doc's localization modal. onExport receives the doc's strings and their current translations, and onImport returns translated rows to merge into the CMS:

ts
// @/root.config.ts

cmsPlugin({
  // ...
  translations: [
    {
      id: 'my-tms',
      label: 'My TMS',
      onExport: async (ctx, rows) => {
        // rows: [{source, translations: {de: '...', fr: '...'}}, ...]
        const job = await myTms.upload(ctx.docId, rows);
        return {
          status: 'success',
          message: `Sent ${rows.length} strings.`,
          link: {url: job.url, label: 'Open in My TMS'},
        };
      },
      onImport: async (ctx) => {
        return await myTms.download(ctx.docId, ctx.translationLanguages);
      },
    },
  ],
}),

ctx has the doc's docId, the project's locales and its translationLanguages (the keys used in translations; see i18n.translationLanguages in root.config.ts). Either handler can return a title, message, link and status to show in a notification, and onImport can return one of these instead of rows when there's nothing to import.

excludeLocalesFromTranslations leaves locales out of every import and export, including CSV and Google Sheets. It takes patterns like ['ALL_*'], which is useful for pseudo-locales that are never translated.

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