tinacms

$npx mdskill add TerminalSkills/skills/tinacms

Configure TinaCMS Git-backed CMS with Next.js and visual editors.

  • Sets up type-safe content schemas and admin interfaces.
  • Integrates with Next.js, Markdown, MDX, and Git repositories.
  • Executes configuration based on environment variables and schema definitions.
  • Delivers ready-to-use admin UI for real-time content editing.
SKILL.md
.github/skills/tinacmsView on GitHub ↗
---
name: tinacms
description: 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.
license: Apache-2.0
compatibility: No special requirements
metadata:
  author: terminal-skills
  version: 1.0.0
  category: content
  tags:
  - cms
  - headless-cms
  - git-backed
  - markdown
  - visual-editing
---

# 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 the auto-generated GraphQL client:

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

```bash
# 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

1. **Git is your database** — Content lives in Markdown/JSON files in your repo; every edit creates a git commit
2. **Use MDX for rich content** — MDX lets editors use custom React components (callouts, embeds, interactive elements)
3. **Define templates for blocks** — Page builder patterns (hero, features, CTA) give editors flexibility without code
4. **Type-safe queries** — Tina generates TypeScript types from your schema; use the generated client, not raw GraphQL
5. **Visual editing in dev** — Run `tinacms dev` locally for real-time editing preview; editors see changes as they type
6. **Branch-based workflow** — Editors can work on branches; content changes go through PR review like code
7. **Media in Git LFS** — For repos with many images, use Git LFS to keep the repo size manageable
8. **Self-host for control** — Tina Cloud handles auth and Git; self-host the backend for full control over the editing API
More from TerminalSkills/skills