TinaCMS
React-based Git headless CMS. Next-generation CMS that balances visual editing with code management.
CMS
TinaCMS
Overview
TinaCMS is a React-based Git-based headless CMS that balances visual editing with code management, representing the next generation of developer-first CMS.
Details
TinaCMS is a next-generation Git-based CMS developed by the Forestry.io team, combining visual editing capabilities with developer-friendly workflows in an innovative CMS. It fully leverages TypeScript and React to provide type safety and a modern development experience.
What sets it apart is the visual editing through "click-to-edit" functionality. Users can directly click and edit content while viewing the website, making it intuitive even for non-technical users. At the same time, by adopting a Git-based workflow, developers can maintain their familiar version control processes.
It provides a GraphQL API and covers all features necessary for modern development, including MDX support, real-time preview, and branch-based editing. Integration with Next.js and Gatsby is particularly excellent, making it ideal for JAMstack projects.
Pros and Cons
Pros
- Visual editing: Intuitive content editing with click-to-edit
- Type safety: Full TypeScript support prevents errors in advance
- Modern DX: Latest development experience based on React/TypeScript
- Git integration: Seamless integration with version control and CI/CD
- GraphQL API: Efficient data fetching
- MDX support: Enables rich content creation
- Open source: Free customization and extension
Cons
- React-specific: Difficult to use with other frameworks
- High learning curve: Requires knowledge of TypeScript/React/GraphQL
- New product: Ecosystem still developing
- Complex configuration: Initial setup takes time
- Resource usage: Visual editing features can be resource-intensive
Main Links
- TinaCMS Official Site
- TinaCMS Documentation
- TinaCMS GitHub
- TinaCMS Starter Templates
- TinaCMS Community
- TinaCMS Cloud
Usage Examples
Project Initialization
# Create new project
npx create-tina-app@latest
# Add to existing project
npx @tinacms/cli init
# Start development server
npm run dev
# TinaCMS admin panel: http://localhost:3000/admin
Schema Definition (Type-safe)
// .tina/schema.ts
import { defineSchema, defineConfig } from "@tinacms/cli";
export const schema = defineSchema({
collections: [
{
label: "Articles",
name: "article",
path: "content/articles",
format: "mdx",
fields: [
{
type: "string",
label: "Title",
name: "title",
required: true,
},
{
type: "datetime",
label: "Published Date",
name: "publishedAt",
},
{
type: "reference",
label: "Author",
name: "author",
collections: ["author"],
},
{
type: "rich-text",
label: "Content",
name: "body",
isBody: true,
templates: [
{
name: "CodeBlock",
label: "Code Block",
fields: [
{
name: "language",
label: "Language",
type: "string",
},
{
name: "code",
label: "Code",
type: "string",
ui: {
component: "textarea",
},
},
],
},
],
},
],
},
],
});
export default defineConfig({
schema,
branch: "main",
clientId: process.env.TINA_CLIENT_ID!,
token: process.env.TINA_TOKEN!,
build: {
publicFolder: "public",
outputFolder: "admin",
},
});
Next.js Integration and Visual Editing
// pages/posts/[slug].tsx
import { useTina } from 'tinacms/dist/react'
import { client } from '../../.tina/__generated__/client'
export default function Post(props: {
data: any
variables: any
query: string
}) {
// Enable visual editing
const { data } = useTina({
query: props.query,
variables: props.variables,
data: props.data,
})
return (
<article>
<h1>{data.article.title}</h1>
<TinaMarkdown content={data.article.body} />
</article>
)
}
export const getStaticProps = async ({ params }: any) => {
const { data, query, variables } = await client.queries.article({
relativePath: params.slug + '.mdx',
})
return {
props: {
data,
query,
variables,
},
}
}
GraphQL Query Execution
// Custom GraphQL query
import { client } from './.tina/__generated__/client'
export async function getArticles() {
const articlesResponse = await client.queries.articleConnection()
return articlesResponse.data.articleConnection.edges?.map(
(article) => ({
title: article.node?.title,
slug: article.node?._sys.filename,
publishedAt: article.node?.publishedAt,
})
)
}
Custom Field Component
// Register custom field
import { wrapFieldsWithMeta } from 'tinacms'
const ColorPickerField = wrapFieldsWithMeta((props) => {
const { field, input } = props
return (
<div>
<label htmlFor={input.name}>{field.label}</label>
<input
type="color"
id={input.name}
value={input.value}
onChange={(e) => input.onChange(e.target.value)}
/>
</div>
)
})
// Register with CMS
export const tinaConfig = defineConfig({
// ...
cmsCallback: (cms) => {
cms.fields.add({
name: 'color',
Component: ColorPickerField,
})
},
})
Switching Between Local Development and Cloud
// tina/database.ts
import { createDatabase, createLocalDatabase } from "@tinacms/datalayer"
import { GitHubProvider } from "tinacms-gitprovider-github"
const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === "true"
export default isLocal
? createLocalDatabase() // Local development
: createDatabase({
gitProvider: new GitHubProvider({
branch: process.env.GITHUB_BRANCH!,
owner: process.env.GITHUB_OWNER!,
repo: process.env.GITHUB_REPO!,
token: process.env.GITHUB_PERSONAL_ACCESS_TOKEN!,
}),
})