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.
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
skills listSkill Instructions
name: tinacms description: | Build content-heavy sites with Git-backed TinaCMS. Provides visual editing and content management for blogs, documentation, and marketing sites with non-technical editors.
Use when implementing Next.js, Vite+React, or Astro CMS setups, self-hosting on Cloudflare Workers, or troubleshooting ESbuild compilation errors, module resolution issues, or Docker binding problems. allowed-tools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep']
TinaCMS
Git-backed headless CMS with visual editing for content-heavy sites.
Last Updated: 2025-11-28 Versions: tinacms@2.10.0, @tinacms/cli@1.12.5
Quick Start
# 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
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
Common Errors & Solutions
1. ❌ 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.tsimports minimal - Only import type definitions and simple utilities
- Avoid importing UI components directly
- Create separate
.schema.tsfiles if needed
Reference: See references/common-errors.md#esbuild
2. ❌ 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-optionalor--omit=optionalflags - Ensure
reactandreact-domare installed (even for non-React frameworks)
3. ❌ 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
4. ❌ 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.0binding 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"
5. ❌ Missing _template Key Error
Error Message:
GetCollection failed: Unable to fetch
template name was not provided
Cause:
- Collection uses
templatesarray (multiple schemas) - Document missing
_templatefield in frontmatter - Migrating from
templatestofieldsand 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' {} +
6. ❌ Path Mismatch Issues
Error:
- Files not appearing in Tina admin
- "File not found" errors when saving
- GraphQL queries return empty results
Cause:
pathin 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 auditto check paths - Verify files exist in specified directory
- Check file extensions match
formatfield
7. ❌ 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 buildgenerates TypeScript types intina/__generated__/- Framework build needs these types to compile successfully
- Running in wrong order causes type errors
8. ❌ Failed Loading TinaCMS Assets
Error Message:
Failed to load resource: net::ERR_CONNECTION_REFUSED
http://localhost:4001/...
Causes:
- Pushed development
admin/index.htmlto production (loads assets from localhost) - Site served on subdirectory but
basePathnot configured
Solution:
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
9. ❌ 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/
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 Cloudflare Workers
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
More by jezweb
View allSelf-hosted auth for TypeScript/Cloudflare Workers with social auth, 2FA, passkeys, organizations, RBAC, and 15+ plugins. Requires Drizzle ORM or Kysely for D1 (no direct adapter). Self-hosted alternative to Clerk/Auth.js. Use when: self-hosting auth on D1, building OAuth provider, multi-tenant SaaS, or troubleshooting D1 adapter errors, session caching, rate limits.
/review-skill - Skill Audit Command: Comprehensive skill documentation audit with automated checks and manual review phases.
Build rich text editors with Tiptap - headless editor framework with React and Tailwind v4. Covers SSR-safe setup, image uploads, prose styling, and collaborative editing. Use when creating blog editors, comment systems, or Notion-like apps, or troubleshooting SSR hydration errors, typography issues, or image upload problems.
Run LLMs and AI models on Cloudflare's GPU network with Workers AI. Includes Llama 4, Gemma 3, Mistral 3.1, Flux images, BGE embeddings, streaming, and AI Gateway. Handles 2025 breaking changes. Use when: implementing LLM inference, images, RAG, or troubleshooting AI_ERROR, rate limits, max_tokens, BGE pooling.
