tinacms
Expert guidance for TinaCMS, the open-source headless CMS that stores content in Git (Markdown/MDX/JSON) and provides visual editing capabilities. Helps developers set up TinaCMS with Next.js, define content schemas, and build visual editing experiences where editors can see changes in real time.
Usage
Getting Started
- Install the skill using the command above
- Open your AI coding agent (Claude Code, Codex, Gemini CLI, or Cursor)
- Reference the skill in your prompt
- The AI will use the skill's capabilities automatically
Example Prompts
- "Write a blog post about the benefits of AI-assisted development"
- "Create social media copy for the product launch announcement"
Documentation
Overview
TinaCMS, the open-source headless CMS that stores content in Git (Markdown/MDX/JSON) and provides visual editing capabilities. Helps developers set up TinaCMS with Next.js, define content schemas, and build visual editing experiences where editors can see changes in real time.
Instructions
Schema Definition
Define your content structure in a type-safe schema:
// tina/config.ts — TinaCMS configuration with content schemas
import { defineConfig } from "tinacms";
export default defineConfig({
branch: process.env.NEXT_PUBLIC_TINA_BRANCH || "main",
clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID!,
token: process.env.TINA_TOKEN!,
build: {
outputFolder: "admin", // Admin UI served at /admin
publicFolder: "public",
},
media: {
tina: {
mediaRoot: "uploads", // Store uploaded images in /public/uploads
publicFolder: "public",
},
},
schema: {
collections: [
{
name: "post",
label: "Blog Posts",
path: "content/posts", // Store as Markdown files in this directory
format: "mdx", // mdx | md | json
fields: [
{
type: "string",
name: "title",
label: "Title",
isTitle: true, // Used as the display name in the CMS
required: true,
},
{
type: "string",
name: "description",
label: "Description",
ui: {
component: "textarea", // Multi-line text input
},
},
{
type: "datetime",
name: "publishedAt",
label: "Published Date",
required: true,
},
{
type: "image",
name: "heroImage",
label: "Hero Image",
},
{
type: "string",
name: "category",
label: "Category",
options: ["engineering", "product", "culture", "tutorial"],
},
{
type: "object",
name: "author",
label: "Author",
fields: [
{ type: "string", name: "name", label: "Name", required: true },
{ type: "image", name: "avatar", label: "Avatar" },
{ type: "string", name: "role", label: "Role" },
],
},
{
type: "rich-text",
name: "body",
label: "Content",
isBody: true, // Maps to the MDX body content
templates: [
// Custom MDX components available in the visual editor
{
name: "Callout",
label: "Callout Box",
fields: [
{
type: "string",
name: "type",
label: "Type",
options: ["info", "warning", "tip", "danger"],
},
{
type: "rich-text",
name: "children",
label: "Content",
},
],
},
{
name: "CodeBlock",
label: "Code Block",
fields: [
{ type: "string", name: "language", label: "Language" },
{ type: "string", name: "code", label: "Code", ui: { component: "textarea" } },
],
},
],
},
],
},
// Page collection — for marketing pages, landing pages
{
name: "page",
label: "Pages",
path: "content/pages",
format: "mdx",
fields: [
{ type: "string", name: "title", label: "Title", isTitle: true, required: true },
{
type: "object",
name: "seo",
label: "SEO",
fields: [
{ type: "string", name: "metaTitle", label: "Meta Title" },
{ type: "string", name: "metaDescription", label: "Meta Description" },
{ type: "image", name: "ogImage", label: "OG Image" },
],
},
{
type: "object",
name: "blocks",
label: "Page Blocks",
list: true, // Repeatable blocks — visual page builder
templates: [
{
name: "hero",
label: "Hero Section",
fields: [
{ type: "string", name: "heading", label: "Heading" },
{ type: "string", name: "subheading", label: "Subheading" },
{ type: "image", name: "backgroundImage", label: "Background" },
{ type: "string", name: "ctaText", label: "CTA Button Text" },
{ type: "string", name: "ctaLink", label: "CTA Link" },
],
},
{
name: "features",
label: "Features Grid",
fields: [
{ type: "string", name: "heading", label: "Section Heading" },
{
type: "object",
name: "items",
label: "Feature Items",
list: true,
fields: [
{ type: "string", name: "title", label: "Title" },
{ type: "string", name: "description", label: "Description" },
{ type: "image", name: "icon", label: "Icon" },
],
},
],
},
],
},
],
},
],
},
});
Visual Editing in Next.js
Render content with live visual editing:
// app/posts/[slug]/page.tsx — Blog post page with visual editing
import { client } from "@/tina/__generated__/client";
import { useTina } from "tinacms/dist/react";
import { TinaMarkdown } from "tinacms/dist/rich-text";
// Components for custom MDX blocks
const components = {
Callout: ({ type, children }: any) => (
<div className={`callout callout-${type}`}>
{type === "tip" && "💡"}
{type === "warning" && "⚠️"}
{type === "danger" && "🚨"}
{type === "info" && "ℹ️"}
<TinaMarkdown content={children} />
</div>
),
CodeBlock: ({ language, code }: any) => (
<pre><code className={`language-${language}`}>{code}</code></pre>
),
};
// Server component: fetch data at build/request time
export default async function PostPage({ params }: { params: { slug: string } }) {
const { data, query, variables } = await client.queries.post({
relativePath: `${params.slug}.mdx`,
});
return <PostClient data={data} query={query} variables={variables} />;
}
// Client component: enables visual editing when in Tina admin
function PostClient({ data, query, variables }: any) {
// useTina enables real-time editing — changes appear instantly
const { data: tinaData } = useTina({ query, variables, data });
const post = tinaData.post;
return (
<article>
{post.heroImage && <img src={post.heroImage} alt={post.title} />}
<h1>{post.title}</h1>
<p>{post.description}</p>
<div className="author">
{post.author?.avatar && <img src={post.author.avatar} alt="" />}
<span>{post.author?.name}</span>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</div>
{/* TinaMarkdown renders rich-text with custom components */}
<TinaMarkdown content={post.body} components={components} />
</article>
);
}
// Generate static paths for all posts
export async function generateStaticParams() {
const posts = await client.queries.postConnection();
return posts.data.postConnection.edges?.map((edge) => ({
slug: edge?.node?._sys.filename,
})) ?? [];
}
Querying Content
Use the auto-generated GraphQL client:
// src/lib/content.ts — Query content using Tina's generated client
import { client } from "@/tina/__generated__/client";
// Get all blog posts (sorted by date)
async function getAllPosts() {
const response = await client.queries.postConnection({
sort: "publishedAt",
last: 100, // Get latest 100 posts
});
return response.data.postConnection.edges?.map((edge) => ({
slug: edge?.node?._sys.filename,
title: edge?.node?.title,
description: edge?.node?.description,
publishedAt: edge?.node?.publishedAt,
category: edge?.node?.category,
author: edge?.node?.author,
})) ?? [];
}
// Get posts filtered by category
async function getPostsByCategory(category: string) {
const response = await client.queries.postConnection({
filter: {
category: { eq: category },
},
});
return response.data.postConnection.edges?.map((e) => e?.node) ?? [];
}
// Get a single post by filename
async function getPost(slug: string) {
const response = await client.queries.post({
relativePath: `${slug}.mdx`,
});
return response.data.post;
}
Installation
# Add to an existing Next.js project
npx @tinacms/cli@latest init
# This creates:
# - tina/config.ts (schema configuration)
# - tina/__generated__/ (auto-generated client and types)
# Run development server with visual editing
npx tinacms dev -c "next dev"
# Build for production
npx tinacms build && next build
Examples
Example 1: Setting up Tinacms with a custom configuration
User request:
I just installed Tinacms. Help me configure it for my TypeScript + React workflow with my preferred keybindings.
The agent creates the configuration file with TypeScript-aware settings, configures relevant plugins/extensions for React development, sets up keyboard shortcuts matching the user's preferences, and verifies the setup works correctly.
Example 2: Extending Tinacms with custom functionality
User request:
I want to add a custom visual editing in next.js to Tinacms. How do I build one?
The agent scaffolds the extension/plugin project, implements the core functionality following Tinacms's API patterns, adds configuration options, and provides testing instructions to verify it works end-to-end.
Guidelines
- Git is your database — Content lives in Markdown/JSON files in your repo; every edit creates a git commit
- Use MDX for rich content — MDX lets editors use custom React components (callouts, embeds, interactive elements)
- Define templates for blocks — Page builder patterns (hero, features, CTA) give editors flexibility without code
- Type-safe queries — Tina generates TypeScript types from your schema; use the generated client, not raw GraphQL
- Visual editing in dev — Run
tinacms devlocally for real-time editing preview; editors see changes as they type - Branch-based workflow — Editors can work on branches; content changes go through PR review like code
- Media in Git LFS — For repos with many images, use Git LFS to keep the repo size manageable
- Self-host for control — Tina Cloud handles auth and Git; self-host the backend for full control over the editing API
Information
- Version
- 1.0.0
- Author
- terminal-skills
- Category
- Content
- License
- Apache-2.0