This package allows you to interact with an automatically generated GraphQL API using TinaCMS. Included are multiple GraphQL adapters that give you a consistent GraphQL API regardless of your datasource.
If you like to work in TypeScript, the @forestry/cli package can generate types using the same schema definition that the GraphQL adapters will use.
This guide assumes you have a working NextJS site. You can create one quickly with:
npx create-next-app --example blog-starter-typescript blog-starter-typescript-app
or
yarn create next-app --example blog-starter-typescript blog-starter-typescript-app
This package provides you with:
Client
class (which you can use as a TinaCMS API Plugin), that takes care of all interaction with the GraphQL server.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
You'll also likely want to install our CLI to help with development:
npm install --save-dev @forestryio/cli
or
yarn add --dev @forestryio/cli
This CLI performs a few functions:
For full documentation of the CLI, see here.(https://github.com/forestryio/graphql-demo/tree/client-documentation/packages/cli)
We'll show how to use this package in a NextJS site
Let's start by creating a simple dummy piece of content. Our goal will to be able to access and change this content through an auto-generated GraphQL API and Tina forms.
/_posts/welcome.md
---
title: This is my post
---
Before we can define the schema of our content, we need set up some configuration. Create a .forestry
directory and then create the following files.
.forestry/settings.yml
---
new_page_extension: md
auto_deploy: false
admin_path:
webhook_url:
sections:
- type: directory
path: _posts # replace this with the relative path to your content section
label: Posts
create: documents
match: '**/*.md'
new_doc_ext: md
templates:
- post # 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 our content to content models. In the above file, we declare any markdown files in our project should be a "post" type (we'll define this post type next).
Templates define the shape of different content models.
.forestry/front_matter/templates/post.yml
---
label: Post
hide_body: false
display_field: title
fields:
- name: title
type: text
config:
required: false
label: Title
pages:
- _posts/welcome.md # This keeps reference to all the pages using this template
Now that we have defined our content model, we can connect our site to the Tina.io Content API
Make sure your .tina directory is pushed to git
The Tina.io content API connects to your Github repository, and puts the content behind Tina.io's expressive content API.
You will then see a client-id for your new app. We will use this shortly.
First, install the TinaCMS dependencies:
npm install tinacms styled-components
or
yarn add tinacms styled-components
In your site root, add TinaCMS & register the ForestryClient
like so:
_app.tsx
import React from 'react'
import { withTina } from 'tinacms'
import { ForestryClient } from '@forestryio/client'
import config from '../.forestry/config'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default withTina(MyApp, {
apis: {
forestry: new ForestryClient({
realm: 'your-realm-name', // this was set by you in the previous step
clientId: 'your-client-id', // this is visible in your Tina.io dashboard
redirectURI: 'your webpage url', //e.g http://localhost:3000
// identityProxy: "", // we can use an identity proxy if we want to use a CSRF token (see token storage below)
// customAPI: "", // might be used with the identityProxy, to proxy through a custom backend service.
// tokenStorage: (Default Memory). Possible values: "MEMORY" | "LOCAL_STORAGE" | "CUSTOM".
// NOTE: If you choose to use LOCAL_STORAGE, you may be prone to CSRF vulnerabilities.
// getTokenFn: undefined, // This is only used when "tokenStorage" is set to "CUSTOM". Instead of grabbing the token from local storage, we can specify how its access token is retreived. You might want to use this if you are fetching content server-side.
}),
},
sidebar: true,
})
We'll also want to wrap our main layout in the TinacmsForestryProvider
to support authentication
//...
function MyApp({ Component, pageProps }) {
const forestryClient = useCMS().api.forestry
return (<TinacmsForestryProvider
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 = '/'
})
}}
onLogout={() => {console.log('exit edit mode')}}
><Component {...pageProps} />)
}
//...
This Next implementation relies on a backend function to save its auth details.
// /pages/api/preview.ts
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
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>
)
}
Your users should at this point be able to login and view their content from Tina.io's API. We will also want the site to build outside of edit-mode, for your production content.
Now that we've defined our schema, let's use the CLI to setup a GraphQL server for our site to use locally, or during production builds.
Start your local GraphQL server by running:
npx tina-gql server:start
or
yarn tina-gql server:start
You can now go to http://localhost:4001/graphql and use GraphiQL to explore your new GraphQL API.
pages/posts/welcome.tsx
import config from "../../.forestry/config";
import query from "../../.forestry/query";
import Cookies from 'cookies'
import { usePlugin } from "tinacms";
import {
useForestryForm,
ForestryClient,
DEFAULT_LOCAL_TINA_GQL_SERVER_URL,
} from "@forestryio/client";
// These are your generated types from CLI
import { DocumentUnion, Query } from "../../.tina/types";
export async function getServerProps({ params }) {
const path = `_posts/welcome.md`;
const cookies = new Cookies(props.req, props.res)
const authToken = cookies.get('tinaio_token')
const client = new ForestryClient({
realm: "your-realm-name", // this was set by you in the previous step
clientId: "your-client-id", // this is visible in your Tina.io dashboard
redirectURI: "your webpage url", //e.g http://localhost:3000
customAPI: preview ? undefined : DEFAULT_LOCAL_TINA_GQL_SERVER_URL,
tokenStorage: "CUSTOM"
getTokenFn: () => authToken //supply our own function to just return the token
});
const content = await client.getContentForSection({
relativePath: path,
section: 'posts'
});
return { props: content };
}
export default function Home(props) {
const [formData, form] = useForestryForm<Query, DocumentUnion>(props).data;
usePlugin(form);
return (
<div>
<h1>{formData.data.title}</h1>
</div>
);
}
Now, if you navigate to /posts/welcome you should see your production content. Once you log-in, you should also be able to update your content using the TinaCMS sidebar.
Next steps:
$ tina-gql schema:audit
There are a few ways to store the authentication token:
Storing tokens in browser local storage persists the user session between refreshes & across browser tabs. One thing to note is; if an attacker is able to inject code in your site using a cross-site scripting (XSS) attack, your token would be vulernable. To add extra security, a CSRF token can be implemented by using a proxy.
Within your client instantiation:
new ForestryClient({
// ...
identityProxy: '/api/auth/token',
})
From your site's server (This example uses NextJS's API functions)
// pages/api/auth/token
// ... Example coming soon
This is our recommended token storage mechanism if possible. Storing tokens in memory means that the user session will not be persisted between refreshes or across browser tabs. This approach does not require a server to handle auth, and is the least vulernable to attacks.
We can automatically generate TypeScript types based on your schema by running the following command with the Tina Cloud CLI:
yarn tina-gql schema:types
or
yarn tina-gql schema:gen-query --typescript
This will create a file at .forestry/types.ts
.