TinaCMS does not officially support Gatsby. We recommend migrating your Gatsby site to a well supported framework such as Next.JS instead.
In this tutorial we'll be showing you how you might go about converting an existing gatsby-mdx blog to use TinaCMS. We've created a starter repo for you to follow along with. The repo is a fork of the official Gatsby md blog starter.
There are a few limitations with the approach outlined in this guide.
First clone the our sample Gatsby project. Then you'll want to navigate into the blog's directory.
git clone https://github.com/tinacms/gatsby-mdx-example-blogcd gatsby-mdx-example-blog
Awesome! You're all set up and ready start adding TinaCMS. You can initialize it using the command below.
npx @tinacms/cli@latest init
After running the command above you'll receive a few propmts
“other”
“NPM”
as your package manager“no”
“public”
Now that we've added Tina to our project, there are a few more steps to integrate it with Gatsby. Start by adding the following line at the top of tina/config.js
export default defineConfig({+ client: { skip: true },// ...
Then add the following line in gatsby-node.js
+ const express = require("express");+ exports.onCreateDevServer = ({ app }) => {+ app.use("/admin", express.static("public/admin"));+ };
To save ourself some pain we can also change our startup command to make sure Tina gets run when we run our app in development mode.
"scripts": {"build": "gatsby build",- "develop": "gatsby develop",+ "develop": "npx tinacms dev -c \"gatsby develop\"","format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"","start": "gatsby develop","serve": "gatsby serve","clean": "gatsby clean","test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"}
Now we've added Tina to our project but it's not pointing at our existing markdown files. Let fix that.
Open up tina/config.ts
and change the path to point to our blog directory. We'll also need to update our schema since we'll be working with mdx files.
schema: {collections: [{name: "post",label: "Posts",+ format: "mdx",- path: "content/posts"+ path: content/blog"// ...
We'll also need to change the location where images get uploaded to. Since "public"
doesn't get tracked in Git.
By moving our images to `"static"`, we're ensuring that they'll be tracked in git and bundled at run time.
export default defineConfig({branch,client: { skip: true },// Get this from tina.ioclientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID,// Get this from tina.iotoken: process.env.TINA_TOKEN,build: {outputFolder: "admin",publicFolder: "public",},media: {tina: {- mediaRoot: "",+ mediaRoot: "images",- publicFolder: "public",+ publicFolder: "static",},// ...
Now our project is set up to use TinaCMS. There's a problem though: our project is still using Gatsby's mdx parser instead of TinaCMS's. We'll need to do a bit of tweaking to get it working. First we'll edit the graphql query inside of gatsby-node.js
so that it returns the raw markdown.
const result = await graphql(`{allMdx(sort: { frontmatter: { date: ASC } }, limit: 1000) {nodes {Id+ body// ...
Using the raw markdown from body
we can generate a custom node that contains the markdown in a format that TinaCMS can read.
To do this, we'll import TinaCMS's markdown parser.
Add this import to the top of gatsby-node.js
.
+ const { parseMDX } = require("@tinacms/dist")
Then inside of onCreateNode
we'll create a new node containing an Abstract Syntax Tree we can pass into our TinaMarkdown component. We'll also need to serialize the result as a string so that we can return it as a single value from our page query.
Tina converts raw markdown into Abstract Syntax Trees containing the HTML needed to structure the page at build time.
exports.onCreateNode = ({ node, actions, getNode }) => {const { createNodeField } = actionsif (node.internal.type === `Mdx`) {const value = createFilePath({ node, getNode })+ createNodeField({+ name: `tinaMarkdown`,+ node,+ value: JSON.stringify(parseMDX(node.body)),+ })createNodeField({name: `slug`,node,value,})}}
There's still a few parameters we need to provide to parseMDX
before it can do it's job.
config.js
file.+ const imageCallback = url => {+ return url.replace(/^\./, "/images")+ }exports.onCreateNode = ({ node, actions, getNode }) => {const { createNodeField } = actionsif (node.internal.type === `Mdx`) {const value = createFilePath({ node, getNode })createNodeField({name: `tinaMarkdown`,node,value: JSON.stringify(parseMDX(node.body,+ {+ type: "rich-text",+ name: "body",+ label: "Body",+ isBody: true,+ },+ imageCallback)),})// ...
Now that we've defined our new custom node we should be ready to use it in our graphql query. Open src/templates/blog-post.js
and add our custom node to the page query.
export const pageQuery = graphql`query BlogPostBySlug($id: String!$previousPostId: String$nextPostId: String) {site {siteMetadata {title}}mdx(id: { eq: $id }) {id+ fields {+ tinaMarkdown+ }// ...`
Now we're ready to parse the formatted markdown into our component. We'll also remove the children
prop which normally contains the HTML from Gatsby's mdx parser.
const BlogPostTemplate = ({data: { previous, next, site, mdx: post },location,- children,}) => {const siteTitle = site.siteMetadata?.title || `Title`+ const tinaMarkdownContent = JSON.parse(post.fields.tinaMarkdown)return (<Layout location={location} title={siteTitle}><articleclassName="blog-post"itemScopeitemType="http://schema.org/Article"><header><h1 itemProp="headline">{post.frontmatter.title}</h1><p>{post.frontmatter.date}</p></header>- {children}+ <TinaMarkdown content={tinaMarkdownContent} /><hr />// ...)}
Unfortunately, we'll need to move images to our media directory to have them appear on our pages. In this case there's only one image
cp ./content/blog/hello-world/salty_egg.jpg static/images/salty_egg.jpg
We'll also need to update all of the lists to be the same format in content/blog/hello-world/index.md
You may need to update other elements on your own website. For a list of unsupported markdown elements for Tina see our guide.
- - Red+ * Red- - Green+ * Green- - Blue+ * Blue* Red* Green* Blue- - Red+ * Red- - Green+* Green- - Blue+* Blue```markdown+ * Red- - Green+ * Green- - Blue+ * Blue* Red* Green* Blue- - Red+ * Red- - Green+* Green- - Blue+* Blue```
Now we should be able to read and edit our existing pages in Tina.
We'll add some css to fix the images in our articles since they aren't being handled by to fix the width of our images since they're no longer being processed by gatsby.
Add the following to the top of src/style.css
. This will resize any images in our blog.
+ img {+ max-width+ }
Congratulations! Your Gatsby MDX blog is now set up with Tina. Run npm run develop
to test it out.
You can create React components in Gatsby as well, but you'll need to do some additional configuration compared to other frameworks.
First, we'll modify the schema for our blog posts, defining the new React component we're planning to add.
schema: {collections: [{name: "posts",label: "Posts",path: "content/blog",format: "mdx",fields: [{type: "string",name: "title",label: "Title",isTitle: true,required: true,},{type: "rich-text",name: "body",label: "Body",isBody: true,+ templates: [+ {+ name: "RichBlockQuote",+ label: "Rich Block Quote",+ fields: [+ {+ name: "children",+ label: "Body",+ type: "rich-text",+ },+ ],+ },+ ],},],},],},
Then we'll provide the new schema information to our MDX parser function, ensuring that it knows what to do when it encounters our custom element.
createNodeField({name: `tinaMarkdown`,node,value: JSON.stringify(parseMDX(node.body,{type: "rich-text",name: "body",label: "Body",isBody: true,+ templates: [+ {+ name: "RichBlockQuote",+ label: "Rich Block Quote",+ fields: [+ {+ name: "children",+ label: "Body",+ type: "rich-text",+ },+ ],+ },+ ],},imageCallback)),})
Next, we'll define how the custom component will look in blog-post.js
. We'll be parsing the child of the component into our TinaMarkdown renderer to give us rich text editing capabilities.
+ const components = {+ RichBlockQuote: props => {+ return (+ <blockquote>+ <TinaMarkdown content={props.children} />+ </blockquote>+ )+ },+ }
Setting the body to the built-in children
property in React allows us to use the children of our React component as a value.
This has the added benefit of making our markdown easy to read. For example, check out the example below.
<RichBlockQuote>### TinaCMS Rocks!Go check out the starter template on [tina.io](https://tina.io/docs/introduction/using-starter/)</RichBlockQuote>
The last thing you'll need to do is pass our component list into the components
prop of our TinaMarkdown
component.
return (<Layout location={location} title={siteTitle}><articleclassName="blog-post"itemScopeitemType="http://schema.org/Article"><header><h1 itemProp="headline">{post.frontmatter.title}</h1><p>{post.frontmatter.date}</p></header>- <TinaMarkdown content={tinaMarkdownContent} />+ <TinaMarkdown content={tinaMarkdownContent} components={components} />// ...
You're all done! You should be able to add our new component to your articles now.
Last Edited: October 29, 2024
© TinaCMS 2019–2024