Tina Cloud is in public beta. Get Started
As we've trialed the GraphQL API and how it works with TinaCMS, it's become apparent that this is the primary path we want to carve out for users of Tina. We'll be moving the tina-graphq-gateway
APIs in to the tinacms
package to reflect its more central role in the Tina workflow. Other backend integrations will still be available to build out via the new @tinacms/toolkit
package. You can read more details on those changes here.
tinacms
is absorbing tina-graphql-gateway
As a result, upgrading to the new and improved GraphQL experience will require you to move to tinacms@0.50.0
and remove the tina-graphql-gateway
package entirely.
yarn add tinacms@latest
yarn remove tina-graphql-gateway
- import { useGraphqlForms } from 'tina-graphql-gateway'
+ import { useGraphqlForms } from 'tinacms'
Heads up,
useGraphqlForms
is now considered a lower-level API. You may want to instead use the default import fromtinacms
to configure things. Learn more
tina-graphql-gateway-cli
is now @tinacms/cli
yarn add @tinacms/cli
yarn remove tina-graphql-gateway-cli
- import { defineSchema } from 'tina-graphql-gateway-cli'
+ import { defineSchema } from '@tinacms/cli'
Note: the
defineSchema
API has changed, too. Read on for how to upgrade
tina-gql
cli command is now tinacms
- yarn tina-gql server:start
+ yarn tinacms server:start
defineSchema
APIWe're going to be leaning on a more primitive concept of how types are defined with Tina, and in doing so will be introducing some breaking changes to the way schemas are defined. Read the detailed RFC discussion for more on this topic, specifically the latter portions of the discussion.
fields
or templates
propertyYou can now provide fields
instead of templates
for your collection, doing so will result in a more straightforward schema definition:
{
collections: [
{
name: 'post',
label: 'Post',
path: 'content/posts',
fields: [
{
name: 'title',
label: 'Title',
type: 'string', // learn more about _type_ changes below
},
],
},
]
}
Why?
Previously, a collection could define multiple templates, the ambiguity introduced with this feature meant that your documents needed a _template
field on them so we'd know which one they belonged to. It also mean having to disambiguate your queries in graphql:
getPostDocument(relativePage: $relativePath) {
data {
...on Article_Doc_Data {
title
}
}
}
Going forward, if you use fields
on a collection, you can omit the _template
key and simplify your query:
getPostDocument(relativePage: $relativePath) {
data {
title
}
}
type
changesTypes will look a little bit different, and are meant to reflect the lowest form of the shape they can represent. Moving forward, the ui
field will represent the UI portion of what you might expect. For a blog post "description" field, you'd define it like this:
{
type: "string",
label: "Description",
name: "description",
}
By default string
will use the text
field, but you can change that by specifying the component
:
{
type: "string",
label: "Description",
name: "description",
ui: {
component: "textarea"
}
}
For the most part, the UI properties are added to the field and adhere to the existing capabilities of Tina's core field plugins. But there's nothing stopping you from providing your own components -- just be sure to register those with the CMS object on the frontend:
{
type: "string",
label: "Description",
name: "description",
ui: {
component: "myMapField"
someAdditionalMapConfig: 'some-value'
}
}
Register your myMapField
with Tina:
cms.fields.add({
name: 'myMapField',
Component: MapPicker,
})
Every property in the defineSchema
API must be serlializable. Meaning functions will not work. For example, there's no way to define a validate
or parse
function at this level. However, you can either use the formifyCallback API to get access to the Tina form, or provide your own logic by specifying a plugin of your choice:
{
type: "string",
label: "Description",
name: "description",
ui: {
component: "myText"
}
}
And then when you register the plugin, provide your custom logic here:
import { TextFieldPlugin } from 'tinacms'
// ...
cms.fields.add({
...TextFieldPlugin, // spread existing text plugin
name: 'myText',
validate: value => {
someValidationLogic(value)
},
})
Why?
The reality is that under the hood this has made no difference to the backend, so we're removing it as a point of friction. Instead, type
is the true definition of the field's shape, while ui
can be used for customizing the look and behavior of the field's UI.
.
type
can be a listPreviously, we had a list
field, which allowed you to supply a field
property. Instead, every primitive type can be represented as a list:
{
type: "string",
label: "Categories",
name: "categories",
list: true
}
Additionally, enumerable lists and selects are inferred from the options
property. The following example is represented by a select
field:
{
type: "string",
label: "Categories",
name: "categories",
options: ["fitness", "movies", "music"]
}
While this, is a checkbox
field
{
type: "string",
label: "Categories",
name: "categories"
list: true,
options: ["fitness", "movies", "music"]
}
object
typeTina currently represents the concept of an object in two ways: a group
(and group-list
), which is a uniform collection of fields; and blocks
, which is a polymporphic collection. Moving forward, we'll be introducing a more comporehensive type, which envelopes the behavior of both group
and blocks
, and since every field can be a list
, this also makes group-list
redundant.
Note: we've previously assumed that
blocks
usage would always be as an array. We'll be keeping that assumption with theblocks
type for compatibility, butobject
will allow for non-array polymorphic objects.
object
typeAn object
type takes either a fields
or templates
property (just like the collections
definition). If you supply fields
, you'll end up with what is essentially a group
item. And if you say list: true
, you'll have what used to be a group-list
definition.
Likewise, if you supply a templates
field and list: true
, you'll get the same API as blocks
. However you can also say list: false
(or omit it entirely), and you'll have a polymorphic object which is not an array.
Gotcha -
type: object
withtemplates: []
andlist: false
is not yet suppored for form generation. You can use it your API but won't be able to edit that field.
This is identical to the current blocks
definition:
{
type: "object",
label: "Page Sections",
name: "pageSections",
list: true,
templates: [{
label: "Hero",
name: "hero",
fields: [{
label: "Title",
name: "title",
type: "string"
}]
}]
}
And here is one for group
:
{
type: "object",
label: "Hero",
name: "hero",
fields: [{
label: "Title",
name: "title",
type: "string"
}]
}
dataJSON
fieldYou can now request dataJSON
for the entire data object as a single query key. This is great for more tedius queries like theme files where including each item in the result is cumbersome.
Note there is no typescript help for this feature for now
getThemeDocument(relativePath: $relativePath) {
dataJSON
}
{
"getThemeDocument": {
"dataJSON": {
"every": "field",
"in": {
"the": "document"
},
"is": "returned"
}
}
}
Keep in mind, dataJSON
does not resolve acrosss multiple documents. Instead, it will return the foreign key for a reference:
{
"getPostDocument": {
"data": {
"title": "Hello, World!",
"author": "path/to/author.md"
}
}
}
Previously, lists would return a simple array of items:
{
getPostList {
id
}
}
Which would result in:
{
"data": {
"getPostList": [
{
"id": "content/posts/voteForPedro.md"
}
]
}
}
In the new API, you'll need to step through edges
& nodes
:
{
getPostList {
edges {
node {
id
}
}
}
}
{
"data": {
"getPostList": {
"edges": [
{
"node": {
"id": "content/posts/voteForPedro.md"
}
}
]
}
}
}
Why?
The GraphQL connection spec opens up a more future-proof structure, allowing us to put more information in to the connection itself like how many results have been returned, and how to request the next page of data.
Read a detailed explanation of how the connection spec provides a richer set of capabilities.
Note: sorting and filtering is still not supported for list queries.
_body
is no longer included by defaultThere is instead an isBody
boolean which can be added to any string
field
Why?
Since markdown files sort of have an implicit "body" to them, we were automatically populating a field which represented the body of your markdown file. This wasn't that useful, and kind of annoying. Instead, just attach isBody
to the field which you want to represent your markdown "body":
{
collections: [{
name: "post",
label: "Post",
path: "content/posts",
fields: [
{
name: "title",
label: "Title",
type: "string"
}
{
name: "myBody",
label: "My Body",
type: "string",
component: 'textarea',
isBody: true
}
]
}]
}
This would result in a form field called My Body
getting saved to the body of your markdown file (if you're using markdown):
---
title: Hello, World!
---
This is the body of the file, it's edited through the "My Body" field in your form.
Instead of a collection
property, you must now define a collections
field, which is an array:
{
type: "reference",
label: "Author",
name: "author",
collections: ["author"]
}
{
getPostDocument(relativePath: "hello.md") {
data {
title
author {
...on Author_Document {
name
}
...on Post_Document {
title
}
}
}
}
template
field on polymorphic objects (formerly blocks) is now _template
Old API:
---
myBlocks:
- template: hero
title: Hello
---
New API:
---
myBlocks:
- _template: hero
title: Hello
---
data
__typename
values have changedThey now include the proper namespace to prevent naming collisions and no longer require _Doc_Data
suffix. All generated __typename
properties are going to be slightly different. We weren't fully namespacing fields so it wasn't possible to guarantee that no collisions would occur. The pain felt here will likely be most seen when querying and filtering through blocks. This ensures the stability of this type in the future
{
getPageDocument(relativePath: "home.md") {
data {
title
myBlocks {
...on Page_Hero_Data { # previously this would have been Hero_Data
# ...
}
}
}
}
}
null
Previously a listable field which wasn't defined in the document was treated as an empty array. Moving forward the API response will result in null
rather than []
:
---
title: 'Hello, World'
categories: []
---
The responsee would be categories: []
. If you omit the field entirely:
---
title: 'Hello, World'
---
The response will be categories: null
. Previously this would have been []
, which was incorrect