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 TinaCloud or Node.js self-hosting. Prevents 10 documented errors. Use when setting up CMS with non-technical editors or troubleshooting ESbuild compilation, module resolution, package manager compatibility, edge runtime limitations, or media upload timeouts.
Git-backed headless CMS with visual editing for content-heavy sites.
Last Updated: 2026-01-21 Versions: tinacms@3.3.1, @tinacms/cli@2.1.1
Package Manager Recommendation:
# Install pnpm (if needed)
npm install -g pnpm
# Initialize TinaCMS
npx @tinacms/cli@latest init
# Install dependencies with pnpm
pnpm install
# 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
pnpm run dev
# Access admin interface
http://localhost:3000/admin/index.htmlVersion Locking (Recommended):
Pin exact versions to prevent breaking changes from automatic CLI/UI updates:
{
"dependencies": {
"tinacms": "3.3.1", // NOT "^3.3.1"
"@tinacms/cli": "2.1.1"
}
}Why: TinaCMS UI assets are served from CDN and may update before your local CLI, causing incompatibilities.
Source: GitHub Issue #5838
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
Reference Field Note: When a reference field references multiple collection types with shared field names, ensure the field types match. Conflicting types (e.g., bio: string vs bio: rich-text) cause GraphQL schema errors.
// Example: Reference field referencing multiple collections
{
type: 'reference',
name: 'contributor',
collections: ['author', 'editor'] // Ensure shared fields have same type
}Source: Community-sourced
Error Message:
ERROR: Schema Not Successfully Built
ERROR: Config Not Successfully ExecutedCauses:
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 installPrevention:
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 charactersCause:
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 providedCause:
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 frameworkWhy 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:
⚠️ Sub-path Deployment Limitation: TinaCMS has known issues loading assets correctly when deployed to a sub-path (e.g.,
example.com/cms/admininstead ofexample.com/admin). This is a limitation even withbasePathconfiguration.Workaround: Deploy TinaCMS admin at root path (
/admin) or use reverse proxy rewrite rules.Source: Community-sourced
// tina/config.ts
export default defineConfig({
build: {
outputFolder: 'admin',
publicFolder: 'public',
basePath: 'your-subdirectory' // ← May have asset loading issues on sub-paths
}
})CI/CD Fix:
# GitHub Actions / Vercel / Netlify
- run: npx @tinacms/cli@latest build # Always use build, not devError:
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)
Error Message:
Upload failed
Error uploading imageCause:
Solution:
If upload shows error:
Status: Known issue (high priority) Source: GitHub Issue #6325
Setup:
NEXT_PUBLIC_TINA_CLIENT_ID, TINA_TOKENPros: Zero config, free tier (10k requests/month)
⚠️ Edge Runtime Limitation: Self-hosted TinaCMS does NOT work in Edge Runtime environments (Cloudflare Workers, Vercel Edge Functions) due to Node.js dependencies in
@tinacms/datalayerand@tinacms/graphql. Use TinaCloud (managed service) for edge deployments.Source: GitHub Issue #4363 (labeled "wontfix")
⚠️ Self-Hosted Examples May Be Outdated: Official self-hosted examples in the TinaCMS repository are acknowledged by the team as "quite out of date". Always cross-reference with latest documentation instead of relying solely on example repos.
Source: GitHub Issue #6365
For Node.js environments only (not edge runtime):
pnpm install @tinacms/datalayer tinacms-authjs
npx @tinacms/cli@latest init backendExample (Node.js server, not Workers):
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'
// This ONLY works in Node.js runtime, NOT edge runtime
const handler = TinaNodeBackend({
authProvider: isLocal
? LocalBackendAuthProvider()
: AuthJsBackendAuthProvider({
authOptions: TinaAuthJSOptions({
databaseClient,
secret: process.env.NEXTAUTH_SECRET,
}),
}),
databaseClient,
})Pros: Full control, self-hosted Cons: Requires Node.js runtime (cannot use edge computing)
fa91c34
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.