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.
Limited to specific tools
Additional assets for this skill
This 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.tsname: 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.
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
# 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