TinaCMS
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
Quick Start
Package Manager Recommendation:
-
Recommended: pnpm (required for TinaCMS >2.7.3)
-
Alternative: npm or yarn (may have module resolution issues in newer versions)
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.html
Version 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
Next.js Integration
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
Schema Configuration
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
Common Errors & Solutions
- ❌ ESbuild Compilation Errors
Error Message:
ERROR: Schema Not Successfully Built ERROR: Config Not Successfully Executed
Causes:
-
Importing code with custom loaders (webpack, babel plugins, esbuild loaders)
-
Importing frontend-only code (uses window , DOM APIs, React hooks)
-
Importing entire component libraries instead of specific modules
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:
-
Keep tina/config.ts imports minimal
-
Only import type definitions and simple utilities
-
Avoid importing UI components directly
-
Create separate .schema.ts files if needed
Reference: See references/common-errors.md#esbuild
- ❌ Module Resolution: "Could not resolve 'tinacms'"
Error Message:
Error: Could not resolve "tinacms"
Causes:
-
Corrupted or incomplete installation
-
Version mismatch between dependencies
-
Missing peer dependencies
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:
-
Use lockfiles (package-lock.json , pnpm-lock.yaml , yarn.lock )
-
Don't use --no-optional or --omit=optional flags
-
Ensure react and react-dom are installed (even for non-React frameworks)
- ❌ Field Naming Constraints
Error Message:
Field name contains invalid characters
Cause:
-
TinaCMS field names can only contain: letters, numbers, underscores
-
Hyphens, spaces, special characters are NOT allowed
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
- ❌ Docker Binding Issues
Error:
- TinaCMS admin not accessible from outside Docker container
Cause:
-
TinaCMS binds to 127.0.0.1 (localhost only) by default
-
Docker containers need 0.0.0.0 binding to accept external connections
Solution:
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"
- ❌ Missing _template Key Error
Error Message:
GetCollection failed: Unable to fetch template name was not provided
Cause:
-
Collection uses templates array (multiple schemas)
-
Document missing _template field in frontmatter
-
Migrating from templates to fields and documents not updated
Solution:
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' {} +
- ❌ Path Mismatch Issues
Error:
-
Files not appearing in Tina admin
-
"File not found" errors when saving
-
GraphQL queries return empty results
Cause:
-
path in collection config doesn't match actual file directory
-
Relative vs absolute path confusion
-
Trailing slash issues
Solution:
// 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:
-
Run npx @tinacms/cli@latest audit to check paths
-
Verify files exist in specified directory
-
Check file extensions match format field
- ❌ Build Script Ordering Problems
Error Message:
ERROR: Cannot find module '../tina/generated/client' ERROR: Property 'queries' does not exist on type '{}'
Cause:
-
Framework build running before tinacms build
-
Tina types not generated before TypeScript compilation
-
CI/CD pipeline incorrect order
Solution:
{ "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/
-
Framework build needs these types to compile successfully
-
Running in wrong order causes type errors
- ❌ Failed Loading TinaCMS Assets
Error Message:
Failed to load resource: net::ERR_CONNECTION_REFUSED http://localhost:4001/...
Causes:
-
Pushed development admin/index.html to production (loads assets from localhost)
-
Site served on subdirectory but basePath not configured
Solution:
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/admin instead of example.com/admin ). This is a limitation even with basePath configuration.
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 dev
- ❌ Reference Field 503 Service Unavailable
Error:
-
Reference field dropdown times out with 503 error
-
Admin interface becomes unresponsive when loading reference field
Cause:
-
Too many items in referenced collection (100s or 1000s)
-
No pagination support for reference fields currently
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)
-
Implement pagination in custom component
-
See TinaCMS docs: https://tina.io/docs/extending-tina/custom-field-components/
- ❌ Media Manager Upload Timeouts (Ghost Uploads)
Error Message:
Upload failed Error uploading image
Cause:
-
Media Manager shows error but image uploads successfully in background
-
UI timeout doesn't reflect actual upload status
-
Similar issue occurs with deletion (error shown but deletion succeeds)
Solution:
If upload shows error:
-
Wait 5-10 seconds
-
Close and reopen Media Manager
-
Check if image already uploaded before retrying
-
Avoid duplicate upload attempts
Status: Known issue (high priority) Source: GitHub Issue #6325
Deployment Options
TinaCloud (Managed) - Recommended
Setup:
-
Sign up at https://app.tina.io
-
Get Client ID and Read Only Token
-
Set env vars: NEXT_PUBLIC_TINA_CLIENT_ID , TINA_TOKEN
-
Deploy to Vercel/Netlify/Cloudflare Pages
Pros: Zero config, free tier (10k requests/month)
Self-Hosted on Node.js
⚠️ 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/datalayer and @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 backend
Example (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)