Claude
Skills
Sign in
Back

tinacms

Included with Lifetime
$97 forever

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.

Web Dev

What this skill does


# TinaCMS — Git-Backed Visual CMS


## 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:

```typescript
// 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:

```tsx
// 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 
Files: 2
Size: 13.1 KB
Complexity: 24/100
Category: Web Dev

Related in Web Dev