home / skills / jezweb / claude-skills / tinacms
/skills/tinacms
This skill helps you configure TinaCMS with Git-backed editing for content sites, addressing ESBuild, module resolution, and media upload issues.
npx playbooks add skill jezweb/claude-skills --skill tinacmsReview the files below or copy the command above to add this skill to your agents.
---
name: tinacms
description: |
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.
user-invocable: true
allowed-tools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep']
---
# TinaCMS
Git-backed headless CMS with visual editing for content-heavy sites.
**Last Updated**: 2026-01-21
**Versions**: [email protected], @tinacms/[email protected]
---
## 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)
```bash
# 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:
```json
{
"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](https://github.com/tinacms/tinacms/issues/5838)
---
## Next.js Integration
**useTina Hook** (enables visual editing):
```tsx
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:
```typescript
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):
```typescript
{
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.
```typescript
// Example: Reference field referencing multiple collections
{
type: 'reference',
name: 'contributor',
collections: ['author', 'editor'] // Ensure shared fields have same type
}
```
**Source**: [Community-sourced](https://adamcogan.com/2024/08/27/7-important-updates-to-tinacms-2-0/)
---
## 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:
```typescript
// ❌ 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`
---
### 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:**
```bash
# 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)
---
### 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:**
```typescript
// ❌ 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.0` binding to accept external connections
**Solution:**
```bash
# 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:**
```yaml
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 `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)**
```typescript
{
name: 'post',
path: 'content/posts',
fields: [/* ... */] // No _template needed
}
```
**Option 2: Ensure `_template` exists in frontmatter**
```yaml
---
_template: article # ← Required when using templates array
title: My Post
---
```
**Migration Script** (if converting from templates to fields):
```bash
# 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:**
- `path` in collection config doesn't match actual file directory
- Relative vs absolute path confusion
- Trailing slash issues
**Solution:**
```typescript
// 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:**
1. Run `npx @tinacms/cli@latest audit` to check paths
2. Verify files exist in specified directory
3. Check file extensions match `format` field
---
### 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:**
```json
{
"scripts": {
"build": "tinacms build && next build" // ✅ Tina FIRST
// NOT: "build": "next build && tinacms build" // ❌ Wrong order
}
}
```
**CI/CD Example (GitHub Actions):**
```yaml
- 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
---
### 8. ❌ 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:**
```json
{
"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](https://adamcogan.com/2024/08/27/7-important-updates-to-tinacms-2-0/)
```typescript
// 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:**
```yaml
# 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**
```typescript
// 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**
```typescript
// 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/
---
### 10. ❌ 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:
1. Wait 5-10 seconds
2. Close and reopen Media Manager
3. Check if image already uploaded before retrying
4. Avoid duplicate upload attempts
**Status**: Known issue (high priority)
**Source**: [GitHub Issue #6325](https://github.com/tinacms/tinacms/issues/6325)
---
## Deployment Options
### TinaCloud (Managed) - Recommended
**Setup:**
1. Sign up at https://app.tina.io
2. Get Client ID and Read Only Token
3. Set env vars: `NEXT_PUBLIC_TINA_CLIENT_ID`, `TINA_TOKEN`
4. 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](https://github.com/tinacms/tinacms/issues/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](https://github.com/tinacms/tinacms/issues/6365)
**For Node.js environments only** (not edge runtime):
```bash
pnpm install @tinacms/datalayer tinacms-authjs
npx @tinacms/cli@latest init backend
```
**Example (Node.js server, not Workers)**:
```typescript
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)
This skill helps teams build and operate Git-backed, visual-editing sites using TinaCMS for blogs, docs, and marketing sites. It focuses on practical setup, Next.js/Vite/Write integrations, and a catalog of fixes for the 10 most common failures encountered in real projects. Use it to enable non-technical editors, stabilize CI/CD builds, and choose the right hosting path (TinaCloud vs self-hosted).
The skill inspects typical TinaCMS setup points: package manager choice, config/schema shape, collection paths, build ordering, and admin asset loading. It identifies root causes for ESbuild failures, module resolution, naming and path mismatches, Docker binding, reference-field timeouts, media upload UI timeouts, and edge runtime limitations. For each issue it provides minimal, actionable fixes and recommended configuration snippets to apply immediately.
Can TinaCMS run in Edge runtime environments (Cloudflare Workers, Vercel Edge)?
No. Self-hosted TinaCMS uses Node-only packages and will not work in Edge runtimes. Use TinaCloud (managed) for edge deployments.
Why pin exact TinaCMS and CLI versions?
UI assets are served from a CDN and can update independently of your local CLI. Pinning prevents mismatches that cause incompatibilities.