Documentation

Getting started

Getting started with unite cms, install unite cms using composer and basic configuration.

A short introduction

unite cms is a decoupled content management system that allows you to manage all kind of content in one application. You can login into unite cms and configure any kind of data / user / settings types. Via the admin interface you and your content editors can manage content according to the defined types. unite cms does not provide any frontend rendering layer, so the only way to access the content is via a GraphQL API.

The big idea behind unite cms is to have a single system just for content management. All other features that are part of many state-of-the-art CMS but have nothing to do with content management (search server, image processing, template rendering etc.) are not part of unite cms and we are not planing to implement them in the future. Because of this, unite cms is designed to be integrated with other services.

One example would be to have a small website application, written in Symfony, that fetches content from unite cms and uses imgix.com for resizing images, that are stored in a S3 storage.

By focusing on content management only, we can put all our effort in the content management architecture and the content editor experience. Still, our SasS platform offers all features you would expect from a state-of-the-art CMS by integrating 3rd party open source services like minio.io.

On Premises Installation

Once our SaaS service (unitecms.io) is ready (summer 2018), you can run unite cms in our cloud, hosted in Vienna (Austria). Since the application is published under an open source license, you can always run it on your own infrastructure. unite cms is based on Symfony 4 and vue.js, the only server dependencies are PHP 7.1 and MySQL 5.7.9

Installation for development
composer create-project unite-cms/standard unitecms --stability dev
cd unitecms
bin/console doctrine:schema:update --force

# run the development server
bin/console serve:run

On composer install (and update) you will be asked to set all required environment (dotenv) variables.

Installation for production
composer create-project unite-cms/standard unitecms --stability dev --no-dev --no-scripts
cd unitecms

bin/console assets:install --env=prod
bin/console doctrine:schema:update --force --env=prod
bin/console cache:clear --env=prod

In order to run unite cms, all required settings must be available as environment variables. It is recommended to set this variables at a web server level.

After unite cms was successfully installed you can create a Platform Administrator and your first organization.

bin/console unite:user:create
bin/console unite:organization:create

Now you can login into unite cms and start using the cms.

Basic Concepts

Learn the basic architecture of unite cms and see how you can define your schema types.

Organizations

Organizations in unite cms are the top level entities that contain all users and domains. On our SaaS platform, each subdomain belongs to one organization (your-org-name.unitecms.io). Multiple organizations are completely separate and will never share any informations. When you create an account on unitecms.io, you will be asked to create your first organization. You can always create new organizations or get invited to other organizations. One organization contains the following entities: 

User

Each user of an organization can login into this organization but cannot access any domains unless he_she is a member of the domain. Users can become organization admins. Organization admins can invite new users, assign the administration role to existing users, create api keys, create new domains and are allowed to access all domains.

Note: Because organization admins can bypass all security checks you should use an user account with organization admin privileges only for administrative tasks and use normal users for content editing.

API Key

Organization admins can create API keys. By using this keys, clients can access the GraphQL API. API keys can only access domains, they are members of. One API key can be member of multiple domains, which is especially necessary if you want to create a reference between different domains.

Domain

One organization can hold an unlimited number of domains, where all of the content management is happening.

Domains

A domain in unite cms groups together related content and setting types.

An example of a domain could be "Website" and contains "Pages", "Page Settings" and "Blog Articles". Another example would be a "Employees" domain that contains a list of all employees and vacation planing.

Domains can have reference fields to other domains which allows you to create very powerful content structures.

Domain configuration is done by creating or updating its JSON configuration. Example of a very simple domain: 

{
    "identifier": "website",
    "title": "Website",
    "content_types": [
        {
            "title": "Pages",
            "identifier": "pages",
            "fields": [
              { "title": "Headline", "identifier": "headline", "type": "text" }
            ]
        }
    ]
}

This domain would provide one content type "Website" and no setting types. By default, each domain contains an "Editor" and "Viewer" member type. Editors have the permission to manage all content and settings, viewers can only read it.

For the website example, you could create an API key and add it as a Viewer Domain Member to the website domain. Now your website can do an GraphQL API request to the unite cms api endpoint of your domain and gets all page headlines to display them:

# https://{YOUR-ORG}.unitecms.io/website/api?token={TOKEN}
query {
    findPages {
        result {
            headline
        }
    }
}

Each domain can define an infinit number of content_types, setting_types and at least one domain_member_types. For each of them you can define an infinit number of fields. Domains also contains the permissions config, which is configured to allow all members to view the domain but only the organization administrator to update the domain schema. To find out more about defining permissions in unite cms, see the topic Permissions.

Content Types

Each new content type in unite cms comes with no fields and one view ("all") per default. By adding fields, you can define all kind of content schemas (for example news articles, web pages, invoices or products). By changing the settings of the "all" view or by adding additional views, you can define different management collections of you content. For example a content type "Webpages" could have a default view that allows sorting the content for displaying a navigation list and a second view that shows all pages that are marked for internal review. For more information about all available views, see chapter "Views".

To improve the UX for your content editors, you can set an optional icon from the icon set, unite cms is using (Feather).

{
    ...,
    "content_types": [
        {
            "title": "Webpages",
            "identifier": "pages",
            "icon": "file",
            "fields": [
              { "title": "Headline", "identifier": "headline", "type": "text" },
              { "title": "Needs review", "identifier": "needs_review", "type": "checkbox" },
              { "title": "Position", "identifier": "position", "type": "sortindex" }
            ]
        }
    ],
    "views": [
        {
           "title": "All",
           "identifier": "all",
           "type": "sortable",
           "settings": {
               "sort_field": "position"
           }
       },
       {
           "title": "Needs review",
           "identifier": "review",
           "type": "table",
           "settings": { "filter": { "field": "needs_review", "operator": "=", "value": "1" }
       }
    ]
}

For each content type you can define permissions, using a very powerful expression engine that is described in chapter "Permissions".

For each content type, the GraphQL API of your domain defines a find{CONTENT_TYPE} query object as well as an create{CONTENT_TYPE} and update{CONTENT_TYPE} mutation object. If you want to query content from multiple content types together, you can use the generic find query object.

# https://{YOUR-ORG}.unitecms.io/website/api?token={TOKEN}
query {
 find(types: ["pages"]) {
   result {
     ... on PagesContent {
      headline
    }
   }
 }

 findPages(
    filter: { field: "needs_review", operator: "!=", value: "1" },
    sort: {field: "position", order: "ASC"}
  ) {
   result {
     headline
   }
 }
}

To learn more about the GraphQL API, see the chapter "GraphQL API" or use a client like GraphiQL to explore API of your domain.

Setting Types

Setting types are very similar to content types, however in contrast to content types, there is exactly one setting instance for each setting type. Therefore there are no views and only a "view"  and an "update" permission, but no "create" and "delete".

"setting_types": [
  {
     "title": "Frontpage",
     "identifier": "frontpage",
     "fields": [
       {
         "title": "Header content",
         "identifier": "header",
         "type": "wysiwyg",
         "settings": {
           "heading": ["p", "h1", "h2", "h3", "h4", "h5"]
         }
       }
     ]
  }
]

Like for content types, setting types can be queried using the GraphQL API (Note: Mutations are not implement yet for setting types):

FrontpageSetting {
   header
 }

To learn more about the GraphQL API, see the chapter "GraphQL API" or use a client like GraphiQL to explore API of your domain.

Domain member types

Each domain comes with two domain member types per default: "Viewer" and "Editor". You can modify or delete this types and add any number of types, however there must be at least one domain member type for each domain. 

Each API key and each CMS user of your organization must become a member of your domain in order to get access to it. So even if you set a content type read permission to "true" (= always grant access) for example, only members of the domain can actually see the content. 

Domain member types are similar to content types and can also define any number of fields. At the moment this fields can only be used to save content and inside a permission check expression, in the future we might implement an API endpoint to allow you to query and create members.

"domain_member_types": [
   {
     "title": "Editor",
     "identifier": "editor",
     "domain_member_label": "{accessor}",
     "fields": []
   },
   {
     "title": "Viewer",
     "identifier": "viewer",
     "domain_member_label": "{accessor} {department}",
     "fields": [
         { "title": "Department", "identifier": "department", "type": "choice", "settings": {...} }
     ]
   }
 ]

The domain_member_label is used whenever a domain member needs to get displayed (for example the title on the member update screen). Per default it shows the name of the member accessor (API key name or user name).

Defining Permissions

unite cms allows you to define permissions for accessing domains, content types and setting types by writing a short expression statement. This expression statement gets compiled and evaluated and must return true or false to allow or deny access.

We are using the Symfony ExpressionLanguage component, so you can use all common syntax elements like "==", "!=" and arithmetic operators (+, -, % etc.). For a full reference see the Symfony ExpressionLanguage syntax docs

Inside an expression you can access the current domain member object and for content and setting types the current content object:

member: {
    type: "editor",
    accessor: {
        name: "User name or API key name",
        id: "XXX-YYY-ZZZ",
        type: "api_key"
    }
    data: { ... }
}

content: {
    locale: "en",
    data: { ... }
}

By using properties (and especially the data property that holds all of the defined fields) of this two objects you can define very powerful expressions to check permission for a user and a content object (or a domain). Here are some examples:

Domain

"permissions": {
    "view domain": "true",
    "update domain": "member.type == \"editor\" or member.data.can_edit_domains == true"
}

Content Type / Setting Type

"permissions": {
    "view content": "member.data.project_admin_for == content.project",
    "list content": "member.type == \"editor\" or member.accessor.type == \"api_key\"",
    "create content": "member.type == \"editor\"",
    "update content": "member.accessor.id == content.my_author_field",
    "delete content": "member.data.manager_since < content.date_of_receipt"
}

Since we are just checking the result of the expression, you can allow access for all domain members with "true" and deny for all members with "false".

Note: Permission expressions are only used to check permission for domain members. So even if you define a "true" permission, only members of the current domain are allowed to access the resource. Therefore you cannot create an anonymous public API endpoint, that can be accessed without API Key, however if you want to deliver public webpages for example you can create an API Key that must not be absolutely private.

Organization admins are allowed to manage all domains and all content for their organizations, independently from defined permissions. Because of this, the default permissions for domains are: view: "true" and update: "false" which means that all domain members can view the domain but only organization admins are allowed to update them.

GraphQL API

The heart of unite cms is its GraphQL API. It allows you to query and manipulate your content and settings. GraphQL APIs are very powerful and allows you to get exactly the fields you need from exactly the content items you want to query. If you are new to GraphQL you should read the introduction from graphql.org before you continue.

Each domain comes with its own API endpoint: # https://{ORG}.unitecms.io/{DOMAIN}/api. In order to access the api, you need to provide an authentication token from an API Key that is a member of the domain. You should always use the HTTP Authentication head field to send the token, but it is also possible to add a GET token query parameter to the API url. 

The unite cms API has the following structure, that you can easily explore using one of the GraphQL clients (for example GraphiQL):

query {  
 find {
   total
   page
   result { ... }
 }
 findPages {
   total
   page
   result { ... }
 }
 WebsiteSetting { ... }
}
mutation {
 createPage {
   ...
 }
 updatePage {
   ...
 }
}

You can use the generic find query object to get results from one or multiple content types. This also allows you to combine multiple content types in one query. In the following example, assumed that there is a "news" and an "events" content type, you would get the 5 newest content items from both types. With to separate find queries you would get the 5 newest news AND the 5 newest events.

query {  
 find(
   types: ["news", "events"],
   limit: 5,
   sort:  { field: "created", order: "DESC" },
   filter: { field: "published", operator: "=", value:"1" }
 ) {
   total,
   result {
     id,
     type,
     created,
     updated,
     
     ... on NewsContent {
       headline
     }
     
     ... on EventsContent {
       location
     }
   }
 }
}

Beside the generic find query object, unite cms creates one find object for each content type ("news" => "findNews", "events" => "findEvents") that has the same filters and structure like find but no types argument. Use this objects to get content from a single content type:

query {
  findEvents {
    result {
      id,
      location
    }
}

For each setting of your domain, there will be one setting query object ("website" => "WebsiteSetting") you can use to get the setting fields. Note: At the moment you can only read settings but not write them. This will be implemented in a future release!

query {
  WebsiteSettings {
    title,
    description,
    footer_text
  }
}

The current version of the API (0.5) allows you to create and update content items, but not to delete them: 

mutation {
 createNews(data: { headline: "Hello World"}) {
   id,
   headline
 }

 updateNews(id: "XXX-YYY-ZZZ", data: {headline: "Updated headline"}) {
   id,
   headline
 }
}

Filtering

All query find objects allows you to reduce the result, using an optional filter input parameter. The filter input has the following basic structure: 

filter: {
  field: "headline",
  operator: "LIKE",
  value: "%Hello%"
}

At the moment the following operators are supported: "=", "<>", "<", "<=", ">", ">=", "IS NULL", "IS NOT NULL", "LIKE". To combine multiple filters, you can create a nested input element using the AND or OR fields.

filter: {
   AND: [
     { field: "published", operator: "=", value:"1"},
     {
       OR: [
         { field: "is_very_important", operator: "=", value:"
         { field: "created", operator: ">", value:"1530611415"}
       ]
     }
   ]
 }

Pagination

All query find objects allows you to limit the result using a limit and page input field. To get the first 10 items, you could do: 

find(limit: 10, page: 1) { ... }

To get the next 10 items, use the page parameter:

find(limit: 10, page: 2) { ... }

Fields

A reference of all fields that are part of unite cms core. You can use this fields for content types, setting types and domain member types.

Checkbox

A checkbox field that can be true or false. This field type has no configureable settings.

{
    ...,
    "type": "checkbox"
}

Choice

A choice field type that allows to select on value of a predefined set of values using a HTML select element. A "choices" setting is required.

{
    ..., 
    "type": "choice",
    "settings": {
        "choices": {
            "red": "Red color",
            "green": "Green color",
            "blue": "Blue color"
        }
    }
}

Date

Renders a HTML5 date input element. This field has no settings.

{
    ...,
    "type": "date"
}

DateTime

Renders a HTML5 date-time input element. This field has no settings.

{
    ...,
    "type": "datetime"
}

Email

Renders a HTML5 email input element. Input will be validated to be a valid email address. This field has no settings.

{
    ...,
    "type": "email"
}

Integer

Renders an input element that accepts integer numbers. This field has no settings.

{
    ...,
    "type": "integer"
}

Number

Renders an input element that accepts any numeric input. This field has no settings.

{
    ...,
    "type": "number"
}

Phone

Renders a HTML 5 tel input element. Note: The phone field to not validate any input. This field has no settings.

{
    ...,
    "type": "phone"
}

Range

Renders a slider input element that allows to select one value between min and max. The default settings are:

{
    ...,
    "type": "range",
    "settings": {
        "min": "0",
        "max": "100,
        "step": 1
    }
}

Reference

This field holds a reference to an content element. The content element can be of this or any other domain in this organization. Note: Make sure, that the content editor is allows to access the referenced domain and content type, otherwise he_she will not be able to fill out this field.

{
    "title": "Related page",
    "identifier": "related_page",
    "type": "reference",
    "settings": {
        "domain": "website",
        "content_type": "page",
        "view": "all",
        "content_label": "{headline}"
    }
}

When using the GraphQL API, referenced content will be resolved automatically allowing you to selected nested fields of the referenced content:

{
    query {
        findCategories {
            result {
                related_page {
                    headline
                }
            }
        }
    }
}

The domain and content_type settings are required, view defaults to all, if content_label is left empty, "content type #{id}" will be used.

Sort Index

A sort index is a special field that stores an inter value (the sort index) and makes sure that all sort indexes of this content type are in sync if a new content item was added or removed and when a sort index was updated. 

{
    ...,
    "type": "sortindex"
}

For example you have the following pages: 

[
  { title: "Start", sort_index: 0},
  { title: "About us", sort_index: 1},
  { title: "Contact", sort_index: 2}
]

Now, if you remove the "About us" page, the sort indexes will automatically be updated:

[
  { title: "Start", sort_index: 0},
  { title: "Contact", sort_index: 1}
]

Sort indexes are needed if you want to have a sortable view, however you can also use them for other purposes.

Textarea

Renders a multi-row textarea. Allows you to set an optional rows setting to configure the textarea HTML rows attribute.

{
    "type": "textarea",
    "settings": {
        "rows": 2
    }
}

Text

Renders a text input field.

{
    "type": "text"
}

Collection

Collection fields allows you to create a repeatable container of one or multiple subfields. They can be used to allow multiple values for one field or to create complex content structures.

The following example would create an input element that allows to add 1-5 tags (min_rows and max_rows are optional).

{
  "title": "Tags",
  "identifier: "tags",
  "type": "collection",
  "settings": {
    "min_rows" 1,
    "max_rows": 5,
    "fields": [
      { "type": "text", "identifier": "name", "title": "Name" }
    ]
  }
}

Using the API, this collection field resolves in a nested GraphQL object:

{
  findNews {
    result {
      tags {
        name
      }
    }
  }
}

Collection fields can also be nested to allow to have multiple levels of collections inside each other:

{
  "title": "Content Blocks",
  "identifier: "content_blocks",
  "type": "collection",
  "settings": {
    "fields": [
      { "type": "text", "identifier": "headline", "title": "Headline" },
      { "type": "collection", "identifier": "blocks", "title": "Blocks", "settings": {
        "fields": [
          { "type": "wysiwyg", "identifier: "content", "title": "Content" }
        ]
      } }
    ]
  }
}

File

unite cms does not manage any files directly but provides a file field that stores a reference using any s3 compatible API (Amazon, minio.io etc.). The file field renders an upload input element that allows the content editors to upload files directly to the s3 compatible server, using a presgined upload url. The file filed also reacts on content delete and update events and tries to delete files, that are not used anymore. In order to use the file field, set the required bucket and optional file_type settings:

{
  "type": "file",
  "settings": {
    "bucket": {
      "endpoint": "S3 Endpoint",
      "bucket": "S3 Bucket",
      "path": "myfiles"
    },
    "file_types": "txt,pdf,doc"
  }
}

A typical Amazon S3 configuration would be: 

{
  "bucket": { 
    "endpoint": "https://s3.amazonaws.com",
     "key": "S3 KEY", 
     "secret": "S3 SECRET", 
     "bucket": "my_bucket"
  }
}

A typical minio.io configuration would be:

{
  "bucket": { 
    "endpoint": "https://example.com:9000",
     "key": "S3 KEY", 
     "secret": "S3 SECRET", 
     "bucket": "my_bucket"
  }
}

Image

The image type is an extension of the file input type that renders a thumbnail preview next to the upload input type and limits file_type to "png,gif,jpeg,jpg":

{
  "type": "image",
  "settings": {
    "bucket": { ... },
    "thumbnail_url": "your_thumbnailing_service.com/{endpoint}/{id}/{name}"
  }
}

The optional thumbnail url allows you to add a link to the file directly or to any thumbnailing service. For a description of the bucket setting, please see the file type documentation.

WYSIWYG Editor

The wysiwyg field renders a ckeditor5 "Classic" editor. You can set toolbar items and heading elements, according to the ckeditor documentation: 

{
  "type": "wysiwyg",
  "settings": {
    "toolbar": ["bold", "italic", "|", "link", "|", "bulletedList", "numberedList", "|", "blockQuote"],
    "heading": ["p", "h1", "h2", "h3", "h4", "h5", "h6", "code"]
  }
}

Views

For each Content Type you can define one or multiple views, that define the management view of content. The default view is a table, that just displays field columns and action buttons for each content row. Another example would be a drag and drop sort view, that allows the user to sort content directly in the view. At the moment there are just two basic views available, however are planing to implement many different views (Media gallery, Kanban board, etc.) in the future!

Table

The following example defines a Table view for the Webpages Content Type. 

The following Settings are provided:

sort_field (optional): to sort by a field (default: updated time)
sort_asc (optional): to sort the results ascending  (boolean, default: false)
columns (optional): to limit the columns displayed in the view
filter (optional): again a filter possibility (see Section Filtering in GraphQL API), in this case we are filtering all results for subline = "My Subline"

{
   ...,
   "content_types": [
       {
           "title": "Webpages",
           "identifier": "pages",
           "icon": "file",
           "fields": [
             { "title": "Headline", "identifier": "headline", "type": "text" },
             { "title": "Subline", "identifier": "subline", "type": "text" },
             { "title": "Description", "identifier": "description", "type": "text" },
             { "title": "Position", "identifier": "position", "type": "sortindex" }
           ]
       }
   ],
   "views": [
      {
          "title": "My Webpages Table",
          "identifier": "mytableview",
          "type": "table",
          "settings": {
            "sort_field": "position",
            "sort_asc": true,
            "columns": {
               "headline": "headline",
               "subline": "Subline"
            },
            "filter": {
               "field": "subline",
               "operator": "=",
               "value": "My Subline"
            }
          }
      }
   ]
}

Sortable

The following example defines a Sort view for the Webpages Content Type, which will display a nice drag & drop interface for sorting the content.

The following Settings are provided:

sort_field: (mandatory): to sort by a field
columns: (optional): to limit the columns displayed in the view

{
   ...,
   "content_types": [
       {
           "title": "Webpages",
           "identifier": "pages",
           "icon": "file",
           "fields": [
             { "title": "Headline", "identifier": "headline", "type": "text" },
             { "title": "Subline", "identifier": "subline", "type": "text" },
             { "title": "Description", "identifier": "description", "type": "text" },
             { "title": "Position", "identifier": "position", "type": "sortindex" }
           ]
       }
   ],
   "views": [
      {
          "title": "My Webpages Sortable View",
          "identifier": "mytableview",
          "type": "sortable",
          "settings": {
            "sort_field": "position",
            "columns": {
               "headline": "headline",
               "subline": "Subline"
            }
          }
      },
      {
          "title": "My Webpages Table",
          "identifier": "mytableview",
          "type": "table",
          ....
      }
   ]
}