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,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',
},
})