March 16, 2022
By James Perkins
Tina allows you as a developer to create an amazing editing experience. By default the editing experience is a more traditional CMS, where you login to a specific URL and you edit your content without being able to see the content till after you finish your changes. But, if you are looking for a more transparent real-time editing experience, Tina has a superpower, called Visual Editing. Just as it sounds, you get instant feedback on the page as well as being able to preview the changes before publishing live to your site.
For this blog post we are going to use Tina’s example which is located in the Next.js official examples and allows us to use create-next-app
.This example is based upon Next.js blog starter which is a markdown blog.
You can find the source code in the Next.js examples directory, under the name cms-tina.
## Create our new Tina sitenpx create-next-app --example cms-tina tina-contextual-editing## Move into our app.cd tina-contextual-editing## Open with your favorite editor
If you want to learn how this example was created check out the blog post that covers getting started with Tina.
Before you add any code to this example, I want to cover how Tina is integrated and how it works.
.tina
folderYou will find a .tina
folder in the root of the project, this is the heart and brain of Tina in any project.
schema.ts
The schema file contains two important pieces of code defineSchema
and defineConfig
. The defineSchema
allows you to define the shape of your content. If you have used a more traditional CMS before you may have done this via a GUI. However, given how Tina is tightly coupled with Git and we treat the filesystem as the “source of truth”, we take the approach of “content-modeling as code”.
If you look at the current defined schema, you will see that each one of the fields is related to the front matter contained within any of the posts in the _posts
folder.
Moving on to the defineConfig
, the defineConfig
tells your project where the content is requested from, what branch to use, and any configuration to TinaCMS itself.
components
The components
folder inside the .tina
holds both our TinaProvider
and DynamicTinaProvider
; these two components wrap your application with the power of Tina. We will be heading back here later for some updates.
_generated__
This folder holds all the auto generated files from Tina, if you open this up you will see files that contain queries, fragments and types. You won’t need to make changes here but it’s good to know what you might find in there.
Before we add visual editing, go ahead and launch the application using yarn tina-dev
and navigate to http://localhost:3000/
. You will notice that it is a static blog. Feel free to navigate around and get a feel for the blog.
Now if you navigate to http://localhost:3000/admin you will be presented with a screen to login, if you login you will land on the CMS dashboard. Selecting a collection on the left will bring you to a screen with all the current files in that space. Then selecting a file will allow you to edit it as you see fit.
Now you have an understanding of both how the CMS works, and how the code behind is laid out we can start working on adding visual editing to our project. What do we need to do to make visual editing work?
getStaticPaths
and getStaticProps
to use Tina’s graphql layeruseTina
and power the project propsThe getStaticPaths
query is going to need to know where all of our markdown files are located. With our current schema you have the option to use postConnection
, which will provide a list of all posts in our _posts
folder. Make sure your local server is running and navigate to http://localhost:4001/altair and select the Docs button. The Docs button gives you the ability to see all the queries possible and the variables returned:
So based upon the postConnection
we will want to query the _sys
which is the filesystem and retrieve the filename
, which will return all the filenames without the extension.
query {postConnection {edges {node {_sys {basename}}}}}
If you run this query in the GraphQL client you will see the following returned:
{"data": {"postConnection": {"edges": [{"node": {"_sys": {"filename": "dynamic-routing"}}},{"node": {"_sys": {"filename": "hello-world"}}},{"node": {"_sys": {"filename": "preview"}}}]}}}
Adding this query to our Blog.
The NextJS starter blog is served on the dynamic route /pages/posts/[slug].js
. When you open the file you will see a function called getStaticPaths
at the bottom of the file.
export async function getStaticPaths() {....
Remove all the code inside of this function and we can update it to use our own code. The first step is to add an import to the top of the file to be able interact with our graphql, and remove the getPostBySlug
and getAllPosts
imports we won’t be using:
//other imports.....- import { getPostBySlug, getAllPosts } from '../../lib/api'+ import { staticRequest } from "tinacms";
Inside of the getStaticPaths
function we can construct our request to our content-api. When making a request we expect a query
or mutation
and then variables
if needed to be passed to the query, here is an example:
staticRequest({ query: '...', // our query }),
“What does staticRequest
do?”
It’s just a helper function which supplies a query to your locally-running GraphQL server, which is started on port 4001
. You can just as easily use fetch
or an http client of your choice.
We can use the getPostsList
query from earlier to build our dynamic routes:
export async function getStaticPaths() {const result = await staticRequest({query: `query {postConnection {edges {node {_sys {filename}}}}`,})return {paths: result.data.postConnection.edges.map(edge => ({params: { slug: edge.node._sys.filename },})),fallback: false,}}
Quick break down of getStaticPaths
The getStaticPaths
code takes the graphql query we created, because it does not require any variables
we can send down an empty object. In the return functionality we map through each item in the postsListData.getPostsList
and create a slug for each one.
The getStaticProps
query is going to deliver all the content to the blog, which is how it works currently with the code provided by Next.js. When we use the GraphQL API we will both deliver the content and give the content team the ability to edit it right in the browser.
We need to query the following things from our content api:
Creating our Query
Using our local graphql client we can query the post
using the path to the blog post in question, below is the skeleton of what we need to fill out.
query BlogPostQuery($relativePath: String!) {post(relativePath: $relativePath) {# data from our posts.}}
We can now fill in the relevant fields we need to query, take special note of both author
and ogImage
which are grouped so they get queried as:
author {namepicture}ogImage {url}
Once you have filled in all the fields you should have a query that looks like the following:
query BlogPostQuery($relativePath: String!) {post(relativePath: $relativePath) {titleexcerptdatecoverImageauthor {namepicture}ogImage {url}body}}
If you would like to test this out, you can add the following to the variables section at the bottom {"relativePath": "hello-world.md"}
Adding our query to our blog
Remove all the code inside of the getStaticProps
function and we can update it to use our own code. Since these pages are dynamic, we’ll want to use the values we returned from getStaticPaths
in our query. We’ll destructure params
to obtain the slug
, using it as a relativePath
. As you’ll recall the “Blog Posts” collection stores files in a folder called _posts
, so we want to make a request for the relative path of our content. Meaning for the file located at _posts/hello-world.md
, we only need to supply the relative portion of hello-world.md
.
export const getStaticProps = async ({ params }) => {const { slug } = params// Ex. `slug` is `hello-world`const variables = { relativePath: `${slug}.md` }// ...}
We’ll also want to call staticRequest
to load our data for our specific page. You’ll also notice that we will return the query & variables from getStaticProps. We’ll be using these values within the TinaCMS frontend.
import { staticRequest } from 'tinacms'
So the full query should look like this:
export const getStaticProps = async ({ params }) => {const { slug } = paramsconst variables = { relativePath: `${slug}.md` }const data = await staticRequest({query: query,variables: variables,})return {props: {data,variables,},}}
You may have noticed that our query isn’t there but we are referencing it. This is because we want to be able to reuse the query for our useTina
hook. At the top of our file, after the imports you can add the query:
const query = `query BlogPostQuery($relativePath: String!) {post(relativePath: $relativePath) {titleexcerptdatecoverImageauthor {namepicture}ogImage {url}body}}`
useTina
hookThe useTina
hook is used to make a piece of Tina content editable. It is code-split so in production, this hook will pass through the data value. When you are in edit mode, it registers an editable form in the sidebar.
The first thing that needs to be done is import useTina, useEffect and useState. We can also remove a few imports that we will no longer be using.
import {useState, useEffect} from 'react'import {useTina} from 'tinacms/dist/edit-state'
We are going to update our props from ({ post, morePosts, preview })
to (props)
. We are going to pass the props into our useTina
hook. Now that has been updated with the props coming in we can use useTina
.
- export default function Post({ post, morePosts, preview }) {+ export default function Post( props ) {+ const { data } = useTina({+ query,+ variables: props.variables,+ data: props.data,+ })
As you can see, we are reusing our query from before and passing the variables, and data to useTina
. This now means we can power our site using visual editing. Congratulations your site now has superpowers!
Now we have access to our Tina powered data we can go through and update all of our elements to use Tina. You can replace each of the post.
with data.getPostsDocument.data.
and then replace the !post?.slug
with !props.variables.relativePath
when you are finished your return code should look like:
const router = useRouter()- if (!router.isFallback && !post?.slug) {+ if (!router.isFallback && !props.variables.relativePath) {return <ErrorPage statusCode={404} />}return (- <Layout preview={preview}>+ <Layout preview={false}><Container><Header />{router.isFallback ? (<PostTitle>Loading…</PostTitle>) : (<><article className="mb-32"><Head><title>- {post.title} | Next.js Blog Example with {CMS_NAME}+ {data.post.title} | Next.js Blog Example with {CMS_NAME}</title>- <meta property="og:image" content={post.ogImage.url} />+ <meta property="og:image" content={data.post.ogImage.url} /></Head><PostHeader- title={post.title}- coverImage={post.coverImage}- date={post.date}- author={post.author}+ title={data.post.title}+ coverImage={data.post.coverImage}+ date={data.post.date}+ author={data.post.author}/>- <PostBody content={post.content} />+ <PostBody content={data.post.body} /></article></>)}</Container></Layout>)
One piece of code that the original was doing was taking the markdown and turning it into HTML, inside of the getStaticPriops
. We currently aren’t doing that with our body and just returning the string. Due to the nature of visual editing, we need to make sure that we are always passing the latest content through the markdownToHtml
function provided by the team at Next.js. This is where useState
and useEffect
come in, first create a content variable that is track by a state:
Note you could use another markdown to html package that would not require this, but in this example we want to reuse as much of the original code as possible to show how you could integrate with minimal code replacement.
const [content, setContent] = useState('')
Then in useEffect we are going to parse our body to the markdownToHtml code provided by the Next.js team and set it to content.
useEffect(() => {const parseMarkdown = async () => {setContent(await markdownToHtml(data.post.body))}parseMarkdown()}, [data.post.body])
Now all we need to do is update our post body content from data.post.body
to content
. If you go ahead and test your application now, you can now edit on the page!
Now that you have visual editing here are a few things you can explore with Tina
The best way to keep up with Tina is to subscribe to our newsletter, we send out updates every two weeks. Updates include new features, what we have been working on, blog posts you may of missed and so much more!
You can subscribe by following this link and entering your email: https://tina.io/community/
Tina has a community Discord that is full of Jamstack lovers and Tina enthusiasts. When you join you will find a place:
Our Twitter account (@tinacms) announces the latest features, improvements, and sneak peeks to Tina. We would also be psyched if you tagged us in projects you have built.
© TinaCMS 2019–2024