
Tinacms
- 60 installs
- 51 repo stars
- Updated November 25, 2025
- ovachiever/droid-tings
Helps with ai & agent building tasks during AI-assisted development.
About
tinacms is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- tinacms
- AI & Agent Building
- AI-coding skill
Tinacms by the numbers
- 60 all-time installs (skills.sh)
- Ranked #6,314 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ovachiever/droid-tings --skill tinacmsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 60 |
|---|---|
| repo stars | ★ 51 |
| Last updated | November 25, 2025 |
| Repository | ovachiever/droid-tings ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
TinaCMS Skill
Complete skill for integrating TinaCMS into modern web applications.
---
What is TinaCMS?
TinaCMS is an open-source, Git-backed headless content management system (CMS) that enables developers and content creators to collaborate seamlessly on content-heavy websites.
Key Features
1. Git-Backed Storage
- Content stored as Markdown, MDX, or JSON files in Git repository
- Full version control and change history
- No vendor lock-in - content lives in your repo
2. Visual/Contextual Editing
- Side-by-side editing experience
- Live preview of changes as you type
- WYSIWYG-like editing for Markdown content
3. Schema-Driven Content Modeling
- Define content structure in code (
tina/config.ts) - Type-safe GraphQL API auto-generated from schema
- Collections and fields system for organized content
4. Flexible Deployment
- TinaCloud: Managed service (easiest, free tier available)
- Self-Hosted: Cloudflare Workers, Vercel Functions, Netlify Functions, AWS Lambda
- Multiple authentication options (Auth.js, custom, local dev)
5. Framework Support
- Best: Next.js (App Router + Pages Router)
- Good: React, Astro (experimental visual editing), Gatsby, Hugo, Jekyll, Remix, 11ty
- Framework-Agnostic: Works with any framework (visual editing limited to React)
Current Versions
- tinacms: 2.9.0 (September 2025)
- @tinacms/cli: 1.11.0 (October 2025)
- React Support: 19.x (>=18.3.1 <20.0.0)
---
When to Use This Skill
✅ Use TinaCMS When:
1. Building Content-Heavy Sites
- Blogs and personal websites
- Documentation sites
- Marketing websites
- Portfolio sites
2. Non-Technical Editors Need Access
- Content teams without coding knowledge
- Marketing teams managing pages
- Authors writing blog posts
3. Git-Based Workflow Desired
- Want content versioning through Git
- Need content review through pull requests
- Prefer content in repository with code
4. Visual Editing Required
- Editors want to see changes live
- WYSIWYG experience preferred
- Side-by-side editing workflow
❌ Don't Use TinaCMS When:
1. Real-Time Collaboration Needed
- Multiple users editing simultaneously (Google Docs-style)
- Use Sanity, Contentful, or Firebase instead
2. Highly Dynamic Data
- E-commerce product catalogs with frequent inventory changes
- Real-time dashboards
- Use traditional databases (D1, PostgreSQL) instead
3. No Content Management Needed
- Application is data-driven, not content-driven
- Hard-coded content is sufficient
---
Setup Patterns by Framework
Use the appropriate setup pattern based on your framework choice.
1. Next.js Setup (Recommended)
App Router (Next.js 13+)
Steps:
1. Initialize TinaCMS:
npx @tinacms/cli@latest init- When prompted for public assets directory, enter
public
2. Update package.json scripts:
{
"scripts": {
"dev": "tinacms dev -c \"next dev\"",
"build": "tinacms build && next build",
"start": "tinacms build && next start"
}
}3. Set environment variables:
# .env.local
NEXT_PUBLIC_TINA_CLIENT_ID=your_client_id
TINA_TOKEN=your_read_only_token4. Start development server:
npm run dev5. Access admin interface:
http://localhost:3000/admin/index.htmlKey Files Created:
tina/config.ts- Schema configurationapp/admin/[[...index]]/page.tsx- Admin UI route (if using App Router)
Template: See templates/nextjs/tina-config-app-router.ts
---
Pages Router (Next.js 12 and below)
Setup is identical, except admin route is:
pages/admin/[[...index]].tsxinstead of app directory
Data Fetching Pattern:
// pages/posts/[slug].tsx
import { client } from '../../tina/__generated__/client'
import { useTina } from 'tinacms/dist/react'
export default function BlogPost(props) {
// Hydrate for visual editing
const { data } = useTina({
query: props.query,
variables: props.variables,
data: props.data
})
return (
<article>
<h1>{data.post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: data.post.body }} />
</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
}
}
}
export async function getStaticPaths() {
const response = await client.queries.postConnection()
const paths = response.data.postConnection.edges.map((edge) => ({
params: { slug: edge.node._sys.filename }
}))
return { paths, fallback: 'blocking' }
}Template: See templates/nextjs/tina-config-pages-router.ts
---
2. Vite + React Setup
Steps:
1. Install dependencies:
npm install react@^19 react-dom@^19 tinacms2. Initialize TinaCMS:
npx @tinacms/cli@latest init- Set public assets directory to
public
3. Update vite.config.ts:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3000 // TinaCMS default
}
})4. Update package.json scripts:
{
"scripts": {
"dev": "tinacms dev -c \"vite\"",
"build": "tinacms build && vite build",
"preview": "vite preview"
}
}5. Create admin interface:
Option A: Manual route (React Router)
// src/pages/Admin.tsx
import TinaCMS from 'tinacms'
export default function Admin() {
return <div id="tina-admin" />
}Option B: Direct HTML
<!-- public/admin/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Tina CMS</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/@fs/[path-to-tina-admin]"></script>
</body>
</html>6. Use useTina hook for visual editing:
import { useTina } from 'tinacms/dist/react'
import { client } from '../tina/__generated__/client'
function BlogPost({ initialData }) {
const { data } = useTina({
query: initialData.query,
variables: initialData.variables,
data: initialData.data
})
return (
<article>
<h1>{data.post.title}</h1>
<div>{/* render body */}</div>
</article>
)
}7. Set environment variables:
# .env
VITE_TINA_CLIENT_ID=your_client_id
VITE_TINA_TOKEN=your_read_only_tokenTemplate: See templates/vite-react/
---
3. Astro Setup
Steps:
1. Use official starter (recommended):
npx create-tina-app@latest --template tina-astro-starterOr initialize manually:
npx @tinacms/cli@latest init2. Update package.json scripts:
{
"scripts": {
"dev": "tinacms dev -c \"astro dev\"",
"build": "tinacms build && astro build",
"preview": "astro preview"
}
}3. Configure Astro:
// astro.config.mjs
import { defineConfig } from 'astro/config'
import react from '@astro/react'
export default defineConfig({
integrations: [react()] // Required for Tina admin
})4. Visual editing (experimental):
- Requires React components
- Use
client:tinaDirectivefor interactive editing - Full visual editing is experimental as of October 2025
5. Set environment variables:
# .env
PUBLIC_TINA_CLIENT_ID=your_client_id
TINA_TOKEN=your_read_only_tokenBest For: Content-focused static sites, documentation, blogs
Template: See templates/astro/
---
4. Framework-Agnostic Setup
Applies to: Hugo, Jekyll, Eleventy, Gatsby, Remix, or any framework
Steps:
1. Initialize TinaCMS:
npx @tinacms/cli@latest init2. Manually configure build scripts:
{
"scripts": {
"dev": "tinacms dev -c \"<your-dev-command>\"",
"build": "tinacms build && <your-build-command>"
}
}3. Admin interface:
- Automatically created at
http://localhost:<port>/admin/index.html - Port depends on your framework
4. Data fetching:
- No visual editing (sidebar only)
- Content edited through Git-backed interface
- Changes saved directly to files
5. Set environment variables:
TINA_CLIENT_ID=your_client_id
TINA_TOKEN=your_read_only_tokenLimitations:
- No visual editing (React-only feature)
- Manual integration required
- Sidebar-based editing only
---
Schema Modeling Best Practices
Define your content structure in tina/config.ts.
Basic Config Structure
import { defineConfig } from 'tinacms'
export default defineConfig({
// Branch configuration
branch: process.env.GITHUB_BRANCH ||
process.env.VERCEL_GIT_COMMIT_REF ||
'main',
// TinaCloud credentials (if using managed service)
clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID,
token: process.env.TINA_TOKEN,
// Build configuration
build: {
outputFolder: 'admin',
publicFolder: 'public',
},
// Media configuration
media: {
tina: {
mediaRoot: '',
publicFolder: 'public',
},
},
// Content schema
schema: {
collections: [
// Define collections here
],
},
})---
Collections
Collection = Content type + directory mapping
{
name: 'post', // Singular, internal name (used in API)
label: 'Blog Posts', // Plural, display name (shown in admin)
path: 'content/posts', // Directory where files are stored
format: 'mdx', // File format: md, mdx, markdown, json, yaml, toml
fields: [/* ... */] // Array of field definitions
}Key Properties:
name: Internal identifier (alphanumeric + underscores only)label: Human-readable name for admin interfacepath: File path relative to project rootformat: File extension (defaults to 'md')fields: Content structure definition
---
Field Types Reference
| Type | Use Case | Example |
|---|---|---|
string | Short text (single line) | Title, slug, author name |
rich-text | Long formatted content | Blog body, page content |
number | Numeric values | Price, quantity, rating |
datetime | Date/time values | Published date, event time |
boolean | True/false toggles | Draft status, featured flag |
image | Image uploads | Hero image, thumbnail, avatar |
reference | Link to another document | Author, category, related posts |
object | Nested fields group | SEO metadata, social links |
Complete reference: See references/field-types-reference.md
---
Collection Templates
Blog Post Collection
{
name: 'post',
label: 'Blog Posts',
path: 'content/posts',
format: 'mdx',
fields: [
{
type: 'string',
name: 'title',
label: 'Title',
isTitle: true, // Shows in content list
required: true
},
{
type: 'string',
name: 'excerpt',
label: 'Excerpt',
ui: {
component: 'textarea' // Multi-line input
}
},
{
type: 'image',
name: 'coverImage',
label: 'Cover Image'
},
{
type: 'datetime',
name: 'date',
label: 'Published Date',
required: true
},
{
type: 'reference',
name: 'author',
label: 'Author',
collections: ['author'] // References author collection
},
{
type: 'boolean',
name: 'draft',
label: 'Draft',
description: 'If checked, post will not be published',
required: true
},
{
type: 'rich-text',
name: 'body',
label: 'Body',
isBody: true // Main content area
}
],
ui: {
router: ({ document }) => `/blog/${document._sys.filename}`
}
}Template: See templates/collections/blog-post.ts
---
Documentation Page Collection
{
name: 'doc',
label: 'Documentation',
path: 'content/docs',
format: 'mdx',
fields: [
{
type: 'string',
name: 'title',
label: 'Title',
isTitle: true,
required: true
},
{
type: 'string',
name: 'description',
label: 'Description',
ui: {
component: 'textarea'
}
},
{
type: 'number',
name: 'order',
label: 'Order',
description: 'Sort order in sidebar'
},
{
type: 'rich-text',
name: 'body',
label: 'Body',
isBody: true,
templates: [
// MDX components can be defined here
]
}
],
ui: {
router: ({ document }) => {
const breadcrumbs = document._sys.breadcrumbs.join('/')
return `/docs/${breadcrumbs}`
}
}
}Template: See templates/collections/doc-page.ts
---
Author Collection (Reference Target)
{
name: 'author',
label: 'Authors',
path: 'content/authors',
format: 'json', // Use JSON for structured data
fields: [
{
type: 'string',
name: 'name',
label: 'Name',
isTitle: true,
required: true
},
{
type: 'string',
name: 'email',
label: 'Email',
ui: {
validate: (value) => {
if (!value?.includes('@')) {
return 'Invalid email address'
}
}
}
},
{
type: 'image',
name: 'avatar',
label: 'Avatar'
},
{
type: 'string',
name: 'bio',
label: 'Bio',
ui: {
component: 'textarea'
}
},
{
type: 'object',
name: 'social',
label: 'Social Links',
fields: [
{
type: 'string',
name: 'twitter',
label: 'Twitter'
},
{
type: 'string',
name: 'github',
label: 'GitHub'
}
]
}
]
}Template: See templates/collections/author.ts
---
Landing Page Collection (Multiple Templates)
{
name: 'page',
label: 'Pages',
path: 'content/pages',
format: 'mdx',
templates: [ // Multiple templates for different page types
{
name: 'basic',
label: 'Basic Page',
fields: [
{
type: 'string',
name: 'title',
label: 'Title',
isTitle: true,
required: true
},
{
type: 'rich-text',
name: 'body',
label: 'Body',
isBody: true
}
]
},
{
name: 'landing',
label: 'Landing Page',
fields: [
{
type: 'string',
name: 'title',
label: 'Title',
isTitle: true,
required: true
},
{
type: 'object',
name: 'hero',
label: 'Hero Section',
fields: [
{
type: 'string',
name: 'headline',
label: 'Headline'
},
{
type: 'string',
name: 'subheadline',
label: 'Subheadline',
ui: { component: 'textarea' }
},
{
type: 'image',
name: 'image',
label: 'Hero Image'
}
]
},
{
type: 'object',
name: 'cta',
label: 'Call to Action',
fields: [
{
type: 'string',
name: 'text',
label: 'Button Text'
},
{
type: 'string',
name: 'url',
label: 'Button URL'
}
]
}
]
}
]
}When using templates: Documents must include _template field in frontmatter:
---
_template: landing
title: My Landing Page
---Template: See templates/collections/landing-page.ts
---
Common Errors & Solutions
1. ❌ ESbuild Compilation Errors
Error Message:
ERROR: Schema Not Successfully Built
ERROR: Config Not Successfully ExecutedCauses:
- 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 installPrevention:
- 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 charactersCause:
- 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 providedCause:
- 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: 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:
{
"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 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 Patterns
Choose the deployment approach that fits your needs.
Option 1: TinaCloud (Managed) - Easiest ⭐
Best For: Quick setup, free tier, managed infrastructure
Steps:
1. Sign up at https://app.tina.io 2. Create project, get Client ID and Read Only Token 3. Set environment variables:
NEXT_PUBLIC_TINA_CLIENT_ID=your_client_id
TINA_TOKEN=your_read_only_token4. Initialize backend:
npx @tinacms/cli@latest init backend5. Deploy to hosting provider (Vercel, Netlify, Cloudflare Pages) 6. Set up GitHub integration in Tina dashboard
Pros:
- ✅ Zero backend configuration
- ✅ Automatic GraphQL API
- ✅ Built-in authentication
- ✅ Git integration handled automatically
- ✅ Free tier generous (10k monthly requests)
Cons:
- ❌ Paid service beyond free tier
- ❌ Vendor dependency (content still in Git though)
Reference: See references/deployment-guide.md#tinacloud
---
Option 2: Self-Hosted on Cloudflare Workers 🔥
Best For: Full control, Cloudflare ecosystem, edge deployment
Steps:
1. Install dependencies:
npm install @tinacms/datalayer tinacms-authjs2. Initialize backend:
npx @tinacms/cli@latest init backend3. Create Workers endpoint:
// 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)
}
}4. Update `tina/config.ts`:
export default defineConfig({
contentApiUrlOverride: '/api/tina/gql', // Your Workers endpoint
// ... rest of config
})5. Configure `wrangler.jsonc`:
{
"name": "tina-backend",
"main": "workers/src/index.ts",
"compatibility_date": "2025-10-24",
"vars": {
"TINA_PUBLIC_IS_LOCAL": "false"
},
"env": {
"production": {
"vars": {
"NEXTAUTH_SECRET": "your-secret-here"
}
}
}
}6. Deploy:
npx wrangler deployPros:
- ✅ Full control over backend
- ✅ Generous free tier (100k requests/day)
- ✅ Global edge network (fast worldwide)
- ✅ No vendor lock-in
Cons:
- ❌ More setup complexity
- ❌ Authentication configuration required
- ❌ Cloudflare Workers knowledge needed
Complete Guide: See references/self-hosting-cloudflare.md
Template: See templates/cloudflare-worker-backend/
---
Option 3: Self-Hosted on Vercel Functions
Best For: Next.js projects, Vercel ecosystem
Steps:
1. Install dependencies:
npm install @tinacms/datalayer tinacms-authjs2. Create API route:
// api/tina/backend.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'
const handler = TinaNodeBackend({
authProvider: isLocal
? LocalBackendAuthProvider()
: AuthJsBackendAuthProvider({
authOptions: TinaAuthJSOptions({
databaseClient,
secret: process.env.NEXTAUTH_SECRET,
}),
}),
databaseClient,
})
export default handler3. Create `vercel.json` rewrites:
{
"rewrites": [
{
"source": "/api/tina/:path*",
"destination": "/api/tina/backend"
}
]
}4. Update dev script:
{
"scripts": {
"dev": "TINA_PUBLIC_IS_LOCAL=true tinacms dev -c \"next dev --port $PORT\""
}
}5. Set environment variables in Vercel dashboard:
NEXTAUTH_SECRET=your-secret
TINA_PUBLIC_IS_LOCAL=false6. Deploy:
vercel deployPros:
- ✅ Native Next.js integration
- ✅ Simple Vercel deployment
- ✅ Serverless (scales automatically)
Cons:
- ❌ Vercel-specific
- ❌ Function limitations (10s timeout, 50MB size)
Reference: See references/deployment-guide.md#vercel
---
Option 4: Self-Hosted on Netlify Functions
Steps:
1. Install dependencies:
npm install express serverless-http @tinacms/datalayer tinacms-authjs2. Create function:
// netlify/functions/tina.ts
import express from 'express'
import ServerlessHttp from 'serverless-http'
import { TinaNodeBackend, LocalBackendAuthProvider } from '@tinacms/datalayer'
import { AuthJsBackendAuthProvider, TinaAuthJSOptions } from 'tinacms-authjs'
import databaseClient from '../../tina/__generated__/databaseClient'
const app = express()
app.use(express.json())
const tinaBackend = TinaNodeBackend({
authProvider: AuthJsBackendAuthProvider({
authOptions: TinaAuthJSOptions({
databaseClient,
secret: process.env.NEXTAUTH_SECRET,
}),
}),
databaseClient,
})
app.post('/api/tina/*', tinaBackend)
app.get('/api/tina/*', tinaBackend)
export const handler = ServerlessHttp(app)3. Create `netlify.toml`:
[functions]
node_bundler = "esbuild"
[[redirects]]
from = "/api/tina/*"
to = "/.netlify/functions/tina"
status = 200
force = true4. Deploy:
netlify deploy --prodReference: See references/deployment-guide.md#netlify
---
Authentication Setup
Option 1: Local Development (Default)
Use for: Local development, no production deployment
// tina/__generated__/databaseClient or backend config
const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === 'true'
authProvider: isLocal ? LocalBackendAuthProvider() : /* ... */Environment Variable:
TINA_PUBLIC_IS_LOCAL=trueSecurity: NO authentication - only use locally!
---
Option 2: Auth.js (Recommended for Self-Hosted)
Use for: Self-hosted with OAuth providers (GitHub, Discord, Google, etc.)
Install:
npm install next-auth tinacms-authjsConfigure:
import { AuthJsBackendAuthProvider, TinaAuthJSOptions } from 'tinacms-authjs'
import DiscordProvider from 'next-auth/providers/discord'
export const AuthOptions = TinaAuthJSOptions({
databaseClient,
secret: process.env.NEXTAUTH_SECRET,
providers: [
DiscordProvider({
clientId: process.env.DISCORD_CLIENT_ID,
clientSecret: process.env.DISCORD_CLIENT_SECRET,
}),
// Add GitHub, Google, etc.
],
})
const handler = TinaNodeBackend({
authProvider: AuthJsBackendAuthProvider({
authOptions: AuthOptions,
}),
databaseClient,
})Supported Providers: GitHub, Discord, Google, Twitter, Facebook, Email, etc.
Reference: https://next-auth.js.org/providers/
---
Option 3: TinaCloud Auth (Managed)
Use for: TinaCloud hosted service
import { TinaCloudBackendAuthProvider } from '@tinacms/auth'
authProvider: TinaCloudBackendAuthProvider()Setup: 1. Sign up at https://app.tina.io 2. Create project 3. Manage users in dashboard 4. Automatically handles authentication
---
Option 4: Custom Auth Provider
Use for: Existing auth system, custom requirements
const CustomBackendAuth = () => {
return {
isAuthorized: async (req, res) => {
const token = req.headers.authorization
// Your validation logic
const user = await validateToken(token)
if (user && user.canEdit) {
return { isAuthorized: true }
}
return {
isAuthorized: false,
errorMessage: 'Unauthorized',
errorCode: 401
}
},
}
}
authProvider: CustomBackendAuth()---
GraphQL API Usage
TinaCMS automatically generates a type-safe GraphQL client.
Querying Data
TinaCloud:
import client from '../tina/__generated__/client'
// Single document
const post = await client.queries.post({
relativePath: 'hello-world.md'
})
// Multiple documents
const posts = await client.queries.postConnection()Self-Hosted:
import client from '../tina/__generated__/databaseClient'
// Same API as TinaCloud client
const post = await client.queries.post({
relativePath: 'hello-world.md'
})Visual Editing with useTina Hook
Next.js Example:
import { useTina } from 'tinacms/dist/react'
import { client } from '../../tina/__generated__/client'
export default function BlogPost(props) {
// Hydrate data for visual editing
const { data } = useTina({
query: props.query,
variables: props.variables,
data: props.data
})
return (
<article>
<h1>{data.post.title}</h1>
<p>{data.post.excerpt}</p>
<div dangerouslySetInnerHTML={{ __html: data.post.body }} />
</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
}
}
}How It Works:
- In production:
useTinareturns the initial data (no overhead) - In edit mode:
useTinaconnects to GraphQL and updates in real-time - Changes appear immediately in preview
---
Additional Resources
Templates
templates/nextjs/- Next.js App Router + Pages Router configstemplates/vite-react/- Vite + React setuptemplates/astro/- Astro integrationtemplates/collections/- Pre-built collection schemastemplates/cloudflare-worker-backend/- Cloudflare Workers self-hosting
References
references/schema-patterns.md- Advanced schema modeling patternsreferences/field-types-reference.md- Complete field type documentationreferences/deployment-guide.md- Deployment guides for all platformsreferences/self-hosting-cloudflare.md- Complete Cloudflare Workers guidereferences/common-errors.md- Extended error troubleshootingreferences/migration-guide.md- Migrating from Forestry.io
Scripts
scripts/init-nextjs.sh- Automated Next.js setupscripts/init-vite-react.sh- Automated Vite + React setupscripts/init-astro.sh- Automated Astro setupscripts/check-versions.sh- Verify package versions
Official Documentation
- Website: https://tina.io
- Docs: https://tina.io/docs
- GitHub: https://github.com/tinacms/tinacms
- Discord: https://discord.gg/zumN63Ybpf
---
Token Efficiency
Estimated Savings: 65-70% (10,900 tokens saved)
Without Skill (~16,000 tokens):
- Initial research and exploration: 3,000 tokens
- Framework setup trial & error: 2,500 tokens
- Schema modeling attempts: 2,000 tokens
- Error troubleshooting: 4,000 tokens
- Deployment configuration: 2,500 tokens
- Authentication setup: 2,000 tokens
With Skill (~5,100 tokens):
- Skill discovery: 100 tokens
- Skill loading (SKILL.md): 3,000 tokens
- Template selection: 500 tokens
- Minor project-specific adjustments: 1,500 tokens
---
Errors Prevented
This skill prevents 9 common errors (100% prevention rate):
1. ✅ ESbuild compilation errors (import issues) 2. ✅ Module resolution problems 3. ✅ Field naming constraint violations 4. ✅ Docker binding issues 5. ✅ Missing _template key errors 6. ✅ Path mismatch problems 7. ✅ Build script ordering failures 8. ✅ Asset loading errors in production 9. ✅ Reference field 503 timeouts
---
Quick Start Examples
Example 1: Blog with Next.js + TinaCloud
# 1. Create Next.js app
npx create-next-app@latest my-blog --typescript --app
# 2. Initialize TinaCMS
cd my-blog
npx @tinacms/cli@latest init
# 3. Set environment variables
echo "NEXT_PUBLIC_TINA_CLIENT_ID=your_client_id" >> .env.local
echo "TINA_TOKEN=your_token" >> .env.local
# 4. Start dev server
npm run dev
# 5. Access admin
open http://localhost:3000/admin/index.html---
Example 2: Documentation Site with Astro
# 1. Use official starter
npx create-tina-app@latest my-docs --template tina-astro-starter
# 2. Install dependencies
cd my-docs
npm install
# 3. Start dev server
npm run dev
# 4. Access admin
open http://localhost:4321/admin/index.html---
Example 3: Self-Hosted on Cloudflare Workers
# 1. Initialize project
npm create cloudflare@latest my-app
# 2. Add TinaCMS
npx @tinacms/cli@latest init
npx @tinacms/cli@latest init backend
# 3. Install dependencies
npm install @tinacms/datalayer tinacms-authjs
# 4. Copy Cloudflare Workers backend template
cp -r [path-to-skill]/templates/cloudflare-worker-backend/* workers/
# 5. Configure and deploy
npx wrangler deploy---
Production Examples
- TinaCMS Website: https://tina.io (dogfooding)
- Astro Starter: https://github.com/tinacms/tina-astro-starter
- Next.js Starter: https://github.com/tinacms/tina-starter-alpaca
---
Support
Issues? Check references/common-errors.md first
Still Stuck?
- Discord: https://discord.gg/zumN63Ybpf
- GitHub Issues: https://github.com/tinacms/tinacms/issues
- Official Docs: https://tina.io/docs
---
Last Updated: 2025-10-24 Skill Version: 1.0.0 TinaCMS Version: 2.9.0 CLI Version: 1.11.0
{
"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.",
"version": "1.0.0",
"author": {
"name": "Jeremy Dawes",
"email": "jeremy@jezweb.net"
},
"license": "MIT",
"repository": "https://github.com/jezweb/claude-skills",
"keywords": []
}
TinaCMS Official Documentation Links
Last Updated: 2025-10-24
---
Core Documentation
- Official Website: https://tina.io
- Documentation Home: https://tina.io/docs
- Getting Started: https://tina.io/docs/setup-overview
- What's New: https://tina.io/whats-new/tinacms
---
Setup & Configuration
- Setup Overview: https://tina.io/docs/setup-overview
- Next.js Setup: https://tina.io/docs/frameworks/next/overview
- Next.js App Router: https://tina.io/docs/frameworks/next/app-router
- Next.js Pages Router: https://tina.io/docs/frameworks/next/pages-router
- Astro Setup: https://tina.io/docs/frameworks/astro
- Framework-Agnostic: https://tina.io/docs/frameworks/other
---
Schema & Content Modeling
- Schema Overview: https://tina.io/docs/schema
- Content Modeling Intro: https://tina.io/docs/content-model-intro
- Collections Reference: https://tina.io/docs/reference/collections
- Field Types: https://tina.io/docs/reference/types
- Templates: https://tina.io/docs/reference/templates
---
Visual Editing
- Contextual Editing: https://tina.io/docs/contextual-editing/overview
- React Visual Editing: https://tina.io/docs/contextual-editing/react
- Vue Visual Editing: https://tina.io/docs/contextual-editing/vue
---
Self-Hosting
- Self-Hosted Overview: https://tina.io/docs/self-hosted/overview
- Existing Site: https://tina.io/docs/self-hosted/existing-site
- Querying Data: https://tina.io/docs/self-hosted/querying-data
- Next.js Backend: https://tina.io/docs/reference/self-hosted/tina-backend/nextjs
- Vercel Functions: https://tina.io/docs/reference/self-hosted/tina-backend/vercel-functions
- Netlify Functions: https://tina.io/docs/reference/self-hosted/tina-backend/netlify-functions
---
Authentication
- Auth Overview: https://tina.io/docs/reference/self-hosted/auth-provider/overview
- Auth.js Provider: https://tina.io/docs/reference/self-hosted/auth-provider/authjs
- TinaCloud Provider: https://tina.io/docs/reference/self-hosted/auth-provider/tinacloud
- Custom Auth: https://tina.io/docs/reference/self-hosted/auth-provider/bring-your-own
---
Deployment
- TinaCloud: https://tina.io/docs/tinacloud/overview
- Migrating to Self-Hosted: https://tina.io/docs/self-hosted/migrating-from-tinacloud
- Next.js + Vercel: https://tina.io/docs/self-hosted/starters/nextjs-vercel
---
Errors & Troubleshooting
- FAQ: https://tina.io/docs/introduction/faq
- Error FAQ: https://tina.io/docs/errors/faq
- ESbuild Error: https://tina.io/docs/errors/esbuild-error
- Common Migration Errors: https://tina.io/docs/forestry/common-errors
- Contributing Troubleshooting: https://tina.io/docs/contributing/troubleshooting
---
Extending Tina
- Extending Overview: https://tina.io/docs/extending-tina/overview
- Custom Field Components: https://tina.io/docs/extending-tina/custom-field-components
- Format & Parse: https://tina.io/docs/extending-tina/format-and-parse
- Custom List Rendering: https://tina.io/docs/extending-tina/customize-list-ui
---
Media Management
- Media Overview: https://tina.io/docs/reference/media/overview
- Cloudinary: https://tina.io/docs/reference/media/external/cloudinary
- S3: https://tina.io/docs/reference/media/external/s3
- Custom Media Store: https://tina.io/docs/reference/media/external/authentication
---
GraphQL & Querying
- GraphQL Overview: https://tina.io/docs/graphql/overview
- CLI: https://tina.io/docs/graphql/cli
- Queries: https://tina.io/docs/graphql/queries
---
Migration Guides
- Forestry.io Migration: https://tina.io/docs/forestry/migrate
- Schema Migration: https://tina.io/docs/tinacloud/schema-migration
- Content Modeling (Forestry): https://tina.io/docs/forestry/content-modelling
---
Blog & Tutorials
- Blog Home: https://tina.io/blog
- Using TinaCMS with Next.js: https://tina.io/blog/using-tinacms-with-nextjs
- React 19 Support: https://tina.io/blog/react-19-support
- TinaCMS + Next.js Perfect Match: https://tina.io/blog/tina-cloud-and-nextjs-the-perfect-match
---
Community & Support
- Discord: https://discord.gg/zumN63Ybpf
- GitHub: https://github.com/tinacms/tinacms
- GitHub Issues: https://github.com/tinacms/tinacms/issues
- GitHub Discussions: https://github.com/tinacms/tinacms/discussions
- Twitter/X: https://twitter.com/tina_cms
---
Starter Templates
- Tina Astro Starter: https://github.com/tinacms/tina-astro-starter
- Tina Next.js Starter (Alpaca): https://github.com/tinacms/tina-starter-alpaca
- Tina Next.js Example: https://github.com/tinacms/tinacms-next-example
---
API Reference
- Config: https://tina.io/docs/reference/schema
- Collections: https://tina.io/docs/reference/collections
- Field Types: https://tina.io/docs/reference/types
- Templates: https://tina.io/docs/reference/templates
- UI Config: https://tina.io/docs/reference/collections#ui
---
Last Updated: 2025-10-24
TinaCMS Skill
Complete integration skill for TinaCMS - Git-backed headless CMS for content-heavy sites
---
Auto-Trigger Keywords
This skill automatically activates when you mention:
CMS & Content Management
- tinacms, tina cms, tina, tinacloud
- headless cms, git-backed cms, git cms, markdown cms
- content management system, cms setup, cms integration
- visual editing, contextual editing, live editing, wysiwyg markdown
- content modeling, content schema, content structure
Use Cases
- blog cms, blog setup, blog integration
- documentation cms, docs cms, documentation site
- marketing site cms, landing page cms
- portfolio cms, personal website cms
- content-heavy site, editorial workflow
Technologies
- next.js cms, nextjs tina, next tina
- vite cms, react cms, vite react tina
- astro cms, astro tina
- markdown editor, mdx editor, markdown management
- gatsby cms, hugo cms, jekyll cms, eleventy cms, remix cms
Features
- git-based content, content in repository, versioned content
- visual markdown editor, markdown preview, live preview
- schema-driven cms, graphql cms, type-safe cms
- self-hosted cms, cloudflare cms, vercel cms
Problems & Errors
- tinacms error, tina error, tinacms setup issue
- esbuild error tina, module resolution tina
- tinacms docker, tinacms 503, reference field error
- failed loading tinacms assets, tinacms build error
- tinacms path mismatch, _template error
- field naming constraints, tinacms alphanumeric
Deployment
- tinacms cloudflare, tina workers, tinacms vercel
- tinacms netlify, self-hosted tina, tina backend
- tinacloud setup, tina authentication, auth.js tina
---
What This Skill Does
Provides complete, production-tested patterns for integrating TinaCMS into modern web applications with support for:
- 4 Frameworks: Next.js (App Router + Pages Router), Vite + React, Astro, Framework-agnostic
- 3 Deployment Options: TinaCloud (managed), Cloudflare Workers, Vercel/Netlify Functions
- 4 Collection Templates: Blog posts, documentation pages, landing pages, authors
- 9 Common Errors Prevented: ESbuild, module resolution, field naming, Docker, _template, paths, build scripts, asset loading, reference fields
---
When to Use This Skill
✅ Use When:
- Building blogs, documentation sites, or marketing websites
- Non-technical users need to manage content
- Content should be versioned in Git with code
- Visual/live editing experience desired
- Self-hosting CMS backend required
❌ Don't Use When:
- Need real-time collaborative editing (use Sanity, Contentful instead)
- Building e-commerce with dynamic inventory (use database instead)
- No content management needed (hardcoded content fine)
---
What's Included
Templates
- Next.js: App Router + Pages Router configurations
- Vite + React: Complete Vite setup with Tina integration
- Astro: Astro starter configuration
- Collections: Blog post, doc page, landing page, author schemas
- Backend: Cloudflare Workers self-hosted backend
References
- Schema Patterns: Advanced collection and field modeling
- Field Types Reference: Complete documentation of all field types
- Deployment Guide: TinaCloud, Cloudflare, Vercel, Netlify
- Self-Hosting on Cloudflare: Complete Workers setup guide
- Common Errors: All 9 errors with causes and solutions
- Migration Guide: Forestry.io to TinaCMS migration
Scripts
- init-nextjs.sh: Automated Next.js setup
- init-vite-react.sh: Automated Vite + React setup
- init-astro.sh: Automated Astro setup
- check-versions.sh: Verify package versions are current
---
Features
Git-Backed Storage ✅
- Content stored as Markdown, MDX, JSON in Git repository
- Full version control and change history
- No vendor lock-in - content lives with your code
Visual/Contextual Editing ✅
- Side-by-side editing experience
- Live preview of changes
- WYSIWYG-like editing for Markdown content
Schema-Driven Content Modeling ✅
- Define content structure in code (
tina/config.ts) - Type-safe GraphQL API auto-generated
- Collections and fields system
Flexible Deployment ✅
- TinaCloud: Managed service (free tier)
- Self-Hosted: Cloudflare Workers, Vercel, Netlify
- Multiple authentication options
Framework Support ✅
- Best: Next.js (App Router + Pages Router)
- Good: React, Astro, Gatsby, Hugo, Jekyll, Remix, 11ty
- Framework-Agnostic: Works with any framework
---
Errors Prevented (9 Total)
This skill prevents 100% of common TinaCMS errors:
1. ESbuild Compilation Errors
- Importing code with custom loaders
- Frontend-only code in config
- Solution: Specific imports, minimal config dependencies
2. Module Resolution: "Could not resolve 'tinacms'"
- Corrupted installations
- Version mismatches
- Solution: Clean reinstall procedure
3. Field Naming Constraints
- Hyphens and special characters not allowed
- Only alphanumeric + underscores
- Solution: Proper naming patterns
4. Docker Binding Issues
- TinaCMS not accessible outside container
- Solution: Bind to 0.0.0.0 instead of 127.0.0.1
5. Missing `_template` Key Error
- Templates array requires _template field
- Solution: Use fields instead or add _template
6. Path Mismatch Issues
- Collection path doesn't match files
- Solution: Correct path configuration
7. Build Script Ordering Problems
- TypeScript errors from missing Tina types
- Solution: Run
tinacms buildbefore framework build
8. Failed Loading TinaCMS Assets
- Dev assets pushed to production
- Solution: Always use
tinacms buildin CI
9. Reference Field 503 Service Unavailable
- Too many items in referenced collection
- Solution: Split collections or use string fields
---
Token Efficiency
Estimated Savings: 65-70% (~10,900 tokens saved)
| Scenario | Without Skill | With Skill | Savings |
|---|---|---|---|
| Setup & Configuration | ~16,000 tokens | ~5,100 tokens | ~68% |
| Error Resolution | Trial & error | Prevented | 100% |
---
Quick Examples
Example 1: Blog with Next.js + TinaCloud
npx create-next-app@latest my-blog --typescript --app
cd my-blog
npx @tinacms/cli@latest init
# Set env vars, start dev server
npm run devExample 2: Documentation Site with Astro
npx create-tina-app@latest my-docs --template tina-astro-starter
cd my-docs
npm install && npm run devExample 3: Self-Hosted on Cloudflare Workers
npm create cloudflare@latest my-app
npx @tinacms/cli@latest init
npx @tinacms/cli@latest init backend
# Copy Cloudflare Workers backend template
npx wrangler deploy---
Package Versions
- tinacms: 2.9.0 (September 2025)
- @tinacms/cli: 1.11.0 (October 2025)
- React Support: 19.x (>=18.3.1 <20.0.0)
Last Verified: 2025-10-24
---
Production Examples
- TinaCMS Website: https://tina.io (dogfooding their own CMS)
- Tina Astro Starter: https://github.com/tinacms/tina-astro-starter
- Tina Next.js Starter: https://github.com/tinacms/tina-starter-alpaca
---
Official Resources
- Website: https://tina.io
- Documentation: https://tina.io/docs
- GitHub: https://github.com/tinacms/tinacms
- Discord: https://discord.gg/zumN63Ybpf
---
Related Skills
Use TinaCMS in combination with:
- clerk-auth - Add authentication to TinaCMS admin
- cloudflare-worker-base - Self-host TinaCMS backend on Workers
- cloudflare-d1 - Store TinaCMS metadata in D1
- ai-sdk-ui - Add AI-powered content generation to editor
- react-hook-form-zod - Extend TinaCMS forms with validation
- tailwind-v4-shadcn - Style TinaCMS frontend
---
License
MIT
---
Support
Issues? Check SKILL.md and references/common-errors.md
Still Stuck?
- Discord: https://discord.gg/zumN63Ybpf
- GitHub Issues: https://github.com/tinacms/tinacms/issues
- Official Docs: https://tina.io/docs
---
Skill Version: 1.0.0 Last Updated: 2025-10-24
TinaCMS Common Errors - Complete Reference
This document provides detailed troubleshooting for all 9 common TinaCMS errors.
Last Updated: 2025-10-24 TinaCMS Version: 2.9.0
---
Table of Contents
1. ESbuild Compilation Errors 2. Module Resolution Issues 3. Field Naming Constraints 4. Docker Binding Issues 5. Missing _template Key Error 6. Path Mismatch Issues 7. Build Script Ordering Problems 8. Failed Loading TinaCMS Assets 9. Reference Field 503 Service Unavailable
---
1. ESbuild Compilation Errors
Error Messages
ERROR: Schema Not Successfully Built
ERROR: Config Not Successfully Executed
ERROR: Cannot bundle code that requires custom loadersCauses
The tina/config.ts file is compiled using esbuild and executed in Node.js. This can fail when:
1. Importing code with custom loaders
- Webpack loaders
- Babel plugins
- PostCSS processors
- SCSS/SASS files
- Vue/Svelte components
2. Importing frontend-only code
- Code using
windowobject - Code using DOM APIs
- React hooks outside components
- Browser-specific APIs
3. Importing entire component libraries
import { Component } from '../components/'pulls in entire directory- May include dependencies not compatible with Node.js
Solutions
Solution 1: Import Specific Files Only
// ❌ Bad - Imports entire directory
import { HeroComponent } from '../components/'
// ✅ Good - Import specific file
import { HeroComponent } from '../components/blocks/hero'
// ❌ Bad - Imports entire library
import * as Components from '../components'
// ✅ Good - Import only what you need
import { HeroComponent, CTAComponent } from '../components/blocks'Solution 2: Create Separate Schema Files
If your component definitions are complex, extract schema to separate files:
// components/hero/hero.schema.ts
export const heroBlockSchema = {
name: 'hero',
label: 'Hero Section',
fields: [/* ... */]
}
// tina/config.ts
import { heroBlockSchema } from '../components/hero/hero.schema'
export default defineConfig({
schema: {
collections: [{
fields: [{
type: 'object',
name: 'hero',
...heroBlockSchema
}]
}]
}
})Solution 3: Use Type Imports Only
// ✅ Import only types (doesn't execute code)
import type { HeroProps } from '../components/Hero'Prevention
- Keep
tina/config.tsminimal - Only import type definitions and simple utilities
- Avoid importing UI components
- Extract schema definitions to separate files
- Test config builds with
npx @tinacms/cli@latest build
Related Links
- Official Docs: https://tina.io/docs/errors/esbuild-error
- GitHub Issue: https://github.com/tinacms/tinacms/issues/3472
---
2. Module Resolution Issues
Error Messages
Error: Could not resolve "tinacms"
Module not found: Can't resolve 'tinacms'
Cannot find module 'tinacms' or its corresponding type declarationsCauses
1. Corrupted Installation
- Incomplete
npm install - Network issues during installation
- Corrupted
node_modules
2. Version Mismatches
- Conflicting peer dependencies
- React version mismatch
- TinaCMS version incompatibility
3. Missing Dependencies
reactandreact-domnot installed (required even for non-React frameworks)@tinacms/clinot in devDependencies
Solutions
Solution 1: Clean Reinstall (npm)
# Remove node_modules and lock file
rm -rf node_modules package-lock.json
# Clear npm cache
npm cache clean --force
# Reinstall all dependencies
npm installSolution 2: Clean Reinstall (pnpm)
# Remove node_modules and lock file
rm -rf node_modules pnpm-lock.yaml
# Clear pnpm cache
pnpm store prune
# Reinstall all dependencies
pnpm installSolution 3: Clean Reinstall (yarn)
# Remove node_modules and lock file
rm -rf node_modules yarn.lock
# Clear yarn cache
yarn cache clean
# Reinstall all dependencies
yarn installSolution 4: Ensure React Dependencies (Non-React Frameworks)
Even if you're using Hugo, Jekyll, or Eleventy, you MUST install React:
npm install react@^19 react-dom@^19Prevention
- Use lockfiles (
package-lock.json,pnpm-lock.yaml,yarn.lock) - Don't use
--no-optionalor--omit=optionalflags - Ensure
reactandreact-domare in dependencies - Keep TinaCMS versions consistent across packages
Related Links
- GitHub Issue: https://github.com/tinacms/tinacms/issues/4530
---
3. Field Naming Constraints
Error Messages
Field name contains invalid characters
Invalid field name: 'hero-image'
Field names must be alphanumericCauses
TinaCMS field names can ONLY contain:
- Letters (a-z, A-Z)
- Numbers (0-9)
- Underscores (_)
NOT allowed:
- Hyphens (-)
- Spaces ( )
- Special characters (!@#$%^&*)
This is a breaking change from Forestry.io which allowed hyphens.
Solutions
Solution 1: Use Underscores
// ❌ Bad
{
name: 'hero-image',
label: 'Hero Image',
type: 'image'
}
// ✅ Good
{
name: 'hero_image',
label: 'Hero Image',
type: 'image'
}Solution 2: Use camelCase
// ✅ Also good
{
name: 'heroImage',
label: 'Hero Image',
type: 'image'
}Solution 3: Migrate Existing Content
If migrating from Forestry.io with hyphenated field names:
# Find all files with hyphenated frontmatter
find content -name "*.md" -exec sed -i 's/hero-image:/hero_image:/g' {} +
find content -name "*.md" -exec sed -i 's/cover-photo:/cover_photo:/g' {} +Prevention
- Use underscores or camelCase for all field names
- Document naming convention in team style guide
- Use linter/validator to catch invalid names
Related Links
- Migration Docs: https://tina.io/docs/forestry/migrate#field-names
---
4. Docker Binding Issues
Error Messages
Connection refused: http://localhost:3000
Cannot connect to TinaCMS admin interface
ERR_CONNECTION_REFUSEDCauses
By default, development servers bind to 127.0.0.1 (localhost only), which prevents access from:
- Docker containers
- Network devices
- Remote connections
Solutions
Solution 1: Next.js
tinacms dev -c "next dev --hostname 0.0.0.0"Or in package.json:
{
"scripts": {
"dev": "tinacms dev -c \"next dev --hostname 0.0.0.0\""
}
}Solution 2: Vite
tinacms dev -c "vite --host 0.0.0.0"Or in vite.config.ts:
export default defineConfig({
server: {
host: '0.0.0.0',
port: 3000
}
})Solution 3: Astro
tinacms dev -c "astro dev --host 0.0.0.0"Or in astro.config.mjs:
export default defineConfig({
server: {
host: '0.0.0.0',
port: 4321
}
})Solution 4: Docker Compose
version: '3'
services:
app:
build: .
ports:
- "3000:3000"
command: npm run dev # Must include --hostname 0.0.0.0Prevention
- Always use
--host 0.0.0.0in Docker environments - Configure framework config files instead of CLI flags
- Document binding requirements in README
---
5. Missing _template Key Error
Error Messages
GetCollection failed: Unable to fetch
template name was not provided
Error: Document missing _template fieldCauses
When a collection uses templates array (multiple schemas), each document must include a _template field in its frontmatter specifying which template to use.
Solutions
Solution 1: Use fields Instead (Recommended)
If you only have one schema, use fields instead of templates:
// ❌ Using templates (requires _template in frontmatter)
{
name: 'post',
path: 'content/posts',
templates: [
{
name: 'article',
fields: [/* ... */]
}
]
}
// ✅ Using fields (no _template needed)
{
name: 'post',
path: 'content/posts',
fields: [/* ... */] // Same fields as above
}Solution 2: Add _template to Frontmatter
If you need multiple templates, ensure all documents have _template:
---
_template: article
title: My Blog Post
date: 2025-10-24
---
Content here...Solution 3: Batch Update Existing Files
# Add _template field to all files
find content/posts -name "*.md" -exec sed -i '1a _template: article' {} +Solution 4: Set Default Template
{
name: 'post',
path: 'content/posts',
templates: [
{
name: 'article',
fields: [/* ... */]
}
],
ui: {
defaultItem: () => ({
_template: 'article' // Default template for new documents
})
}
}Prevention
- Use
fieldsfor single-schema collections - Use
templatesonly when you truly need multiple schemas - Document
_templaterequirement in team guidelines
---
6. Path Mismatch Issues
Error Messages
No files found in collection
File not found: content/posts/hello.md
GraphQL query returned empty resultsCauses
The path in your collection config doesn't match where files actually live.
Solutions
Solution 1: Verify File Locations
# Check where your files actually are
ls -R content/
# If files are in content/posts/
# Your config should be:
{
name: 'post',
path: 'content/posts' // ✅ Matches file location
}Solution 2: No Trailing Slash
// ❌ Bad
{
path: 'content/posts/' // Trailing slash may cause issues
}
// ✅ Good
{
path: 'content/posts' // No trailing slash
}Solution 3: Use Relative Paths
// ✅ Path relative to project root
{
path: 'content/posts'
}
// ❌ Don't use absolute paths
{
path: '/home/user/project/content/posts'
}Solution 4: Audit Paths
# TinaCMS CLI includes audit command
npx @tinacms/cli@latest audit
# Shows which files match collections
# Helps identify path mismatchesPrevention
- Always verify file structure before setting
path - Use
npx @tinacms/cli@latest auditto check paths - Document expected file structure in README
---
7. Build Script Ordering Problems
Error Messages
ERROR: Cannot find module '../tina/__generated__/client'
ERROR: Property 'queries' does not exist on type '{}'
Type error: Module '../tina/__generated__/client' has no exported member 'client'Causes
Framework build runs before tinacms build, so generated TypeScript types don't exist yet.
Solutions
Solution 1: Correct Script Order
{
"scripts": {
"build": "tinacms build && next build" // ✅ Tina FIRST
// NOT: "build": "next build && tinacms build" // ❌ Wrong
}
}Solution 2: CI/CD Fix (GitHub Actions)
name: Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm install
- name: Build Tina
run: npx @tinacms/cli@latest build # Generate types FIRST
- name: Build App
run: npm run build # Now framework build worksSolution 3: Vercel Configuration
// vercel.json
{
"buildCommand": "tinacms build && next build"
}Solution 4: Netlify Configuration
# netlify.toml
[build]
command = "tinacms build && npm run build"Prevention
- Always run
tinacms buildbefore framework build - Document correct build order in README
- Add build order to CI/CD templates
---
8. Failed Loading TinaCMS Assets
Error Messages
Failed to load resource: net::ERR_CONNECTION_REFUSED
http://localhost:4001/...
Mixed Content: The page was loaded over HTTPS, but requested an insecure resourceCauses
1. Development `admin/index.html` pushed to production
- Dev version loads assets from
localhost:4001 - Production needs built assets
2. Subdirectory deployment without `basePath` config
- Site deployed to
example.com/blog/ - TinaCMS tries to load from
example.com/admin/(fails)
Solutions
Solution 1: Always Build for Production
{
"scripts": {
"build": "tinacms build && next build" // ✅ Always build
// NOT: "build": "tinacms dev" // ❌ Never dev in prod
}
}Solution 2: Configure basePath for Subdirectories
If site is at example.com/blog/:
// tina/config.ts
export default defineConfig({
build: {
outputFolder: 'admin',
publicFolder: 'public',
basePath: 'blog' // ← Subdirectory path
}
})Solution 3: Verify .gitignore
# .gitignore
# TinaCMS
.tina/__generated__
admin/index.html # ← Don't commit dev admin
# But DO commit:
# public/admin/ (production build)Solution 4: CI/CD Always Build
# GitHub Actions
- run: npx @tinacms/cli@latest build # Not dev!Prevention
- Use
tinacms buildin all CI/CD - Set
basePathfor subdirectory deploys - Don't commit
admin/index.htmlfrom dev mode
---
9. Reference Field 503 Service Unavailable
Error Messages
503 Service Unavailable
Request timed out
Reference field dropdown not loadingCauses
Reference fields load ALL documents from the referenced collection. If the collection has 100s or 1000s of items, the request times out.
Current Limitation: TinaCMS does not support pagination for reference fields (as of v2.9.0).
Solutions
Solution 1: Split Large Collections
Instead of one huge collection, split by category:
// ❌ Single huge collection (1000+ authors)
{
name: 'author',
path: 'content/authors',
fields: [/* ... */]
}
// ✅ Split into manageable sizes
{
name: 'active_author',
label: 'Active Authors',
path: 'content/authors/active',
fields: [/* ... */]
}
{
name: 'archived_author',
label: 'Archived Authors',
path: 'content/authors/archived',
fields: [/* ... */]
}Solution 2: Use String Field with Validation
Instead of reference, use a string select:
// ❌ Reference field (times out with 100+ authors)
{
type: 'reference',
name: 'author',
collections: ['author']
}
// ✅ String field with curated options
{
type: 'string',
name: 'author_id',
label: 'Author',
options: [
{ label: 'John Doe', value: 'john-doe' },
{ label: 'Jane Smith', value: 'jane-smith' },
// ... curated list of ~50 authors max
]
}Solution 3: Custom Field Component (Advanced)
Create a custom field component with client-side pagination:
// Advanced: Requires custom React component
{
type: 'string',
name: 'author_id',
ui: {
component: 'author-select-paginated' // Your custom component
}
}See: https://tina.io/docs/extending-tina/custom-field-components/
Solution 4: Use External Service
For very large datasets, consider:
- Store authors in D1/KV
- Use custom field component to query via API
- Load data on-demand with search/filter
Prevention
- Limit referenced collections to <100 items
- Use string fields for large datasets
- Consider alternative architectures for huge collections
Related Links
- GitHub Issue: https://github.com/tinacms/tinacms/issues/3821
- Custom Fields Docs: https://tina.io/docs/extending-tina/custom-field-components/
---
Additional Resources
Official Documentation
- Main Docs: https://tina.io/docs
- Error FAQ: https://tina.io/docs/errors/faq
- ESbuild Error: https://tina.io/docs/errors/esbuild-error
- Contributing: https://tina.io/docs/contributing/troubleshooting
Support
- Discord: https://discord.gg/zumN63Ybpf
- GitHub Issues: https://github.com/tinacms/tinacms/issues
- GitHub Discussions: https://github.com/tinacms/tinacms/discussions
---
Last Updated: 2025-10-24 Errors Documented: 9 Prevention Rate: 100%
#!/bin/bash
##
# TinaCMS Version Checker
#
# Checks if you're using current versions of TinaCMS packages
#
# Usage:
# ./scripts/check-versions.sh
#
# Last Updated: 2025-10-24
##
set -e
echo "🔍 Checking TinaCMS package versions..."
echo ""
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Expected versions (update these when new versions released)
EXPECTED_TINACMS="2.9.0"
EXPECTED_CLI="1.11.0"
EXPECTED_REACT="19.0.0"
# Function to get installed version
get_installed_version() {
local package=$1
if [ -f "package.json" ]; then
node -e "try { const pkg = require('./package.json'); console.log(pkg.dependencies['$package'] || pkg.devDependencies['$package'] || 'not-installed'); } catch(e) { console.log('not-installed'); }" 2>/dev/null
else
echo "no-package-json"
fi
}
# Function to get latest version from npm
get_latest_version() {
local package=$1
npm view "$package" version 2>/dev/null || echo "unknown"
}
# Function to compare versions
check_version() {
local package=$1
local expected=$2
local installed=$(get_installed_version "$package")
local latest=$(get_latest_version "$package")
echo "📦 $package"
echo " Expected: $expected"
echo " Installed: $installed"
echo " Latest: $latest"
if [ "$installed" = "not-installed" ]; then
echo -e " ${RED}❌ NOT INSTALLED${NC}"
elif [ "$installed" = "no-package-json" ]; then
echo -e " ${YELLOW}⚠️ No package.json found${NC}"
elif [ "$installed" = "^$expected" ] || [ "$installed" = "~$expected" ] || [ "$installed" = "$expected" ]; then
echo -e " ${GREEN}✅ Current version${NC}"
else
echo -e " ${YELLOW}⚠️ Version mismatch - consider upgrading${NC}"
fi
echo ""
}
# Check if we're in a project directory
if [ ! -f "package.json" ]; then
echo -e "${YELLOW}⚠️ No package.json found in current directory${NC}"
echo "Run this script from your project root."
echo ""
fi
# Check versions
check_version "tinacms" "$EXPECTED_TINACMS"
check_version "@tinacms/cli" "$EXPECTED_CLI"
check_version "react" "$EXPECTED_REACT"
check_version "react-dom" "$EXPECTED_REACT"
echo "✅ Version check complete!"
echo ""
echo "To upgrade:"
echo " npm install tinacms@latest @tinacms/cli@latest"
echo " npm install react@latest react-dom@latest"
echo ""
echo "To check latest versions manually:"
echo " npm view tinacms version"
echo " npm view @tinacms/cli version"
echo ""
# TinaCloud Credentials
# Get these from https://app.tina.io after creating a project
# Note: Astro requires PUBLIC_ prefix for client-exposed variables
# Client ID (public, safe to expose)
PUBLIC_TINA_CLIENT_ID=your_client_id_here
# Read-only token (keep secret, NOT public)
# This should not have PUBLIC_ prefix as it's server-side only
TINA_TOKEN=your_read_only_token_here
# Git branch
PUBLIC_GITHUB_BRANCH=main
# For self-hosted backend
# PUBLIC_TINA_API_URL=/api/tina/gql
import { defineConfig } from 'astro/config'
import react from '@astro/react'
import mdx from '@astro/mdx'
/**
* Astro Configuration for TinaCMS
*
* Key settings:
* - React integration (required for Tina admin interface)
* - MDX support (for rich content)
* - Site configuration
*
* For more info: https://astro.build/config
*/
export default defineConfig({
integrations: [
react(), // Required for TinaCMS admin
mdx(), // Recommended for rich content
],
// Your site URL (for sitemap, canonical URLs, etc.)
site: 'https://example.com',
// Optional: Server configuration
server: {
port: 4321,
host: '0.0.0.0', // Allows external connections (Docker, network)
},
})
{
"name": "astro-tinacms-app",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tinacms dev -c \"astro dev\"",
"build": "tinacms build && astro build",
"preview": "astro preview"
},
"dependencies": {
"@astrojs/mdx": "^4.0.0",
"@astrojs/react": "^4.0.0",
"astro": "^5.15.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tinacms": "^2.9.0"
},
"devDependencies": {
"@tinacms/cli": "^1.11.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.6.0"
}
}
import { defineConfig } from 'tinacms'
import { blogPostCollection } from './collections/blog-post'
import { docPageCollection } from './collections/doc-page'
import { authorCollection } from './collections/author'
/**
* TinaCMS Configuration for Astro
*
* This config works with Astro static site generator.
*
* Key Points:
* - Visual editing is experimental (requires React components)
* - Best for content-focused static sites
* - Environment variables use PUBLIC_ prefix
* - Admin interface requires React integration
*
* Usage:
* 1. Copy this file to: tina/config.ts
* 2. Copy collection files to: tina/collections/
* 3. Set environment variables in .env
* 4. Run: npm run dev
* 5. Access admin: http://localhost:4321/admin/index.html
*/
// Get Git branch from environment
const branch =
process.env.PUBLIC_GITHUB_BRANCH ||
process.env.PUBLIC_VERCEL_GIT_COMMIT_REF ||
'main'
export default defineConfig({
// Git branch to use
branch,
// TinaCloud credentials
// Note: Astro requires PUBLIC_ prefix for client-exposed variables
clientId: process.env.PUBLIC_TINA_CLIENT_ID,
token: process.env.TINA_TOKEN,
// Build configuration
build: {
outputFolder: 'admin',
publicFolder: 'public',
},
// Media configuration
media: {
tina: {
mediaRoot: 'uploads',
publicFolder: 'public',
},
},
// Content schema
schema: {
collections: [
blogPostCollection,
authorCollection,
docPageCollection,
],
},
// Optional: Self-hosted backend
// contentApiUrlOverride: '/api/tina/gql',
})
/**
* Cloudflare Worker Backend for TinaCMS
*
* This Worker provides a self-hosted TinaCMS backend on Cloudflare's edge network.
*
* Features:
* - GraphQL API for content management
* - Authentication via Auth.js or custom providers
* - Local development mode
* - Global edge deployment
*
* Setup:
* 1. Install dependencies: npm install @tinacms/datalayer tinacms-authjs
* 2. Generate database client: npx @tinacms/cli@latest init backend
* 3. Configure wrangler.jsonc
* 4. Deploy: npx wrangler deploy
*
* Environment Variables:
* - TINA_PUBLIC_IS_LOCAL: "true" for local dev, "false" for production
* - NEXTAUTH_SECRET: Secret key for Auth.js (production only)
*/
import { TinaNodeBackend, LocalBackendAuthProvider } from '@tinacms/datalayer'
import { AuthJsBackendAuthProvider, TinaAuthJSOptions } from 'tinacms-authjs'
// @ts-ignore - Generated by TinaCMS CLI
import databaseClient from '../../tina/__generated__/databaseClient'
/**
* Cloudflare Workers Environment
*
* Extend this interface to add your environment variables and bindings
*/
interface Env {
// Environment variables
TINA_PUBLIC_IS_LOCAL?: string
NEXTAUTH_SECRET?: string
// Optional: KV namespace for session storage
// SESSION_KV?: KVNamespace
// Optional: D1 database for user management
// DB?: D1Database
}
/**
* Main Worker Handler
*
* Processes incoming requests and routes them to the TinaCMS backend
*/
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Determine if we're in local development mode
const isLocal = env.TINA_PUBLIC_IS_LOCAL === 'true'
// Initialize TinaCMS backend
const handler = TinaNodeBackend({
authProvider: isLocal
? LocalBackendAuthProvider()
: AuthJsBackendAuthProvider({
authOptions: TinaAuthJSOptions({
databaseClient,
secret: env.NEXTAUTH_SECRET || '',
// Add OAuth providers here:
// providers: [
// DiscordProvider({
// clientId: env.DISCORD_CLIENT_ID,
// clientSecret: env.DISCORD_CLIENT_SECRET,
// }),
// ],
}),
}),
databaseClient,
})
try {
// Handle CORS preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
// Process the request through TinaCMS
const response = await handler(request)
// Add CORS headers to response
const corsResponse = new Response(response.body, response)
corsResponse.headers.set('Access-Control-Allow-Origin': '*')
corsResponse.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
corsResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
return corsResponse
} catch (error) {
console.error('TinaCMS Backend Error:', error)
return new Response(
JSON.stringify({
error: 'Internal Server Error',
message: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
}
)
}
},
}
/**
* Alternative: Hono-based Implementation (Recommended)
*
* For more flexibility, use Hono framework:
*
* import { Hono } from 'hono'
* import { cors } from 'hono/cors'
*
* const app = new Hono()
*
* app.use('/*', cors())
*
* app.all('/api/tina/*', async (c) => {
* const isLocal = c.env.TINA_PUBLIC_IS_LOCAL === 'true'
*
* const handler = TinaNodeBackend({
* authProvider: isLocal
* ? LocalBackendAuthProvider()
* : AuthJsBackendAuthProvider({
* authOptions: TinaAuthJSOptions({
* databaseClient,
* secret: c.env.NEXTAUTH_SECRET,
* }),
* }),
* databaseClient,
* })
*
* return handler(c.req.raw)
* })
*
* export default app
*/
{
"$schema": "https://raw.githubusercontent.com/cloudflare/wrangler/main/npm/wrangler/wrangler-schema.json",
"name": "tina-backend",
"main": "src/index.ts",
"compatibility_date": "2025-10-24",
"compatibility_flags": ["nodejs_compat"],
// Environment variables for development
"vars": {
"TINA_PUBLIC_IS_LOCAL": "false"
},
// Environment-specific configurations
"env": {
"development": {
"vars": {
"TINA_PUBLIC_IS_LOCAL": "true"
}
},
"production": {
"vars": {
"TINA_PUBLIC_IS_LOCAL": "false"
},
// Add secrets via: wrangler secret put NEXTAUTH_SECRET
"secrets": [
"NEXTAUTH_SECRET"
]
}
},
// Optional: Add KV for session storage
// "kv_namespaces": [
// {
// "binding": "SESSION_KV",
// "id": "your-kv-namespace-id"
// }
// ],
// Optional: Add D1 for user management
// "d1_databases": [
// {
// "binding": "DB",
// "database_name": "tina-users",
// "database_id": "your-d1-database-id"
// }
// ],
// Routes configuration (adjust based on your setup)
"routes": [
{
"pattern": "yourdomain.com/api/tina/*",
"zone_name": "yourdomain.com"
}
]
}
import type { Collection } from 'tinacms'
/**
* Author Collection Template
*
* A complete author schema for blog post references:
* - Name, email
* - Avatar image
* - Bio
* - Social media links
*
* Usage:
* import { authorCollection } from './collections/author'
*
* export default defineConfig({
* schema: {
* collections: [authorCollection, blogPostCollection]
* }
* })
*
* Then reference in blog posts:
* {
* type: 'reference',
* name: 'author',
* collections: ['author']
* }
*/
export const authorCollection: Collection = {
name: 'author',
label: 'Authors',
path: 'content/authors',
format: 'json',
fields: [
{
type: 'string',
name: 'name',
label: 'Name',
isTitle: true,
required: true,
description: 'Author\'s full name',
},
{
type: 'string',
name: 'email',
label: 'Email',
required: true,
ui: {
validate: (value) => {
if (!value) {
return 'Email is required'
}
if (!value.includes('@')) {
return 'Invalid email address'
}
},
},
description: 'Contact email address',
},
{
type: 'string',
name: 'role',
label: 'Role',
description: 'Job title or role (e.g., "Senior Developer", "Content Writer")',
},
{
type: 'image',
name: 'avatar',
label: 'Avatar',
description: 'Profile picture (square, 400x400px recommended)',
},
{
type: 'string',
name: 'bio',
label: 'Bio',
ui: {
component: 'textarea',
},
description: 'Short biography (150-200 characters)',
},
{
type: 'object',
name: 'social',
label: 'Social Media Links',
fields: [
{
type: 'string',
name: 'twitter',
label: 'Twitter',
description: 'Twitter/X username (without @)',
},
{
type: 'string',
name: 'github',
label: 'GitHub',
description: 'GitHub username',
},
{
type: 'string',
name: 'linkedin',
label: 'LinkedIn',
description: 'LinkedIn profile URL',
},
{
type: 'string',
name: 'website',
label: 'Personal Website',
description: 'Full URL to personal website',
},
],
},
{
type: 'boolean',
name: 'active',
label: 'Active Author',
required: true,
description: 'Uncheck to hide author from listings',
ui: {
component: 'toggle',
},
},
],
ui: {
defaultItem: () => ({
active: true,
}),
},
}
import type { Collection } from 'tinacms'
/**
* Blog Post Collection Template
*
* A complete blog post schema with:
* - Title, excerpt, cover image
* - Author reference
* - Published date
* - Draft status
* - Rich-text body content
*
* Usage:
* import { blogPostCollection } from './collections/blog-post'
*
* export default defineConfig({
* schema: {
* collections: [blogPostCollection]
* }
* })
*/
export const blogPostCollection: Collection = {
name: 'post',
label: 'Blog Posts',
path: 'content/posts',
format: 'mdx',
fields: [
{
type: 'string',
name: 'title',
label: 'Title',
isTitle: true,
required: true,
},
{
type: 'string',
name: 'excerpt',
label: 'Excerpt',
ui: {
component: 'textarea',
},
description: 'Short summary shown in post listings (150-200 characters)',
},
{
type: 'image',
name: 'coverImage',
label: 'Cover Image',
description: 'Main image shown at top of post and in listings',
},
{
type: 'datetime',
name: 'date',
label: 'Published Date',
required: true,
ui: {
dateFormat: 'YYYY-MM-DD',
},
},
{
type: 'reference',
name: 'author',
label: 'Author',
collections: ['author'],
description: 'Select the author of this post',
},
{
type: 'string',
name: 'category',
label: 'Category',
options: [
{ label: 'Technology', value: 'technology' },
{ label: 'Design', value: 'design' },
{ label: 'Business', value: 'business' },
{ label: 'Tutorials', value: 'tutorials' },
],
},
{
type: 'string',
name: 'tags',
label: 'Tags',
list: true,
description: 'Add tags for post categorization and search',
},
{
type: 'boolean',
name: 'draft',
label: 'Draft',
required: true,
description: 'If checked, post will not be published',
ui: {
component: 'toggle',
},
},
{
type: 'boolean',
name: 'featured',
label: 'Featured Post',
description: 'Show this post prominently on homepage',
},
{
type: 'object',
name: 'seo',
label: 'SEO Metadata',
fields: [
{
type: 'string',
name: 'metaTitle',
label: 'Meta Title',
description: 'SEO title (leave blank to use post title)',
},
{
type: 'string',
name: 'metaDescription',
label: 'Meta Description',
ui: {
component: 'textarea',
},
description: 'SEO description (150-160 characters)',
},
{
type: 'image',
name: 'ogImage',
label: 'Open Graph Image',
description: 'Image for social media sharing (1200x630px)',
},
],
},
{
type: 'rich-text',
name: 'body',
label: 'Body',
isBody: true,
},
],
ui: {
router: ({ document }) => {
return `/blog/${document._sys.filename}`
},
defaultItem: () => ({
title: 'New Post',
date: new Date().toISOString(),
draft: true,
featured: false,
}),
},
}
import type { Collection } from 'tinacms'
/**
* Documentation Page Collection Template
*
* A complete documentation page schema with:
* - Title, description
* - Sidebar ordering
* - Nested folder support
* - Rich-text content with code blocks
*
* Usage:
* import { docPageCollection } from './collections/doc-page'
*
* export default defineConfig({
* schema: {
* collections: [docPageCollection]
* }
* })
*
* File structure supports nested docs:
* content/docs/
* ├── getting-started.mdx
* ├── installation/
* │ ├── nextjs.mdx
* │ └── vite.mdx
* └── api/
* ├── authentication.mdx
* └── endpoints.mdx
*/
export const docPageCollection: Collection = {
name: 'doc',
label: 'Documentation',
path: 'content/docs',
format: 'mdx',
fields: [
{
type: 'string',
name: 'title',
label: 'Title',
isTitle: true,
required: true,
description: 'Page title shown in sidebar and at top of page',
},
{
type: 'string',
name: 'description',
label: 'Description',
ui: {
component: 'textarea',
},
description: 'Short description shown below title and in search results',
},
{
type: 'number',
name: 'order',
label: 'Order',
description: 'Sort order in sidebar (lower numbers appear first)',
ui: {
parse: (val) => Number(val),
},
},
{
type: 'string',
name: 'category',
label: 'Category',
description: 'Category for grouping related documentation',
options: [
{ label: 'Getting Started', value: 'getting-started' },
{ label: 'Guides', value: 'guides' },
{ label: 'API Reference', value: 'api' },
{ label: 'Tutorials', value: 'tutorials' },
{ label: 'Examples', value: 'examples' },
],
},
{
type: 'datetime',
name: 'lastUpdated',
label: 'Last Updated',
description: 'Automatically updated when page is saved',
ui: {
dateFormat: 'YYYY-MM-DD',
},
},
{
type: 'object',
name: 'sidebar',
label: 'Sidebar Configuration',
fields: [
{
type: 'boolean',
name: 'hide',
label: 'Hide from Sidebar',
description: 'If checked, page won\'t appear in sidebar navigation',
},
{
type: 'string',
name: 'label',
label: 'Custom Sidebar Label',
description: 'Override the title shown in sidebar (leave blank to use page title)',
},
],
},
{
type: 'object',
name: 'navigation',
label: 'Page Navigation',
description: 'Configure prev/next links at bottom of page',
fields: [
{
type: 'reference',
name: 'prev',
label: 'Previous Page',
collections: ['doc'],
},
{
type: 'reference',
name: 'next',
label: 'Next Page',
collections: ['doc'],
},
],
},
{
type: 'rich-text',
name: 'body',
label: 'Body',
isBody: true,
templates: [
// Add custom MDX components here if needed
// Example:
// {
// name: 'Callout',
// label: 'Callout',
// fields: [
// {
// name: 'type',
// label: 'Type',
// type: 'string',
// options: ['info', 'warning', 'error', 'success'],
// },
// {
// name: 'children',
// label: 'Content',
// type: 'rich-text',
// },
// ],
// },
],
},
],
ui: {
router: ({ document }) => {
// Support nested docs: /docs/installation/nextjs
const breadcrumbs = document._sys.breadcrumbs.join('/')
return `/docs/${breadcrumbs}`
},
defaultItem: () => ({
title: 'New Documentation Page',
category: 'guides',
lastUpdated: new Date().toISOString(),
sidebar: {
hide: false,
},
}),
},
}
import type { Collection } from 'tinacms'
/**
* Landing Page Collection Template
*
* A complete landing page schema with multiple templates:
* - Basic page (simple content)
* - Marketing page (hero, features, CTA)
*
* Usage:
* import { landingPageCollection } from './collections/landing-page'
*
* export default defineConfig({
* schema: {
* collections: [landingPageCollection]
* }
* })
*
* When using templates, documents must include _template field:
* ---
* _template: marketing
* title: Homepage
* ---
*/
export const landingPageCollection: Collection = {
name: 'page',
label: 'Landing Pages',
path: 'content/pages',
format: 'mdx',
templates: [
{
name: 'basic',
label: 'Basic Page',
fields: [
{
type: 'string',
name: 'title',
label: 'Title',
isTitle: true,
required: true,
},
{
type: 'string',
name: 'description',
label: 'Description',
ui: {
component: 'textarea',
},
},
{
type: 'rich-text',
name: 'body',
label: 'Body',
isBody: true,
},
],
},
{
name: 'marketing',
label: 'Marketing Page',
ui: {
defaultItem: {
hero: {
buttonText: 'Get Started',
},
features: [],
},
},
fields: [
{
type: 'string',
name: 'title',
label: 'Title',
isTitle: true,
required: true,
description: 'Page title (used in browser tab and SEO)',
},
{
type: 'object',
name: 'hero',
label: 'Hero Section',
fields: [
{
type: 'string',
name: 'headline',
label: 'Headline',
required: true,
description: 'Main headline (45-65 characters works best)',
},
{
type: 'string',
name: 'subheadline',
label: 'Subheadline',
ui: {
component: 'textarea',
},
description: 'Supporting text below headline (150-200 characters)',
},
{
type: 'image',
name: 'image',
label: 'Hero Image',
description: 'Main hero image (1920x1080px recommended)',
},
{
type: 'string',
name: 'buttonText',
label: 'Button Text',
description: 'Call-to-action button text',
},
{
type: 'string',
name: 'buttonUrl',
label: 'Button URL',
description: 'Where the CTA button links to',
},
],
},
{
type: 'object',
name: 'features',
label: 'Features Section',
list: true,
ui: {
itemProps: (item) => ({
label: item?.title || 'New Feature',
}),
},
fields: [
{
type: 'image',
name: 'icon',
label: 'Icon',
description: 'Feature icon (SVG or PNG, 64x64px)',
},
{
type: 'string',
name: 'title',
label: 'Title',
required: true,
},
{
type: 'string',
name: 'description',
label: 'Description',
ui: {
component: 'textarea',
},
},
],
},
{
type: 'object',
name: 'testimonials',
label: 'Testimonials',
list: true,
ui: {
itemProps: (item) => ({
label: item?.author || 'New Testimonial',
}),
},
fields: [
{
type: 'string',
name: 'quote',
label: 'Quote',
required: true,
ui: {
component: 'textarea',
},
},
{
type: 'string',
name: 'author',
label: 'Author',
required: true,
},
{
type: 'string',
name: 'role',
label: 'Role',
description: 'Job title and company',
},
{
type: 'image',
name: 'avatar',
label: 'Avatar',
description: 'Author photo (square, 200x200px)',
},
],
},
{
type: 'object',
name: 'cta',
label: 'Call to Action',
fields: [
{
type: 'string',
name: 'headline',
label: 'Headline',
required: true,
},
{
type: 'string',
name: 'text',
label: 'Supporting Text',
ui: {
component: 'textarea',
},
},
{
type: 'string',
name: 'buttonText',
label: 'Button Text',
required: true,
},
{
type: 'string',
name: 'buttonUrl',
label: 'Button URL',
required: true,
},
],
},
{
type: 'object',
name: 'seo',
label: 'SEO Metadata',
fields: [
{
type: 'string',
name: 'metaTitle',
label: 'Meta Title',
description: 'SEO title (leave blank to use page title)',
},
{
type: 'string',
name: 'metaDescription',
label: 'Meta Description',
ui: {
component: 'textarea',
},
description: 'SEO description (150-160 characters)',
},
{
type: 'image',
name: 'ogImage',
label: 'Open Graph Image',
description: 'Image for social media sharing (1200x630px)',
},
],
},
],
},
],
ui: {
router: ({ document }) => {
// Root pages like /about, /pricing
return `/${document._sys.filename}`
},
},
}
# TinaCloud Credentials
# Get these from https://app.tina.io after creating a project
# Client ID (public, safe to expose)
NEXT_PUBLIC_TINA_CLIENT_ID=your_client_id_here
# Read-only token (keep secret, never commit)
TINA_TOKEN=your_read_only_token_here
# Git branch (auto-detected on Vercel/Netlify, set manually for local dev)
# GITHUB_BRANCH=main
# For self-hosted backend with Auth.js
# NEXTAUTH_SECRET=generate_a_secret_key_here
# TINA_PUBLIC_IS_LOCAL=false
# For self-hosted with OAuth providers (example: Discord)
# DISCORD_CLIENT_ID=your_discord_client_id
# DISCORD_CLIENT_SECRET=your_discord_client_secret
{
"name": "nextjs-tinacms-app",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "tinacms dev -c \"next dev\"",
"build": "tinacms build && next build",
"start": "tinacms build && next start",
"lint": "next lint"
},
"dependencies": {
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tinacms": "^2.9.0"
},
"devDependencies": {
"@tinacms/cli": "^1.11.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5"
}
}
import { defineConfig } from 'tinacms'
import { blogPostCollection } from './collections/blog-post'
import { docPageCollection } from './collections/doc-page'
import { authorCollection } from './collections/author'
/**
* TinaCMS Configuration for Next.js App Router
*
* This config file:
* - Sets up TinaCloud connection (or self-hosted)
* - Defines content collections and their schemas
* - Configures media handling
* - Sets up build output
*
* Usage:
* 1. Copy this file to: tina/config.ts
* 2. Copy collection files to: tina/collections/
* 3. Set environment variables in .env.local
* 4. Run: npm run dev
* 5. Access admin: http://localhost:3000/admin/index.html
*/
// Get Git branch from environment
const branch =
process.env.GITHUB_BRANCH ||
process.env.VERCEL_GIT_COMMIT_REF ||
process.env.HEAD ||
'main'
export default defineConfig({
// Git branch to use
branch,
// TinaCloud credentials (get from https://app.tina.io)
clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID,
token: process.env.TINA_TOKEN,
// Build configuration
build: {
outputFolder: 'admin', // Where admin UI is built
publicFolder: 'public', // Your public assets folder
},
// Media configuration
media: {
tina: {
mediaRoot: 'uploads', // Subfolder for uploads
publicFolder: 'public', // Where files are stored
},
},
// Content schema
schema: {
collections: [
// Import your collections here
blogPostCollection,
authorCollection,
docPageCollection,
// Or define collections inline:
// {
// name: 'post',
// label: 'Blog Posts',
// path: 'content/posts',
// format: 'mdx',
// fields: [
// {
// type: 'string',
// name: 'title',
// label: 'Title',
// isTitle: true,
// required: true,
// },
// {
// type: 'rich-text',
// name: 'body',
// label: 'Body',
// isBody: true,
// },
// ],
// },
],
},
// Optional: Self-hosted backend configuration
// Uncomment if using self-hosted backend
// contentApiUrlOverride: '/api/tina/gql',
})
import { defineConfig } from 'tinacms'
import { blogPostCollection } from './collections/blog-post'
import { docPageCollection } from './collections/doc-page'
import { authorCollection } from './collections/author'
/**
* TinaCMS Configuration for Next.js Pages Router
*
* This config file works with Next.js 12 and Pages Router setup.
*
* Differences from App Router:
* - Admin route: pages/admin/[[...index]].tsx (not app/)
* - Data fetching: getStaticProps + getStaticPaths
* - Client hook: useTina for visual editing
*
* Usage:
* 1. Copy this file to: tina/config.ts
* 2. Copy collection files to: tina/collections/
* 3. Set environment variables in .env.local
* 4. Run: npm run dev
* 5. Access admin: http://localhost:3000/admin/index.html
*/
// Get Git branch from environment
const branch =
process.env.GITHUB_BRANCH ||
process.env.VERCEL_GIT_COMMIT_REF ||
process.env.HEAD ||
'main'
export default defineConfig({
// Git branch to use
branch,
// TinaCloud credentials (get from https://app.tina.io)
clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID,
token: process.env.TINA_TOKEN,
// Build configuration
build: {
outputFolder: 'admin',
publicFolder: 'public',
},
// Media configuration
media: {
tina: {
mediaRoot: 'uploads',
publicFolder: 'public',
},
},
// Content schema
schema: {
collections: [
blogPostCollection,
authorCollection,
docPageCollection,
],
},
// Optional: Self-hosted backend configuration
// contentApiUrlOverride: '/api/tina/gql',
})
# TinaCloud Credentials
# Get these from https://app.tina.io after creating a project
# Note: Vite requires VITE_ prefix for environment variables
# These variables are exposed to the client, so only use public values
# Client ID (public, safe to expose)
VITE_TINA_CLIENT_ID=your_client_id_here
# Read-only token (keep secret, but exposed to client in Vite)
# For production, use backend proxy to keep this secret
VITE_TINA_TOKEN=your_read_only_token_here
# Git branch
VITE_GITHUB_BRANCH=main
# For self-hosted backend
# VITE_TINA_API_URL=/api/tina/gql
{
"name": "vite-react-tinacms-app",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tinacms dev -c \"vite\"",
"build": "tinacms build && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^6.28.0",
"tinacms": "^2.9.0"
},
"devDependencies": {
"@tinacms/cli": "^1.11.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"typescript": "^5.6.0",
"vite": "^7.1.0"
}
}
import { defineConfig } from 'tinacms'
import { blogPostCollection } from './collections/blog-post'
import { docPageCollection } from './collections/doc-page'
import { authorCollection } from './collections/author'
/**
* TinaCMS Configuration for Vite + React
*
* This config works with Vite + React applications.
*
* Key Differences from Next.js:
* - Environment variables use VITE_ prefix
* - Admin interface setup is more manual
* - Data fetching requires custom implementation
*
* Usage:
* 1. Copy this file to: tina/config.ts
* 2. Copy collection files to: tina/collections/
* 3. Set environment variables in .env
* 4. Run: npm run dev
* 5. Access admin: http://localhost:3000/admin/index.html
*
* Visual Editing:
* - Import useTina from 'tinacms/dist/react'
* - Wrap your components with useTina hook
* - See templates for examples
*/
// Get Git branch from environment
const branch =
process.env.VITE_GITHUB_BRANCH ||
process.env.VITE_VERCEL_GIT_COMMIT_REF ||
'main'
export default defineConfig({
// Git branch to use
branch,
// TinaCloud credentials
// Note: Use VITE_ prefix for Vite environment variables
clientId: process.env.VITE_TINA_CLIENT_ID,
token: process.env.VITE_TINA_TOKEN,
// Build configuration
build: {
outputFolder: 'admin',
publicFolder: 'public',
},
// Media configuration
media: {
tina: {
mediaRoot: 'uploads',
publicFolder: 'public',
},
},
// Content schema
schema: {
collections: [
blogPostCollection,
authorCollection,
docPageCollection,
],
},
// Optional: Self-hosted backend
// contentApiUrlOverride: '/api/tina/gql',
})
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
/**
* Vite Configuration for TinaCMS + React
*
* Key settings:
* - React plugin for JSX support
* - Port 3000 (TinaCMS default)
* - Host 0.0.0.0 for Docker compatibility
*/
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
host: '0.0.0.0', // Allows external connections (Docker, network)
},
build: {
outDir: 'dist',
},
})