Build content-heavy sites with Git-backed TinaCMS. Provides visual editing for blogs, documentation, and marketing sites. Supports Next.js, Vite+React, and Astro with self-hosting options. Use when setting up CMS with non-technical editors or troubleshooting ESbuild compilation, module resolution, or Docker binding issues.
/plugin marketplace add jezweb/claude-skills/plugin install jezweb-tooling-skills@jezweb/claude-skillsThis skill is limited to using the following tools:
README.mdassets/links-to-official-docs.mdreferences/common-errors.mdscripts/check-versions.shtemplates/astro/astro.config.mjstemplates/astro/package.jsontemplates/astro/tina-config.tstemplates/cloudflare-worker-backend/src/index.tstemplates/cloudflare-worker-backend/wrangler.jsonctemplates/collections/author.tstemplates/collections/blog-post.tstemplates/collections/doc-page.tstemplates/collections/landing-page.tstemplates/nextjs/package.jsontemplates/nextjs/tina-config-app-router.tstemplates/nextjs/tina-config-pages-router.tstemplates/vite-react/package.jsontemplates/vite-react/tina-config.tstemplates/vite-react/vite.config.tsGit-backed headless CMS with visual editing for content-heavy sites.
Last Updated: 2026-01-09 Versions: tinacms@3.2.0, @tinacms/cli@2.0.7
# Initialize TinaCMS
npx @tinacms/cli@latest init
# Update package.json scripts
{
"dev": "tinacms dev -c \"next dev\"",
"build": "tinacms build && next build"
}
# Set environment variables
NEXT_PUBLIC_TINA_CLIENT_ID=your_client_id
TINA_TOKEN=your_read_only_token
# Start dev server
npm run dev
# Access admin interface
http://localhost:3000/admin/index.html
useTina Hook (enables visual editing):
import { useTina } from 'tinacms/dist/react'
import { client } from '../../tina/__generated__/client'
export default function BlogPost(props) {
const { data } = useTina({
query: props.query,
variables: props.variables,
data: props.data
})
return <article><h1>{data.post.title}</h1></article>
}
export async function getStaticProps({ params }) {
const response = await client.queries.post({
relativePath: `${params.slug}.md`
})
return {
props: {
data: response.data,
query: response.query,
variables: response.variables
}
}
}
App Router: Admin route at app/admin/[[...index]]/page.tsx
Pages Router: Admin route at pages/admin/[[...index]].tsx
tina/config.ts structure:
import { defineConfig } from 'tinacms'
export default defineConfig({
branch: process.env.GITHUB_BRANCH || 'main',
clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID,
token: process.env.TINA_TOKEN,
build: {
outputFolder: 'admin',
publicFolder: 'public',
},
schema: {
collections: [/* ... */],
},
})
Collection Example (Blog Post):
{
name: 'post', // Alphanumeric + underscores only
label: 'Blog Posts',
path: 'content/posts', // No trailing slash
format: 'mdx',
fields: [
{
type: 'string',
name: 'title',
label: 'Title',
isTitle: true,
required: true
},
{
type: 'rich-text',
name: 'body',
label: 'Body',
isBody: true
}
]
}
Field Types: string, rich-text, number, datetime, boolean, image, reference, object
Error Message:
ERROR: Schema Not Successfully Built
ERROR: Config Not Successfully Executed
Causes:
window, DOM APIs, React hooks)Solution:
Import only what you need:
// ❌ Bad - Imports entire component directory
import { HeroComponent } from '../components/'
// ✅ Good - Import specific file
import { HeroComponent } from '../components/blocks/hero'
Prevention Tips:
tina/config.ts imports minimal.schema.ts files if neededReference: See references/common-errors.md#esbuild
Error Message:
Error: Could not resolve "tinacms"
Causes:
Solution:
# Clear cache and reinstall
rm -rf node_modules package-lock.json
npm install
# Or with pnpm
rm -rf node_modules pnpm-lock.yaml
pnpm install
# Or with yarn
rm -rf node_modules yarn.lock
yarn install
Prevention:
package-lock.json, pnpm-lock.yaml, yarn.lock)--no-optional or --omit=optional flagsreact and react-dom are installed (even for non-React frameworks)Error Message:
Field name contains invalid characters
Cause:
Solution:
// ❌ Bad - Uses hyphens
{
name: 'hero-image',
label: 'Hero Image',
type: 'image'
}
// ❌ Bad - Uses spaces
{
name: 'hero image',
label: 'Hero Image',
type: 'image'
}
// ✅ Good - Uses underscores
{
name: 'hero_image',
label: 'Hero Image',
type: 'image'
}
// ✅ Good - CamelCase also works
{
name: 'heroImage',
label: 'Hero Image',
type: 'image'
}
Note: This is a breaking change from Forestry.io migration
Error:
Cause:
127.0.0.1 (localhost only) by default0.0.0.0 binding to accept external connectionsSolution:
# Ensure framework dev server listens on all interfaces
tinacms dev -c "next dev --hostname 0.0.0.0"
tinacms dev -c "vite --host 0.0.0.0"
tinacms dev -c "astro dev --host 0.0.0.0"
Docker Compose Example:
services:
app:
build: .
ports:
- "3000:3000"
command: npm run dev # Which runs: tinacms dev -c "next dev --hostname 0.0.0.0"
_template Key ErrorError Message:
GetCollection failed: Unable to fetch
template name was not provided
Cause:
templates array (multiple schemas)_template field in frontmattertemplates to fields and documents not updatedSolution:
Option 1: Use fields instead (recommended for single template)
{
name: 'post',
path: 'content/posts',
fields: [/* ... */] // No _template needed
}
Option 2: Ensure _template exists in frontmatter
---
_template: article # ← Required when using templates array
title: My Post
---
Migration Script (if converting from templates to fields):
# Remove _template from all files in content/posts/
find content/posts -name "*.md" -exec sed -i '/_template:/d' {} +
Error:
Cause:
path in collection config doesn't match actual file directorySolution:
// Files located at: content/posts/hello.md
// ✅ Correct
{
name: 'post',
path: 'content/posts', // Matches file location
fields: [/* ... */]
}
// ❌ Wrong - Missing 'content/'
{
name: 'post',
path: 'posts', // Files won't be found
fields: [/* ... */]
}
// ❌ Wrong - Trailing slash
{
name: 'post',
path: 'content/posts/', // May cause issues
fields: [/* ... */]
}
Debugging:
npx @tinacms/cli@latest audit to check pathsformat fieldError Message:
ERROR: Cannot find module '../tina/__generated__/client'
ERROR: Property 'queries' does not exist on type '{}'
Cause:
tinacms buildSolution:
{
"scripts": {
"build": "tinacms build && next build" // ✅ Tina FIRST
// NOT: "build": "next build && tinacms build" // ❌ Wrong order
}
}
CI/CD Example (GitHub Actions):
- name: Build
run: |
npx @tinacms/cli@latest build # Generate types first
npm run build # Then build framework
Why This Matters:
tinacms build generates TypeScript types in tina/__generated__/Error Message:
Failed to load resource: net::ERR_CONNECTION_REFUSED
http://localhost:4001/...
Causes:
admin/index.html to production (loads assets from localhost)basePath not configuredSolution:
For Production Deploys:
{
"scripts": {
"build": "tinacms build && next build" // ✅ Always build
// NOT: "build": "tinacms dev" // ❌ Never dev in production
}
}
For Subdirectory Deployments:
// tina/config.ts
export default defineConfig({
build: {
outputFolder: 'admin',
publicFolder: 'public',
basePath: 'your-subdirectory' // ← Set if site not at domain root
}
})
CI/CD Fix:
# GitHub Actions / Vercel / Netlify
- run: npx @tinacms/cli@latest build # Always use build, not dev
Error:
Cause:
Solutions:
Option 1: Split collections
// Instead of one huge "authors" collection
// Split by active status or alphabetically
{
name: 'active_author',
label: 'Active Authors',
path: 'content/authors/active',
fields: [/* ... */]
}
{
name: 'archived_author',
label: 'Archived Authors',
path: 'content/authors/archived',
fields: [/* ... */]
}
Option 2: Use string field with validation
// Instead of reference
{
type: 'string',
name: 'authorId',
label: 'Author ID',
ui: {
component: 'select',
options: ['author-1', 'author-2', 'author-3'] // Curated list
}
}
Option 3: Custom field component (advanced)
Setup:
NEXT_PUBLIC_TINA_CLIENT_ID, TINA_TOKENPros: Zero config, free tier (10k requests/month)
npm install @tinacms/datalayer tinacms-authjs
npx @tinacms/cli@latest init backend
workers/src/index.ts:
import { TinaNodeBackend, LocalBackendAuthProvider } from '@tinacms/datalayer'
import { AuthJsBackendAuthProvider, TinaAuthJSOptions } from 'tinacms-authjs'
import databaseClient from '../../tina/__generated__/databaseClient'
const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === 'true'
export default {
async fetch(request: Request, env: Env) {
const handler = TinaNodeBackend({
authProvider: isLocal
? LocalBackendAuthProvider()
: AuthJsBackendAuthProvider({
authOptions: TinaAuthJSOptions({
databaseClient,
secret: env.NEXTAUTH_SECRET,
}),
}),
databaseClient,
})
return handler(request)
}
}
Pros: Full control, 100k requests/day free tier, global edge network
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.