Initial commit
This commit is contained in:
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}`
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user