Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:25:40 +08:00
commit 69df674920
25 changed files with 4327 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"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"
},
"skills": [
"./"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# tinacms
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.

1752
SKILL.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,154 @@
# 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

129
plugin.lock.json Normal file
View File

@@ -0,0 +1,129 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:jezweb/claude-skills:skills/tinacms",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "400c8d7d5b20494cc309834adbcc646e89795c2b",
"treeHash": "0bc45f580a6e72fd7a2205584c2c2007615ac0ac5a1b62b6fd9b0d14f5ccc026",
"generatedAt": "2025-11-28T10:19:03.520835Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"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"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "345374a33d68f21054d4a91608abb47a8ce8b989d51d716cc65962971954b672"
},
{
"path": "SKILL.md",
"sha256": "3a23414177453d5fcaf9c0ae3c58ec4deae9fdf48e55e3ac864160538128ae39"
},
{
"path": "references/common-errors.md",
"sha256": "965f9864c31c43909351ddd64a590b1a82ac8f9fdb4923c92591c754346e887f"
},
{
"path": "scripts/check-versions.sh",
"sha256": "81ffc7c0d58cc5f9fe50d53c57761f0cab3c748861ce6bbc0c1944abbcaeff11"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "5453e8d3dbdbe6b1fb09e5e37fef2736c916a488ccee717748da6ad3e952e1ec"
},
{
"path": "templates/cloudflare-worker-backend/wrangler.jsonc",
"sha256": "ec1dc37cc41687d4987f7ed3280493685f72d5bd90355a329d25fd226713a189"
},
{
"path": "templates/cloudflare-worker-backend/src/index.ts",
"sha256": "6e8b4b616283809a0924514c0fd7fbd777befb5401d5fa0aa82f4335e36ace77"
},
{
"path": "templates/nextjs/tina-config-pages-router.ts",
"sha256": "6ed3b89206df2c0da1876abecf6efbb9c38ee02abcda7db24cc3ab7fb20a114d"
},
{
"path": "templates/nextjs/package.json",
"sha256": "5f9441b2927cd8e01bc33f1fbd3c9abf00752536065e98c8fb22c461680d77b3"
},
{
"path": "templates/nextjs/tina-config-app-router.ts",
"sha256": "41ff3aa4f21658b29e1619c8202d5bea3d31945aa4f7c32a647d581f07e661f3"
},
{
"path": "templates/nextjs/.env.example",
"sha256": "46c87a8855083f88835d3c383a115bd393e8af07260bd92ce5121a4081058d26"
},
{
"path": "templates/astro/astro.config.mjs",
"sha256": "4773ee6afe0104912e525eadbbaa225d213c75f315579dd99eb60352ee1fb75a"
},
{
"path": "templates/astro/package.json",
"sha256": "cd8be334e9c9fd98e1d17f40e58a9bcfe46ea006efee36f93fb7a50eae332d4b"
},
{
"path": "templates/astro/tina-config.ts",
"sha256": "66e71d8bc778b0599a7c3acf852f4b8df73ec3018fdc9333cf45677a9f3ee0c6"
},
{
"path": "templates/astro/.env.example",
"sha256": "48b9bd8607b943e920f3acd710c7c8817cdf75da457e1cc73373e9c1d87af9c4"
},
{
"path": "templates/vite-react/package.json",
"sha256": "9c06e396c0cca07f6519d3bcaf1da94ce4f198b2cffba204e3fc440a4a303468"
},
{
"path": "templates/vite-react/tina-config.ts",
"sha256": "6c33063bda0ee8fa4102621714cc0627dcac98672cdb4b57ae4e113f19440590"
},
{
"path": "templates/vite-react/.env.example",
"sha256": "27bc67321a4601b6e6911c8f11f23faf76b406d631ba49d9631f086add46dcc8"
},
{
"path": "templates/vite-react/vite.config.ts",
"sha256": "79f38283a8c3a9456ea995fcb8c8ce4bf51a36fe505fbb05ffe3a77f2df59deb"
},
{
"path": "templates/collections/blog-post.ts",
"sha256": "533ce87c5594915d6274bcdbc24c2ec80db97ad10e2a46221e35e1fbcf35e237"
},
{
"path": "templates/collections/landing-page.ts",
"sha256": "81bc8481e4f657a92948e74b648cbfce23bddf8a6a92cbc01a2ccfdac06d77c9"
},
{
"path": "templates/collections/author.ts",
"sha256": "d541878e50c0b5798d830b30143ecf9f5d862c840af254f23e06ce881da77d18"
},
{
"path": "templates/collections/doc-page.ts",
"sha256": "c9448083d9eedc42d39ab829e69ba2c7a4de765f86d02cf6d3006ff588d67bc7"
},
{
"path": "assets/links-to-official-docs.md",
"sha256": "b0ff88175119eeaa2d60e1053f2f006cac4a94ad6a626f7a552a033761dc2424"
}
],
"dirSha256": "0bc45f580a6e72fd7a2205584c2c2007615ac0ac5a1b62b6fd9b0d14f5ccc026"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

816
references/common-errors.md Normal file
View File

@@ -0,0 +1,816 @@
# 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](#1-esbuild-compilation-errors)
2. [Module Resolution Issues](#2-module-resolution-issues)
3. [Field Naming Constraints](#3-field-naming-constraints)
4. [Docker Binding Issues](#4-docker-binding-issues)
5. [Missing _template Key Error](#5-missing-_template-key-error)
6. [Path Mismatch Issues](#6-path-mismatch-issues)
7. [Build Script Ordering Problems](#7-build-script-ordering-problems)
8. [Failed Loading TinaCMS Assets](#8-failed-loading-tinacms-assets)
9. [Reference Field 503 Service Unavailable](#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 loaders
```
### Causes
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 `window` object
- 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
```typescript
// ❌ 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:
```typescript
// 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
```typescript
// ✅ Import only types (doesn't execute code)
import type { HeroProps } from '../components/Hero'
```
### Prevention
- Keep `tina/config.ts` minimal
- 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 declarations
```
### Causes
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**
- `react` and `react-dom` not installed (required even for non-React frameworks)
- `@tinacms/cli` not in devDependencies
### Solutions
#### Solution 1: Clean Reinstall (npm)
```bash
# Remove node_modules and lock file
rm -rf node_modules package-lock.json
# Clear npm cache
npm cache clean --force
# Reinstall all dependencies
npm install
```
#### Solution 2: Clean Reinstall (pnpm)
```bash
# Remove node_modules and lock file
rm -rf node_modules pnpm-lock.yaml
# Clear pnpm cache
pnpm store prune
# Reinstall all dependencies
pnpm install
```
#### Solution 3: Clean Reinstall (yarn)
```bash
# Remove node_modules and lock file
rm -rf node_modules yarn.lock
# Clear yarn cache
yarn cache clean
# Reinstall all dependencies
yarn install
```
#### Solution 4: Ensure React Dependencies (Non-React Frameworks)
Even if you're using Hugo, Jekyll, or Eleventy, you MUST install React:
```bash
npm install react@^19 react-dom@^19
```
### Prevention
- Use lockfiles (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`)
- Don't use `--no-optional` or `--omit=optional` flags
- Ensure `react` and `react-dom` are 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 alphanumeric
```
### Causes
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
```typescript
// ❌ Bad
{
name: 'hero-image',
label: 'Hero Image',
type: 'image'
}
// ✅ Good
{
name: 'hero_image',
label: 'Hero Image',
type: 'image'
}
```
#### Solution 2: Use camelCase
```typescript
// ✅ Also good
{
name: 'heroImage',
label: 'Hero Image',
type: 'image'
}
```
#### Solution 3: Migrate Existing Content
If migrating from Forestry.io with hyphenated field names:
```bash
# 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_REFUSED
```
### Causes
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
```bash
tinacms dev -c "next dev --hostname 0.0.0.0"
```
Or in `package.json`:
```json
{
"scripts": {
"dev": "tinacms dev -c \"next dev --hostname 0.0.0.0\""
}
}
```
#### Solution 2: Vite
```bash
tinacms dev -c "vite --host 0.0.0.0"
```
Or in `vite.config.ts`:
```typescript
export default defineConfig({
server: {
host: '0.0.0.0',
port: 3000
}
})
```
#### Solution 3: Astro
```bash
tinacms dev -c "astro dev --host 0.0.0.0"
```
Or in `astro.config.mjs`:
```javascript
export default defineConfig({
server: {
host: '0.0.0.0',
port: 4321
}
})
```
#### Solution 4: Docker Compose
```yaml
version: '3'
services:
app:
build: .
ports:
- "3000:3000"
command: npm run dev # Must include --hostname 0.0.0.0
```
### Prevention
- Always use `--host 0.0.0.0` in 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 field
```
### Causes
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`:
```typescript
// ❌ 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`:
```yaml
---
_template: article
title: My Blog Post
date: 2025-10-24
---
Content here...
```
#### Solution 3: Batch Update Existing Files
```bash
# Add _template field to all files
find content/posts -name "*.md" -exec sed -i '1a _template: article' {} +
```
#### Solution 4: Set Default Template
```typescript
{
name: 'post',
path: 'content/posts',
templates: [
{
name: 'article',
fields: [/* ... */]
}
],
ui: {
defaultItem: () => ({
_template: 'article' // Default template for new documents
})
}
}
```
### Prevention
- Use `fields` for single-schema collections
- Use `templates` only when you truly need multiple schemas
- Document `_template` requirement 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 results
```
### Causes
The `path` in your collection config doesn't match where files actually live.
### Solutions
#### Solution 1: Verify File Locations
```bash
# 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
```typescript
// ❌ Bad
{
path: 'content/posts/' // Trailing slash may cause issues
}
// ✅ Good
{
path: 'content/posts' // No trailing slash
}
```
#### Solution 3: Use Relative Paths
```typescript
// ✅ Path relative to project root
{
path: 'content/posts'
}
// ❌ Don't use absolute paths
{
path: '/home/user/project/content/posts'
}
```
#### Solution 4: Audit Paths
```bash
# TinaCMS CLI includes audit command
npx @tinacms/cli@latest audit
# Shows which files match collections
# Helps identify path mismatches
```
### Prevention
- Always verify file structure before setting `path`
- Use `npx @tinacms/cli@latest audit` to 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
```json
{
"scripts": {
"build": "tinacms build && next build" // ✅ Tina FIRST
// NOT: "build": "next build && tinacms build" // ❌ Wrong
}
}
```
#### Solution 2: CI/CD Fix (GitHub Actions)
```yaml
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 works
```
#### Solution 3: Vercel Configuration
```json
// vercel.json
{
"buildCommand": "tinacms build && next build"
}
```
#### Solution 4: Netlify Configuration
```toml
# netlify.toml
[build]
command = "tinacms build && npm run build"
```
### Prevention
- Always run `tinacms build` before 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 resource
```
### Causes
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
```json
{
"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/`:
```typescript
// tina/config.ts
export default defineConfig({
build: {
outputFolder: 'admin',
publicFolder: 'public',
basePath: 'blog' // ← Subdirectory path
}
})
```
#### Solution 3: Verify .gitignore
```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
```yaml
# GitHub Actions
- run: npx @tinacms/cli@latest build # Not dev!
```
### Prevention
- Use `tinacms build` in all CI/CD
- Set `basePath` for subdirectory deploys
- Don't commit `admin/index.html` from dev mode
---
## 9. Reference Field 503 Service Unavailable
### Error Messages
```
503 Service Unavailable
Request timed out
Reference field dropdown not loading
```
### Causes
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:
```typescript
// ❌ 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:
```typescript
// ❌ 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:
```typescript
// 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%

92
scripts/check-versions.sh Executable file
View File

@@ -0,0 +1,92 @@
#!/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 ""

View File

@@ -0,0 +1,17 @@
# 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

View File

@@ -0,0 +1,29 @@
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)
},
})

View File

@@ -0,0 +1,25 @@
{
"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"
}
}

View File

@@ -0,0 +1,65 @@
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',
})

View File

@@ -0,0 +1,148 @@
/**
* 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
*/

View File

@@ -0,0 +1,55 @@
{
"$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"
}
]
}

View File

@@ -0,0 +1,127 @@
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,
}),
},
}

View File

@@ -0,0 +1,146 @@
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,
}),
},
}

View File

@@ -0,0 +1,167 @@
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,
},
}),
},
}

View File

@@ -0,0 +1,262 @@
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}`
},
},
}

View File

@@ -0,0 +1,19 @@
# 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

View File

@@ -0,0 +1,24 @@
{
"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"
}
}

View File

@@ -0,0 +1,88 @@
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',
})

View File

@@ -0,0 +1,64 @@
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',
})

View File

@@ -0,0 +1,18 @@
# 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

View File

@@ -0,0 +1,25 @@
{
"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"
}
}

View File

@@ -0,0 +1,69 @@
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',
})

View File

@@ -0,0 +1,21 @@
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',
},
})