Tina Docs
Introduction
Core Concepts
Querying Content
Editing
Customizing Tina
Going To Production
Drafts
Guides
Further Reference

The following guide requires tinacms: 1.0.2 or later.
Want to skip to the end result? Check out the final result

Using Drafts with Visual Editing

In most cases, you will not want to create pages on your production site for your draft documents. This makes handling drafts a challenge with visual editing. In this example we will show how to add visual editing to a draft document using Next.js preview-mode.

In preview-mode getStaticProps will be called on every request. This means that we can conditionally grab draft documents in preview-mode, and keep them out of your production site.

"Preview-mode" can be added in just a few steps:

1. Add the preview-mode api handlers

Note: If you have not installed @tinacms/auth you can do so by running yarn add @tinacms/auth or npm install @tinacms/auth

Create a file called pages/api/preview/enter.{ts,js} this will handle the request to enter preview-mode. This file should look like this:

import { isUserAuthorized } from '@tinacms/auth'
const handler = async (req, res) => {
if (process.env.NODE_ENV === 'development') {
// Enter preview-mode in local development
res.setPreviewData({})
return res.redirect(req.query.slug)
}
// Check TinaCloud token
const isAuthorizedRes = await isUserAuthorized({
token: `Bearer ${req.query.token}`,
clientID: process.env.NEXT_PUBLIC_TINA_CLIENT_ID,
})
if (isAuthorizedRes) {
res.setPreviewData({})
return res.redirect(req.query.slug)
}
return res.status(401).json({ message: 'Invalid token' })
}
export default handler

This handler verifies (With TinaCloud) that the token is valid and then redirects to the document we want to edit.

Next, create a file called pages/api/preview/exit.{ts,js} this will handle the request to exit preview-mode. This file should look like this:

const handler = (req, res) => {
res.clearPreviewData()
res.redirect(req.query.slug)
}
export default handler

Both of these files are based on the Next.js preview-mode api handlers.

2. Update tina/config

export default defineConfig({
// ...
// Add this to your config
admin: {
auth: {
onLogin: async ({ token }) => {
// When the user logs in enter preview mode
location.href =
`/api/preview/enter?token=${token.id_token}&slug=` + location
},
onLogout: async () => {
// When the user logs out exit preview mode
location.href = `/api/preview/exit?slug=` + location
},
},
},
// ...
})

3. Update data fetching

Updates to getStaticPaths

We'll now update our getStaticPaths, so that draft pages are excluded in our production site.

const req = await client.queries.postConnection()
export const getStaticPaths = async () => {
- const req = await client.queries.postConnection()
+ const req = await client.queries.postConnection({
+ filter: { draft: { eq: false } },
+ })
// ...
}

Depending on your use case you can also safely use any value for fallback.

Updates to getStaticProps

Listing pages

First we will create a util function that will either return all the documents or just the production documents depending on if we are in preview-mode.

util/getPosts.{ts,js}

import { client } from '../<PathToTina>/tina/__generated__/client'
export const getPosts = async ({ preview }) => {
// by default get non-draft posts
let filter = { draft: { eq: false } }
// if preview-mode is enabled, get all posts
if (preview) {
filter = {}
}
return client.queries.postConnection({
filter,
})
}

Use this function anywhere you are fetching a list of posts (Posts index page).

import { getPosts } from '../util/getPosts'
//...
export const getStaticProps = async ({ preview = false }) => {
const { data, query, variables } = await getPosts({
preview,
})
return {
props: {
preview,
data,
query,
variables,
//myOtherProp: 'some-other-data',
},
}
}
Slug pages (optional)

On pages that use SSR or "incremental static regeneration" (ISR), your getStaticProps function will be called on every request. This means that we need to return a 404 when the document is a draft and we are not in preview-mode.

export const getStaticProps = async ({ params, preview = false }) => {
const { data, query, variables } = await client.queries.post({
relativePath: params.slug + '.md',
})
return {
// the post is not found if its a draft and the preview is false
notFound: data?.post?.draft && !preview,
props: {
preview,
data,
query,
variables,
},
}
}

4. Add the exit preview button

To help with exiting preview-mode we can add a button to the top of the site. The button will show up in any page that returns preview: true in getStaticProps.

In pages/_app.{ts,js} add the following:

const App = ({ Component, pageProps }) => {
const slug = typeof window !== 'undefined' ? window.location.pathname : '/'
return (
<>
{/* Feel free to add your own styling! */}
{pageProps.preview && (
<div>
You are in preview-mode
{/* This link will logout of Tina and exit preview mode */}
<a
href={`/admin/index.html#/logout?slug=/api/preview/exit?slug=${slug}`}
>
Click here
</a>{' '}
to exit
</div>
)}
<Component {...pageProps} />
</>
)
}
export default App

Now when an editor logs in they will enter preview mode and be able to contextual edit draft documents.

You can see the final result here and if you want to learn more about preview mode see the Next.js docs.

Working with editorial workflows

If you're using editorial workflows, you'll likely want to ensure that the preview data is fetching content from the branch you're editing. To enable this, subscribe to the branch:change event via the cmsCallback function in the config:

// tina/config.ts
import { defineConfig } from 'tinacms'
export default defineConfig({
// ...
cmsCallback: (cms) => {
cms.events.subscribe('branch:change', async ({ branchName }) => {
console.log(`branch change detected. setting branch to ${branchName}`)
return fetch(`/api/preview/change-branch?branchName=${branchName}`)
})
return cms
},
})

Add another api endpoint at /api/preview/change-branch, which only updates the branch data if we're in preview mode:

// api/preview/change-branch
export default function handler(req, res) {
if (req.preview && req.query?.branchName) {
res.setPreviewData({ branch: req.query.branchName })
return res.status(200).json({ message: 'Success' })
}
return res.status(403).json({ message: 'Unauthorized' })
}

Update our request to include the branch (if provided) in getStaticProps:

export const getStaticProps = async ({
params,
preview = false,
previewData = {},
}) => {
const { data, query, variables } = await client.queries.post(
{
relativePath: params.slug + '.md',
},
{
branch: preview && previewData?.branch,
}
)
return {
// the post is not found if its a draft and the preview is false
notFound: data?.post?.draft && !preview,
props: {
preview,
data,
query,
variables,
},
}
}