tinacms
About
This skill provides a complete TinaCMS setup for building Git-backed content management systems with visual editing capabilities. It's ideal for blogs, documentation, and marketing sites that need content management for non-technical users. The skill supports multiple frameworks including Next.js, Vite+React, and Astro while preventing common configuration and deployment errors.
Quick Install
Claude Code
Recommended/plugin add https://github.com/jezweb/claude-skillsgit clone https://github.com/jezweb/claude-skills.git ~/.claude/skills/tinacmsCopy and paste this command in Claude Code to install this skill
Documentation
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
-
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
-
Visual/Contextual Editing
- Side-by-side editing experience
- Live preview of changes as you type
- WYSIWYG-like editing for Markdown content
-
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
- Define content structure in code (
-
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)
-
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:
-
Building Content-Heavy Sites
- Blogs and personal websites
- Documentation sites
- Marketing websites
- Portfolio sites
-
Non-Technical Editors Need Access
- Content teams without coding knowledge
- Marketing teams managing pages
- Authors writing blog posts
-
Git-Based Workflow Desired
- Want content versioning through Git
- Need content review through pull requests
- Prefer content in repository with code
-
Visual Editing Required
- Editors want to see changes live
- WYSIWYG experience preferred
- Side-by-side editing workflow
❌ Don't Use TinaCMS When:
-
Real-Time Collaboration Needed
- Multiple users editing simultaneously (Google Docs-style)
- Use Sanity, Contentful, or Firebase instead
-
Highly Dynamic Data
- E-commerce product catalogs with frequent inventory changes
- Real-time dashboards
- Use traditional databases (D1, PostgreSQL) instead
-
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:
-
Initialize TinaCMS:
npx @tinacms/cli@latest init- When prompted for public assets directory, enter
public
- When prompted for public assets directory, enter
-
Update package.json scripts:
{ "scripts": { "dev": "tinacms dev -c \"next dev\"", "build": "tinacms build && next build", "start": "tinacms build && next start" } } -
Set environment variables:
# .env.local NEXT_PUBLIC_TINA_CLIENT_ID=your_client_id TINA_TOKEN=your_read_only_token -
Start development server:
npm run dev -
Access admin interface:
http://localhost:3000/admin/index.html
Key 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:
-
Install dependencies:
npm install react@^19 react-dom@^19 tinacms -
Initialize TinaCMS:
npx @tinacms/cli@latest init- Set public assets directory to
public
- Set public assets directory to
-
Update vite.config.ts:
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { port: 3000 // TinaCMS default } }) -
Update package.json scripts:
{ "scripts": { "dev": "tinacms dev -c \"vite\"", "build": "tinacms build && vite build", "preview": "vite preview" } } -
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> -
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> ) } -
Set environment variables:
# .env VITE_TINA_CLIENT_ID=your_client_id VITE_TINA_TOKEN=your_read_only_token
Template: See templates/vite-react/
3. Astro Setup
Steps:
-
Use official starter (recommended):
npx create-tina-app@latest --template tina-astro-starterOr initialize manually:
npx @tinacms/cli@latest init -
Update package.json scripts:
{ "scripts": { "dev": "tinacms dev -c \"astro dev\"", "build": "tinacms build && astro build", "preview": "astro preview" } } -
Configure Astro:
// astro.config.mjs import { defineConfig } from 'astro/config' import react from '@astro/react' export default defineConfig({ integrations: [react()] // Required for Tina admin }) -
Visual editing (experimental):
- Requires React components
- Use
client:tinaDirectivefor interactive editing - Full visual editing is experimental as of October 2025
-
Set environment variables:
# .env PUBLIC_TINA_CLIENT_ID=your_client_id TINA_TOKEN=your_read_only_token
Best 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:
-
Initialize TinaCMS:
npx @tinacms/cli@latest init -
Manually configure build scripts:
{ "scripts": { "dev": "tinacms dev -c \"<your-dev-command>\"", "build": "tinacms build && <your-build-command>" } } -
Admin interface:
- Automatically created at
http://localhost:<port>/admin/index.html - Port depends on your framework
- Automatically created at
-
Data fetching:
- No visual editing (sidebar only)
- Content edited through Git-backed interface
- Changes saved directly to files
-
Set environment variables:
TINA_CLIENT_ID=your_client_id TINA_TOKEN=your_read_only_token
Limitations:
- 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 Executed
Causes:
- Importing code with custom loaders (webpack, babel plugins, esbuild loaders)
- Importing frontend-only code (uses
window, DOM APIs, React hooks) - Importing entire component libraries instead of specific modules
Solution:
Import only what you need:
// ❌ Bad - Imports entire component directory
import { HeroComponent } from '../components/'
// ✅ Good - Import specific file
import { HeroComponent } from '../components/blocks/hero'
Prevention Tips:
- Keep
tina/config.tsimports minimal - Only import type definitions and simple utilities
- Avoid importing UI components directly
- Create separate
.schema.tsfiles if needed
Reference: See references/common-errors.md#esbuild
2. ❌ Module Resolution: "Could not resolve 'tinacms'"
Error Message:
Error: Could not resolve "tinacms"
Causes:
- Corrupted or incomplete installation
- Version mismatch between dependencies
- Missing peer dependencies
Solution:
# Clear cache and reinstall
rm -rf node_modules package-lock.json
npm install
# Or with pnpm
rm -rf node_modules pnpm-lock.yaml
pnpm install
# Or with yarn
rm -rf node_modules yarn.lock
yarn install
Prevention:
- Use lockfiles (
package-lock.json,pnpm-lock.yaml,yarn.lock) - Don't use
--no-optionalor--omit=optionalflags - Ensure
reactandreact-domare installed (even for non-React frameworks)
3. ❌ Field Naming Constraints
Error Message:
Field name contains invalid characters
Cause:
- TinaCMS field names can only contain: letters, numbers, underscores
- Hyphens, spaces, special characters are NOT allowed
Solution:
// ❌ Bad - Uses hyphens
{
name: 'hero-image',
label: 'Hero Image',
type: 'image'
}
// ❌ Bad - Uses spaces
{
name: 'hero image',
label: 'Hero Image',
type: 'image'
}
// ✅ Good - Uses underscores
{
name: 'hero_image',
label: 'Hero Image',
type: 'image'
}
// ✅ Good - CamelCase also works
{
name: 'heroImage',
label: 'Hero Image',
type: 'image'
}
Note: This is a breaking change from Forestry.io migration
4. ❌ Docker Binding Issues
Error:
- TinaCMS admin not accessible from outside Docker container
Cause:
- TinaCMS binds to
127.0.0.1(localhost only) by default - Docker containers need
0.0.0.0binding to accept external connections
Solution:
# Ensure framework dev server listens on all interfaces
tinacms dev -c "next dev --hostname 0.0.0.0"
tinacms dev -c "vite --host 0.0.0.0"
tinacms dev -c "astro dev --host 0.0.0.0"
Docker Compose Example:
services:
app:
build: .
ports:
- "3000:3000"
command: npm run dev # Which runs: tinacms dev -c "next dev --hostname 0.0.0.0"
5. ❌ Missing _template Key Error
Error Message:
GetCollection failed: Unable to fetch
template name was not provided
Cause:
- Collection uses
templatesarray (multiple schemas) - Document missing
_templatefield in frontmatter - Migrating from
templatestofieldsand documents not updated
Solution:
Option 1: Use fields instead (recommended for single template)
{
name: 'post',
path: 'content/posts',
fields: [/* ... */] // No _template needed
}
Option 2: Ensure _template exists in frontmatter
---
_template: article # ← Required when using templates array
title: My Post
---
Migration Script (if converting from templates to fields):
# Remove _template from all files in content/posts/
find content/posts -name "*.md" -exec sed -i '/_template:/d' {} +
6. ❌ Path Mismatch Issues
Error:
- Files not appearing in Tina admin
- "File not found" errors when saving
- GraphQL queries return empty results
Cause:
pathin collection config doesn't match actual file directory- Relative vs absolute path confusion
- Trailing slash issues
Solution:
// Files located at: content/posts/hello.md
// ✅ Correct
{
name: 'post',
path: 'content/posts', // Matches file location
fields: [/* ... */]
}
// ❌ Wrong - Missing 'content/'
{
name: 'post',
path: 'posts', // Files won't be found
fields: [/* ... */]
}
// ❌ Wrong - Trailing slash
{
name: 'post',
path: 'content/posts/', // May cause issues
fields: [/* ... */]
}
Debugging:
- Run
npx @tinacms/cli@latest auditto check paths - Verify files exist in specified directory
- Check file extensions match
formatfield
7. ❌ Build Script Ordering Problems
Error Message:
ERROR: Cannot find module '../tina/__generated__/client'
ERROR: Property 'queries' does not exist on type '{}'
Cause:
- Framework build running before
tinacms build - Tina types not generated before TypeScript compilation
- CI/CD pipeline incorrect order
Solution:
{
"scripts": {
"build": "tinacms build && next build" // ✅ Tina FIRST
// NOT: "build": "next build && tinacms build" // ❌ Wrong order
}
}
CI/CD Example (GitHub Actions):
- name: Build
run: |
npx @tinacms/cli@latest build # Generate types first
npm run build # Then build framework
Why This Matters:
tinacms buildgenerates TypeScript types intina/__generated__/- Framework build needs these types to compile successfully
- Running in wrong order causes type errors
8. ❌ Failed Loading TinaCMS Assets
Error Message:
Failed to load resource: net::ERR_CONNECTION_REFUSED
http://localhost:4001/...
Causes:
- Pushed development
admin/index.htmlto production (loads assets from localhost) - Site served on subdirectory but
basePathnot configured
Solution:
For Production Deploys:
{
"scripts": {
"build": "tinacms build && next build" // ✅ Always build
// NOT: "build": "tinacms dev" // ❌ Never dev in production
}
}
For Subdirectory Deployments:
// tina/config.ts
export default defineConfig({
build: {
outputFolder: 'admin',
publicFolder: 'public',
basePath: 'your-subdirectory' // ← Set if site not at domain root
}
})
CI/CD Fix:
# GitHub Actions / Vercel / Netlify
- run: npx @tinacms/cli@latest build # Always use build, not dev
9. ❌ Reference Field 503 Service Unavailable
Error:
- Reference field dropdown times out with 503 error
- Admin interface becomes unresponsive when loading reference field
Cause:
- Too many items in referenced collection (100s or 1000s)
- No pagination support for reference fields currently
Solutions:
Option 1: Split collections
// Instead of one huge "authors" collection
// Split by active status or alphabetically
{
name: 'active_author',
label: 'Active Authors',
path: 'content/authors/active',
fields: [/* ... */]
}
{
name: 'archived_author',
label: 'Archived Authors',
path: 'content/authors/archived',
fields: [/* ... */]
}
Option 2: Use string field with validation
// Instead of reference
{
type: 'string',
name: 'authorId',
label: 'Author ID',
ui: {
component: 'select',
options: ['author-1', 'author-2', 'author-3'] // Curated list
}
}
Option 3: Custom field component (advanced)
- Implement pagination in custom component
- See TinaCMS docs: https://tina.io/docs/extending-tina/custom-field-components/
Deployment Patterns
Choose the deployment approach that fits your needs.
Option 1: TinaCloud (Managed) - Easiest ⭐
Best For: Quick setup, free tier, managed infrastructure
Steps:
- Sign up at https://app.tina.io
- Create project, get Client ID and Read Only Token
- Set environment variables:
NEXT_PUBLIC_TINA_CLIENT_ID=your_client_id TINA_TOKEN=your_read_only_token - Initialize backend:
npx @tinacms/cli@latest init backend - Deploy to hosting provider (Vercel, Netlify, Cloudflare Pages)
- 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:
-
Install dependencies:
npm install @tinacms/datalayer tinacms-authjs -
Initialize backend:
npx @tinacms/cli@latest init backend -
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) } } -
Update
tina/config.ts:export default defineConfig({ contentApiUrlOverride: '/api/tina/gql', // Your Workers endpoint // ... rest of config }) -
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" } } } } -
Deploy:
npx wrangler deploy
Pros:
- ✅ 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:
-
Install dependencies:
npm install @tinacms/datalayer tinacms-authjs -
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 handler -
Create
vercel.jsonrewrites:{ "rewrites": [ { "source": "/api/tina/:path*", "destination": "/api/tina/backend" } ] } -
Update dev script:
{ "scripts": { "dev": "TINA_PUBLIC_IS_LOCAL=true tinacms dev -c \"next dev --port $PORT\"" } } -
Set environment variables in Vercel dashboard:
NEXTAUTH_SECRET=your-secret TINA_PUBLIC_IS_LOCAL=false -
Deploy:
vercel deploy
Pros:
- ✅ 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:
-
Install dependencies:
npm install express serverless-http @tinacms/datalayer tinacms-authjs -
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) -
Create
netlify.toml:[functions] node_bundler = "esbuild" [[redirects]] from = "/api/tina/*" to = "/.netlify/functions/tina" status = 200 force = true -
Deploy:
netlify deploy --prod
Reference: 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=true
Security: 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-authjs
Configure:
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:
- Sign up at https://app.tina.io
- Create project
- Manage users in dashboard
- 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):
- ✅ ESbuild compilation errors (import issues)
- ✅ Module resolution problems
- ✅ Field naming constraint violations
- ✅ Docker binding issues
- ✅ Missing
_templatekey errors - ✅ Path mismatch problems
- ✅ Build script ordering failures
- ✅ Asset loading errors in production
- ✅ 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
GitHub Repository
Related Skills
evaluating-llms-harness
TestingThis Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
Algorithmic Art Generation
MetaThis skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.
webapp-testing
TestingThis Claude Skill provides a Playwright-based toolkit for testing local web applications through Python scripts. It enables frontend verification, UI debugging, screenshot capture, and log viewing while managing server lifecycles. Use it for browser automation tasks but run scripts directly rather than reading their source code to avoid context pollution.
