Introduction
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.
For example, if your content is Git-backed, you might want to use your local content in development. While in your production Cloud Editing Environment, you can use our "Tina Teams" server to fetch your content. The API for both backends will be consistent, so you can easily switch between the two datasources without changing your site's code.
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.
Install
Prerequisites
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
Install the client package
This package provides you with:
- A
ForestryClient
class (which you can use as a TinaCMS API Plugin), that takes care of all interaction with the GraphQL server. - A
useForestryForm
hook, that you can use to hook into the Tina forms that let you edit your content.
npm install --save @forestryio/client
or
yarn add @forestryio/client
CLI package
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:
- 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].(https://github.com/forestryio/graphql-demo/tree/client-documentation/packages/cli)
Implementation
We'll show how to use this package in a NextJS site
Create Dummy Content
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
---
Configuration
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
label: Posts
create: documents
match: "**/*.md"
new_doc_ext: md
templates:
- post
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).
Define Content Schema
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
Sourcing your content
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
Creating a Tina.io app
The Tina.io content API connects to your Github repository, and puts the content behind Tina.io's expressive content API.
- Navigate to Tina.io
- Create a realm
- Create an app
You will then see a client-id for your new app. We will use this shortly.
Using the data within our Next.JS site
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",
clientId: "your-client-id",
redirectURI: "your webpage url",
}),
},
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.
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.
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.
Creating a local GraphQL server
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
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";
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",
clientId: "your-client-id",
redirectURI: "your webpage url",
customAPI: preview ? undefined : DEFAULT_LOCAL_TINA_GQL_SERVER_URL,
tokenStorage: "CUSTOM"
getTokenFn: () => authToken
});
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:
- Make changes to our data-model, and verify our templates with
$ tina-gql schema:audit
- Setup typescript types for your data-model
Token storage
There are a few ways to store the authentication token:
Local storage (Default)
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)
In memory (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.
Typescript
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
.