Initial commit
This commit is contained in:
17
templates/astro/.env.example
Normal file
17
templates/astro/.env.example
Normal 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
|
||||
29
templates/astro/astro.config.mjs
Normal file
29
templates/astro/astro.config.mjs
Normal 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)
|
||||
},
|
||||
})
|
||||
25
templates/astro/package.json
Normal file
25
templates/astro/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
65
templates/astro/tina-config.ts
Normal file
65
templates/astro/tina-config.ts
Normal 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',
|
||||
})
|
||||
148
templates/cloudflare-worker-backend/src/index.ts
Normal file
148
templates/cloudflare-worker-backend/src/index.ts
Normal 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
|
||||
*/
|
||||
55
templates/cloudflare-worker-backend/wrangler.jsonc
Normal file
55
templates/cloudflare-worker-backend/wrangler.jsonc
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
127
templates/collections/author.ts
Normal file
127
templates/collections/author.ts
Normal 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,
|
||||
}),
|
||||
},
|
||||
}
|
||||
146
templates/collections/blog-post.ts
Normal file
146
templates/collections/blog-post.ts
Normal 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,
|
||||
}),
|
||||
},
|
||||
}
|
||||
167
templates/collections/doc-page.ts
Normal file
167
templates/collections/doc-page.ts
Normal 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,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
262
templates/collections/landing-page.ts
Normal file
262
templates/collections/landing-page.ts
Normal 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}`
|
||||
},
|
||||
},
|
||||
}
|
||||
19
templates/nextjs/.env.example
Normal file
19
templates/nextjs/.env.example
Normal 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
|
||||
24
templates/nextjs/package.json
Normal file
24
templates/nextjs/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
88
templates/nextjs/tina-config-app-router.ts
Normal file
88
templates/nextjs/tina-config-app-router.ts
Normal 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',
|
||||
})
|
||||
64
templates/nextjs/tina-config-pages-router.ts
Normal file
64
templates/nextjs/tina-config-pages-router.ts
Normal 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',
|
||||
})
|
||||
18
templates/vite-react/.env.example
Normal file
18
templates/vite-react/.env.example
Normal 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
|
||||
25
templates/vite-react/package.json
Normal file
25
templates/vite-react/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
69
templates/vite-react/tina-config.ts
Normal file
69
templates/vite-react/tina-config.ts
Normal 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',
|
||||
})
|
||||
21
templates/vite-react/vite.config.ts
Normal file
21
templates/vite-react/vite.config.ts
Normal 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',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user