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.
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.
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:
yarn add --dev tina-graphql-gateway-cli
Choose the latest version
This CLI performs a few functions:
For full documentation of the CLI, see here.
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.
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
---
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).
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
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.
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.
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.
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.
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.