Files
gh-yebot-rad-cc-plugins-plu…/skills/frontmatter-schemas/SKILL.md
2025-11-30 09:07:42 +08:00

12 KiB

Astro Frontmatter Schemas Skill

Common Zod schema patterns and frontmatter examples for Astro content collections.

When to Use This Skill

  • Designing content collection schemas
  • Creating frontmatter for markdown files
  • Validating content structure
  • Setting up type-safe content

Common Schema Patterns

Blog Post Schema

import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
  schema: z.object({
    // Core Fields
    title: z.string().max(80),
    description: z.string().max(160),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),

    // Author
    author: z.string(),
    authorUrl: z.string().url().optional(),

    // Categorization
    tags: z.array(z.string()).default([]),
    category: z.enum(['tutorial', 'news', 'guide', 'review']).optional(),

    // Publishing
    draft: z.boolean().default(false),
    featured: z.boolean().default(false),

    // Media
    image: z.object({
      url: z.string(),
      alt: z.string()
    }).optional(),

    // SEO
    canonicalURL: z.string().url().optional(),

    // Metadata
    readingTime: z.number().optional(),
    relatedPosts: z.array(z.string()).default([])
  })
});

Corresponding Frontmatter:

---
title: 'Getting Started with Astro'
description: 'Learn the fundamentals of building fast websites with Astro'
pubDate: 2024-01-15
updatedDate: 2024-01-20
author: 'John Doe'
authorUrl: 'https://example.com/john'
tags: ['astro', 'tutorial', 'web-dev']
category: 'tutorial'
draft: false
featured: true
image:
  url: './images/hero.jpg'
  alt: 'Astro logo with stars'
canonicalURL: 'https://example.com/blog/getting-started'
readingTime: 8
relatedPosts: ['advanced-astro', 'astro-tips']
---

Documentation Schema

const docs = defineCollection({
  loader: glob({ pattern: "**/*.mdx", base: "./src/content/docs" }),
  schema: z.object({
    title: z.string(),
    description: z.string(),

    // Sidebar
    sidebar: z.object({
      order: z.number(),
      label: z.string().optional(),
      hidden: z.boolean().default(false),
      badge: z.string().optional()
    }).optional(),

    // Status
    lastUpdated: z.coerce.date().optional(),
    version: z.string().optional(),
    deprecated: z.boolean().default(false),

    // Contributors
    contributors: z.array(z.string()).default([]),

    // Navigation
    prev: z.object({
      text: z.string(),
      link: z.string()
    }).optional(),
    next: z.object({
      text: z.string(),
      link: z.string()
    }).optional(),

    // Table of Contents
    tableOfContents: z.boolean().default(true),
    tocDepth: z.number().min(1).max(6).default(3)
  })
});

Corresponding Frontmatter:

---
title: 'API Reference'
description: 'Complete API documentation for the core library'
sidebar:
  order: 2
  label: 'API Docs'
  hidden: false
  badge: 'New'
lastUpdated: 2024-01-20
version: '2.1.0'
deprecated: false
contributors: ['john-doe', 'jane-smith']
prev:
  text: 'Installation'
  link: '/docs/installation'
next:
  text: 'Configuration'
  link: '/docs/configuration'
tableOfContents: true
tocDepth: 3
---

Product Schema

const products = defineCollection({
  loader: glob({ pattern: "**/*.json", base: "./src/content/products" }),
  schema: z.object({
    // Basic Info
    name: z.string(),
    description: z.string(),
    shortDescription: z.string().max(100).optional(),

    // Pricing
    price: z.number().positive(),
    currency: z.string().default('USD'),
    salePrice: z.number().positive().optional(),

    // Categorization
    category: z.enum(['software', 'hardware', 'service', 'digital']),
    tags: z.array(z.string()).default([]),

    // Inventory
    sku: z.string(),
    inStock: z.boolean().default(true),
    quantity: z.number().int().min(0).optional(),

    // Features
    features: z.array(z.string()),
    specifications: z.record(z.string()).optional(),

    // Media
    images: z.array(z.object({
      url: z.string(),
      alt: z.string(),
      primary: z.boolean().default(false)
    })),

    // Rating
    rating: z.number().min(0).max(5).optional(),
    reviewCount: z.number().int().min(0).default(0)
  })
});

Corresponding JSON:

{
  "name": "Premium Web Hosting",
  "description": "Fast, reliable web hosting with 99.9% uptime",
  "shortDescription": "Premium hosting solution",
  "price": 29.99,
  "currency": "USD",
  "salePrice": 19.99,
  "category": "service",
  "tags": ["hosting", "cloud", "premium"],
  "sku": "HOST-001",
  "inStock": true,
  "quantity": 100,
  "features": [
    "99.9% uptime guarantee",
    "Free SSL certificate",
    "24/7 support"
  ],
  "specifications": {
    "storage": "100GB SSD",
    "bandwidth": "Unlimited",
    "domains": "1"
  },
  "images": [
    {
      "url": "/images/hosting-main.jpg",
      "alt": "Server rack",
      "primary": true
    }
  ],
  "rating": 4.5,
  "reviewCount": 127
}

Team Member Schema

const team = defineCollection({
  loader: glob({ pattern: "**/*.yml", base: "./src/content/team" }),
  schema: z.object({
    name: z.string(),
    role: z.string(),
    department: z.enum(['engineering', 'design', 'marketing', 'sales']).optional(),

    bio: z.string(),

    avatar: z.string(),

    social: z.object({
      twitter: z.string().url().optional(),
      github: z.string().url().optional(),
      linkedin: z.string().url().optional(),
      website: z.string().url().optional()
    }).optional(),

    startDate: z.coerce.date(),
    location: z.string().optional(),

    skills: z.array(z.string()).default([]),
    featured: z.boolean().default(false)
  })
});

Corresponding YAML:

name: 'Jane Smith'
role: 'Senior Software Engineer'
department: 'engineering'
bio: 'Full-stack developer with 10 years experience building scalable web applications.'
avatar: '/images/team/jane.jpg'
social:
  twitter: 'https://twitter.com/janesmith'
  github: 'https://github.com/janesmith'
  linkedin: 'https://linkedin.com/in/janesmith'
  website: 'https://janesmith.dev'
startDate: 2020-03-15
location: 'San Francisco, CA'
skills: ['TypeScript', 'React', 'Node.js', 'AWS']
featured: true

Project/Portfolio Schema

const projects = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/content/projects" }),
  schema: z.object({
    title: z.string(),
    description: z.string(),

    technologies: z.array(z.string()),

    liveUrl: z.string().url().optional(),
    githubUrl: z.string().url().optional(),
    demoUrl: z.string().url().optional(),

    thumbnail: z.string(),
    images: z.array(z.string()).default([]),

    date: z.coerce.date(),
    endDate: z.coerce.date().optional(),

    status: z.enum(['active', 'completed', 'archived']).default('active'),
    featured: z.boolean().default(false),

    client: z.string().optional(),
    team: z.array(z.string()).default([]),

    tags: z.array(z.string()).default([])
  })
});

Corresponding Frontmatter:

---
title: 'E-commerce Platform Redesign'
description: 'Complete redesign and modernization of legacy e-commerce platform'
technologies: ['React', 'Next.js', 'TypeScript', 'Tailwind CSS', 'PostgreSQL']
liveUrl: 'https://shop.example.com'
githubUrl: 'https://github.com/example/shop'
thumbnail: './images/shop-thumb.jpg'
images:
  - './images/shop-1.jpg'
  - './images/shop-2.jpg'
  - './images/shop-3.jpg'
date: 2023-06-01
endDate: 2023-12-15
status: 'completed'
featured: true
client: 'Example Corp'
team: ['john-doe', 'jane-smith']
tags: ['web-dev', 'e-commerce', 'react']
---

Zod Validation Patterns

String Validation

// Basic string
title: z.string()

// Length constraints
title: z.string().min(1).max(100)
description: z.string().max(500)

// Email validation
email: z.string().email()

// URL validation
website: z.string().url()

// Regex pattern
slug: z.string().regex(/^[a-z0-9-]+$/)
phone: z.string().regex(/^\+?[1-9]\d{1,14}$/)

// Specific values
status: z.string().refine(val => ['draft', 'published'].includes(val))

Number Validation

// Basic number
count: z.number()

// Integer only
age: z.number().int()

// Positive numbers
price: z.number().positive()

// Range
rating: z.number().min(1).max(5)
percentage: z.number().min(0).max(100)

// With coercion (converts strings)
views: z.coerce.number()

Date Validation

// With coercion (recommended)
pubDate: z.coerce.date()

// Strict date object
deadline: z.date()

// Date range
startDate: z.coerce.date()
endDate: z.coerce.date().refine(
  (date) => date > startDate,
  "End date must be after start date"
)

Array Validation

// Array of strings
tags: z.array(z.string())

// Minimum items
tags: z.array(z.string()).min(1)

// Maximum items
images: z.array(z.string()).max(10)

// Default value
tags: z.array(z.string()).default([])

// Array of objects
authors: z.array(z.object({
  name: z.string(),
  email: z.string().email()
}))

Object Validation

// Nested object
author: z.object({
  name: z.string(),
  email: z.string().email(),
  url: z.string().url().optional()
})

// Record (dynamic keys)
metadata: z.record(z.string())
settings: z.record(z.boolean())

// Partial object (all fields optional)
options: z.object({
  theme: z.string(),
  color: z.string()
}).partial()

Enum Validation

// Fixed set of values
status: z.enum(['draft', 'published', 'archived'])
priority: z.enum(['low', 'medium', 'high'])
category: z.enum(['news', 'tutorial', 'guide'])

Boolean Validation

// Basic boolean
published: z.boolean()

// With default
draft: z.boolean().default(false)
featured: z.boolean().default(false)

Optional and Default Values

// Optional field
subtitle: z.string().optional()
updatedDate: z.coerce.date().optional()

// With default value
draft: z.boolean().default(false)
views: z.number().default(0)
tags: z.array(z.string()).default([])

Union Types

// Multiple possible types
content: z.union([
  z.string(),
  z.object({ html: z.string() })
])

// Discriminated union
media: z.discriminatedUnion('type', [
  z.object({
    type: z.literal('image'),
    url: z.string(),
    alt: z.string()
  }),
  z.object({
    type: z.literal('video'),
    url: z.string(),
    duration: z.number()
  })
])

Best Practices

  1. Use z.coerce.date() for date strings in frontmatter
  2. Provide defaults for booleans and arrays
  3. Use enums for fixed sets of values
  4. Validate formats with built-in validators (email, url)
  5. Add constraints (min, max) for better validation
  6. Use optional() for truly optional fields
  7. Document schemas with TypeScript comments
  8. Keep schemas DRY by extracting common patterns

Common Frontmatter Patterns

Minimal Blog Post

---
title: 'Post Title'
description: 'Post description'
pubDate: 2024-01-15
author: 'Author Name'
tags: ['tag1', 'tag2']
---

Full-Featured Blog Post

---
title: 'Comprehensive Post Title'
description: 'Detailed SEO-optimized description'
pubDate: 2024-01-15
updatedDate: 2024-01-20
author: 'Author Name'
authorUrl: 'https://author.com'
tags: ['web-dev', 'tutorial']
category: 'tutorial'
draft: false
featured: true
image:
  url: './hero.jpg'
  alt: 'Hero image description'
canonicalURL: 'https://example.com/original'
readingTime: 10
relatedPosts: ['post-1', 'post-2']
---

Documentation Page

---
title: 'Getting Started'
description: 'Introduction to the framework'
sidebar:
  order: 1
  label: 'Intro'
lastUpdated: 2024-01-15
contributors: ['john', 'jane']
tableOfContents: true
---

Product (JSON)

{
  "name": "Product Name",
  "description": "Product description",
  "price": 99.99,
  "category": "software",
  "sku": "PROD-001",
  "inStock": true,
  "features": ["Feature 1", "Feature 2"],
  "images": [
    { "url": "/img.jpg", "alt": "Product", "primary": true }
  ]
}

Tips

  • Always match frontmatter to schema exactly
  • Use ISO date format: 2024-01-15
  • Quote strings with special characters
  • Use arrays for multiple values: ['tag1', 'tag2']
  • Nest objects with proper indentation
  • Test with sample content first
  • Check generated types in .astro/types.d.ts