@tinalabs/tinacms-studio
TypeScript icon, indicating that this package has built-in type declarations

0.1.2 • Public • Published

tinacms-studio

Architecture

Stand-alone Admin

Tina Studio enables a stand-alone admin experience by managing the creation and setup of the TinaCMS React Microfrontend for the user using a vanilla JS API, and providing web standards API(s) for selecting entities to edit.

We've prototyped doing this by adding a mount method to the CMS and using basic query param routing:

const tinaConfig = require('./tina.config')
const cms = new TinaStudio(tinaConfig)

cms.mount({ routing: true })

Mount takes the following configuration options:

type MountOptions = {
  /* DOM element to mount the CMS to. Will create an element if omitted */
  el?: Element;
  
  /* Enable query param routing. Defaults to false */
  routing?: boolean;

  /* Render a client-side app in the admin using JSX */
  render?: (window?: Window, document?: Document) => ReactNode;
}

Routing is explained in Entities.

Browser Bundle

Currently TinaCMS is built as a UMD library. This means it cannot be used in the browser as-is. To work around this, we forked @tinacms/scripts as tinacms-plugin-utils and updated it to have more control over console output using ink.js and added support for building a UMD bundle, IIFE (Immediately Invoked Function Expression) browser bundle that mounts named exports to the window, and a ES module if desired for modern browsers, and packages direct dependencies inside it at build time, enabling usage in the browser.

This package is built with this tool, and exports the following:

  • dist/index.js: a UMD bundle that extends TinaCMS with prototypes of the functionality outlined in this RFC
  • dist/index.browser.js: an IIFE that adds TinaCMS and Form under the tina namespace

This allows us to include the browser bundle inside tools like Hugo and 11ty and use Tina off of the window:

<script src="https://unpkg.com/browse/react@17.0.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/browse/react-dom@17.0.1/umd/react-dom.production.min.js"></script>
<script src="/tina-studio/index.browser.js"></script>
<script>
  const { TinaCMS, Form } = tina || window.tina;

  const cms = new TinaCMS({ ... })
  const form = new Form({ ... })

  cms.plugins.add(form)
  cms.mount({ routing: true })
</script>

Entities

Entities make TinaCMS aware of the content models of your content sources, such as Tina Cloud, Github, or other CMSes and databases.

Configuration

The following options are available for entities:

type EntityOptions {
  /** Provide a unique name/id for the entity */
  name: string;
  
  /** Single for a single document, list for an array, or a graph representing a hierarchy */
  type: "single" | "list" | "hierarchy"
  
  /** The schema/base form template for this entity */
  template: FormOptions<EntityShape> | ((record: EntityRecord, cms: TinaStudio) => FormOptions<EntityShape>)

  /** Provide the records to include, or a function that resolves them */
  include: EntityRecord | EntityRecord[] | (() => EntityRecord | EntityRecord[]);

  /** Provide instructions for exlcuding the records returned by `include` */
  exclude?: (record: EntityRecord, cms: TinaStudio) => boolean

  /** Override behaviour for finding a record by its id. Default behaviour is to match an `_id` property. */
  find?: (id: string, record: EntityRecord) => boolean

  /** Instructions for previewing this entity. Optional. */
  preview?: Preview | ((record: EntityRecord, cms: TinaStudio) => Promise<Preview>
}

You can add entities to the CMS config when creating the CMS:

const homepage = require('./content/homepage.json')

const cms = new TinaStudio({
  entities: [
    {
      name: "homepage",
      type: "single",
      include: homepage
    }
  ]
})

Or you can add them on-the-fly:

cms.api.github.getFile("./homepage.json")
  .then(data => {
    cms.entities.add({
        name: "homepage",
      type: "single",
      include: homepage  
    }))
  })
  .catch(error => console.error(error))

Using Entities

Entities can be queried from the CMS after adding them:

const postEntity = cms.entities.find('post');

Which returns the following:

type EntityState = {
  __type: "entity";
  name: string;
  
  /** Indicates the type of entity */
  type: "single" | "list" | "hierarchy";

  /** The base form template */
  template: FormOptions<EntityShape>;

  /** Returns all records */
  all: () => Type extends "single" ? Promise<EntityRecord> : 

  /** Find a specific record by id */
  find: (id: string) => Promise<EntityRecord | undefined>

  /** Fetch the preview for a given record */
  preview?: (id: string) => void
}

You can request all records for an entity:

const postEntity = cms.entities.find('post');
const posts = postEntity.all()
  .then(posts => console.log(posts));

You can also requst a specific record by id:

const postEntity = cms.entities.find('post');
const post = postEntitiy.find('example-id')
  .then(post => console.log(post));

Records also have their form template and a prepopulated form with their values:

const postEntity = cms.entities.find('post');
const post = postEntitiy.find('example-id')
  .then(post => {
    const { _template, _form } = post;
    const customForm = new Form({ 
      ..._template,
      label: "Custom Post Form",
      onChange: (values) => console.log(values)
    })

    cms.plugins.add(_form)
    cms.plugins.add(customForm)
  });

Routing

Entities can be browsed by query param when routing is set to true in the cms.mount arguments.

  • A single entity can be loaded by name: /admin?entity=homepage
  • Multiple entities can be loaded by name: /admin?entities=header,homepage,footer
  • For entities that represent lists or hierarchies, and additional param is requiered matching the entities name, equal to the unqiue ID of the record: /admin?entities=post&post=example-id

This is a very rough example currently. Some considerations for a production use case:

  • Only one preview should be loaded at a time; we need to handle when two entities with preview logic are select
    • Perhaps "first entity wins" or throw an error?
  • The CMS config does not support async logic for including entity records; this API should be revised.

Previews

Create an integration with the TinaCMS to enable a preview frame to display loading messages while previews are being fetched, display new previews when they are ready, and display errors to editors when fetching fails with the information developers will need to debug.

Entity Previews

Preview configuration can be added to entity configurations:

cms.entities.add({
  name: "homepage",
  type: "single",
  include: homepage,
  preview: (record, cms) => {
    const src = `http://example.com/${record.slug}`;

    return {
      src
    }
  }
})

And then enabled in code:

const homepageEntity = cms.entities.find("homepage")

homepageEntity.preview()

Remote Rendering

For tools that only support build-time rendering of pages, you can get the URL for the page hosted elsewhere and render that page in the preview frame each time a form is saved:

import { createPreview, STUDIO_PREVIEW_LOADING } from 'tinacms-studio';

const useMyPreview = (recordId: string) = createPreview(cms, async () => {
  try {
    const src = `http://example.com/${recordId}`;

    cms.events.dispatch({
      type: STUDIO_PREVIEW_LOADING,
      message: "Fetching preview..."
    })

    return {
      src
    }
  } catch (error) {
    return {
      error
    }
  }
})

Server-side Rendering

For tools that only support server-side rendering of pages, you can get the HTML for the page and render that HTML in the preview frame each time a form is saved:

Fetch remote HTML for a record and render it in the iframe:

import { createPreview, STUDIO_PREVIEW_LOADING } from 'tinacms-studio';

const useMyPreview = createPreview(cms, async () => {
  try {
    const src = `http://example.com/api/${recordId}`;

    cms.events.dispatch({
      type: STUDIO_PREVIEW_LOADING,
      message: "Fetching preview..."
    })

    const res = await fetch(src);
    const html = await res.text();

    return {
      html
    }
  } catch (error) {
    return {
      error
    }
  }
})

Miscellenaeous

Due to time constraints, we did not have time to alter the existing TinaCMS UI (sidebar, toolbar) to support this experience. Due to this, we rapidly prototyped a new UI with a terrible name currently of fullscreen.

This UI provides a split-pane experience closer to the ForestryCMS UX, which allowed us to rapidly prototype outcomes.

We suggest the feedback from usability testing is explored to decide how to integrate this functionality and develop a better TinaCMS UI.

export type FullscreenOptions {
  overlay: boolean; // Render as a floating overlay instead of a fullscreen SPA
  toggle: boolean; // Render a Tina branded floating action button to enable the CMS
  mode: "editor" | "studio"
}

This is configured on the CMS configuration:

const cms = new TinaCMS({
  fullscreen: {
    overlay: false,
    toggle: true,
    mode: "editor"
  }
})

NOTE

The studio mode was intended to be a much more Forestry-like experience, with a "entity" browser and document list. We did not have time to complete this experiment.

The fullscreen UI works by applying CSS styles to other CMS UIs using fuzzy CSS selectors. This is very fragile and is not production ready.

Contributing

Prerequisites

You must have NodeJS LTS installed.

Installation

Install the project dependencies by running:

npm install

Development

Start a development server by running:

npm start

Production Builds

Build a production build by running:

npm run build

Readme

Keywords

none

Package Sidebar

Install

npm i @tinalabs/tinacms-studio

Weekly Downloads

1

Version

0.1.2

License

ISC

Unpacked Size

914 kB

Total Files

26

Last publish

Collaborators

  • scottgallant
  • jbevis
  • jpatters