Setup Tina Cloud Client


Prerequisite

This guide gets the client working on a fresh NextJS site but can also be used as a reference to add the client to an existing site.

To start with a new site. you can create one with:

npx create-next-app

or

yarn create next-app

Going forward yarn with be used for examples.

Name it whatever you like and cd in your new project.

Add Typescript

For this guide we will be using Typescript. Add it to the project with this:

yarn add --dev typescript @types/react @types/react-dom @types/node

Then replace _app.js with _app.tsx and index.js with index.tsx.

Run:

touch tsconfig.json

And finally run:

yarn dev

That should generate the tsconfig.json content required.

Install the client package

This package provides you with:

  • A Client class (which you can use as a TinaCMS API Plugin), that takes care of all interaction with the GraphQL server.
  • A useForm hook, that you can use to hook into the Tina forms that let you edit your content.
yarn add tina-graphql-gateway

Choose the latest version

Install CLI package

You'll also likely want to install our CLI to help with development:

yarn add --dev tina-graphql-gateway-cli

Choose the latest version

This CLI performs a few functions:

  • Generates GraphQL queries (and optionally TypeScript types) based on your content's schema.
  • Auditing your content's schema and checking for errors.
  • Running a GraphQL server using the built-in filesystem adapter.

For full documentation of the CLI, see here.

Implementation

We'll now show how to use this package in a NextJS site

Note: This solution relies on a long-running server, and will not work with NextJS' serverless build target.

Create Example Content

Let's start by creating a simple piece of content. Our goal will to be able to access and change this content through an auto-generated GraphQL API and Tina forms.

First create a new folder at the root of the project called content, then a subfolder of content called pages, and in there create a markdown file called index.md.

In content/pages/index.md, add this:

---
title: A great sight
---

Configuration

Before we can define the schema of our content, we need set up some configuration. Create a .tina directory at the project root and then create the following files.

.tina/settings.yml

---
new_page_extension: md
auto_deploy: false
admin_path:
webhook_url:
sections:
  - type: directory
    path: content/pages # replace this with the relative path to your content section
    label: Pages
    create: documents
    match: '*.md'
    new_doc_ext: md
    templates:
      - index # replace this with your template filename name
upload_dir: public/uploads
public_path: '/uploads'
front_matter_path: ''
use_front_matter_path: false
file_template: ':filename:'

These files will create a map to our content to content models. In the above settings file, we declare any markdown files in our project should be a "index" type (we'll define this index type next).

Define Content Schema

Templates define the shape of different content models.

.tina/front_matter/templates/index.yml

---
label: Index
hide_body: false
display_field: title
fields:
  - name: title
    type: text
    config:
      required: false
    label: Title
pages:
  - content/pages/index.md # This keeps reference to all the pages using this template

Sourcing your content

Store the project in a GitHub repo

If you're following this guide with an existing project, make sure it is stored in GitHub and .tina has been pushed up.

If you have created a new NextJS site, then please now go and create a repository in GitHub and push up the project to it.

Create a Tina Cloud "app"

Once you have you have a repository, it is time to create a Tina Cloud "app". For help doing so, you can checkout this guide.

Once you have a Tina Cloud "app", add a .env file to the root of your project. It should look like this:

NEXT_PUBLIC_REALM_NAME=YOUR ORGANIZATION ID
NEXT_PUBLIC_TINA_CLIENT_ID=YOUR APP'S CLIENT ID

Fill out the environment variables with the values you've recieved by creating your app.

Using the data within our Next.JS site

First, install the TinaCMS dependencies:

yarn add tinacms styled-components

In _app.tsx, add TinaCMS, register the Client, and wrap our main layout in the TinaCloudProvider to support authentication, like so:

_app.tsx

import React from 'react'
import { TinaProvider, TinaCMS } from 'tinacms'
import { TinaCloudProvider } from 'tina-graphql-gateway'
import createClient from '../components/client'

function App({ Component, pageProps }) {
  return (
    <TinaCloudProvider
      onLogin={(token: string) => {
        const headers = new Headers()

        //TODO - the token should could as a param from onLogin
        headers.append('Authorization', 'Bearer ' + token)
        fetch('/api/preview', {
          method: 'POST',
          headers: headers,
        }).then(() => {
          window.location.href = '/'
        })
        return ''
      }}
      onLogout={() => {
        console.log('exit edit mode')
      }}
    >
      <Component {...pageProps} />
    </TinaCloudProvider>
  )
}

export default function _App(props: any) {
  const cms = new TinaCMS({
    apis: {
      tina: createClient(true),
    },
    sidebar: props.pageProps.preview,
    enabled: props.pageProps.preview,
  })

  return (
    <TinaProvider cms={cms}>
      <App {...props} />
    </TinaProvider>
  )
}

_app.tsx imports createClient from /components/client which you don't have yet. So let's add that now.

First create a folder at the project's root called components then create a file within it called client.ts and fill it with this:

import { Client, DEFAULT_LOCAL_TINA_GQL_SERVER_URL } from 'tina-graphql-gateway'

const createClient = (preview: boolean, getTokenFn?: () => string) =>
  new Client({
    realm: process.env.NEXT_PUBLIC_REALM_NAME || '',
    clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID || '',
    redirectURI: 'http://localhost:3000',
    customAPI: DEFAULT_LOCAL_TINA_GQL_SERVER_URL,
    getTokenFn,
    tokenStorage: getTokenFn ? 'CUSTOM' : 'MEMORY',
  })

export default createClient

This Next implementation relies on a backend function to save its auth details. Create a new file under /pages/api/ called preview.ts and fill it in as such:

import Cookies from 'cookies'

const preview = (req: any, res: any) => {
  const token = (req.headers['authorization'] || '').split(' ')[1] || null

  res.setPreviewData({})

  const cookies = new Cookies(req, res)
  cookies.set('tinaio_token', token, {
    httpOnly: true,
  })

  res.end('Preview mode enabled')
}

export default preview

This file reqiures the cookies package, simply add it by running

yarn add cookies

The last step is to add a way for the user to enter edit-mode. Let's create a /login page.

/pages/login.tsx

import { useCMS } from 'tinacms'

export default function Login(props) {
  const cms = useCMS()

  return (
    <>
      <button onClick={() => cms.toggle()}>
        {cms.enabled ? 'Exit Edit Mode' : 'Edit This Site'}
      </button>
      {props.preview && (
        <p>
          You are logged in to Tina.io. Return to <a href="/">homepage</a>
        </p>
      )}
    </>
  )
}

export const getStaticProps = async ({ preview }) => {
  return {
    props: {
      preview: !!preview,
    },
  }
}

At this point, if you go to /login you should now be able to login, for no good reason though because there's nothing yet to edit. Let's fix that next.

Editing your content

Let's rejig index.tsx a bit:

import Head from 'next/head'
import styles from '../styles/Home.module.css'
import Cookies from 'cookies'
import createClient from '../components/client'
import { useForm, useTinaAuthRedirect } from 'tina-graphql-gateway'
import { DocumentUnion, Index as IndexResponse } from '../.tina/types'

export default function Home(props) {
  useTinaAuthRedirect()

  const data = props.preview
    ? useForm<IndexResponse>(props).data
    : props.document?.node.data || {}

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>{data.title}</h1>
        {props.preview && <p>You are currently in edit mode.</p>}
        {!props.preview && (
          <p>
            To edit this site, go to <a href="/login">login</a>
          </p>
        )}
      </main>
    </div>
  )
}

export async function getServerSideProps(props) {
  const relativePath = `index.md`
  const section = 'pages'

  const cookies = new Cookies(props.req, props.res)
  const authToken = cookies.get('tinaio_token')

  const getTokenFn = () => authToken || ''

  const client = createClient(props.preview, getTokenFn)

  const content = await client.getContentForSection<DocumentUnion>({
    relativePath: relativePath,
    section: section,
  })

  return {
    props: {
      ...content,
      relativePath,
      section,
      preview: !!props.preview,
    },
  }
}

Using getServerSideProps and the tina-graphql-gateway client we are fetching the content for this page. Then in the page component we are conditionally registering a form and recieving data that we are displaying on the page.

We are missing something here though, a .tina/types file. We'll generate one very simply with the client CLI, just run:

yarn tina-gql schema:types

Also update your package.json scripts to also run the graphql server like such:

"scripts": {
    "next-dev": "next dev",
    "dev": "yarn tina-gql server:start -c \"yarn next-dev\"",
    "next-build": "next build",
    "start": "next start",
    "build": "yarn tina-gql server:start -c \"yarn next-build\""
  }

And run the project again.

You should be able to edit the title using the sidebar and when you save it it'll update content/pages/index.md on the local filesytem.

Editing content on GitHub

To make your changes affect your repository on GitHub instead of the local filesystem, only one line of code needs to be changed.

Change components/client.ts to:

import { Client, DEFAULT_LOCAL_TINA_GQL_SERVER_URL } from 'tina-graphql-gateway'

const createClient = (preview: boolean, getTokenFn?: () => string) =>
  new Client({
    realm: process.env.NEXT_PUBLIC_REALM_NAME || '',
    clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID || '',
    redirectURI: 'http://localhost:3000',
    customAPI: preview ? undefined : DEFAULT_LOCAL_TINA_GQL_SERVER_URL,
    getTokenFn,
    tokenStorage: getTokenFn ? 'CUSTOM' : 'MEMORY',
  })

export default createClient

Line 8 changed from customAPI: DEFAULT_LOCAL_TINA_GQL_SERVER_URL, to customAPI: preview ? undefined : DEFAULT_LOCAL_TINA_GQL_SERVER_URL,.

Now in edit mode, changes will be saved directly to GitHub.