Content Components
Blog and documentation components that auto-wire to Payload CMS collections
Content Components
The mdxui framework provides specialized content components for blogs and documentation that automatically integrate with Payload CMS collections. These components handle both content listing and individual page rendering with built-in features like navigation, search, and SEO.
Overview
Content components come in two pairs:
-
Blog Components - For content publishing and articles
Blog- Blog listing page (auto-wires topostscollection)BlogPost- Individual blog article
-
Documentation Components - For technical documentation
Docs- Documentation listing (auto-wires todocscollection)DocsPage- Individual documentation page
All content components support both automatic fetching from Payload CMS and manual data override for maximum flexibility.
Blog Component
The <Blog> component provides a listing page for blog posts with filtering, search, and pagination.
Interface
import type { BlogProps } from 'mdxui'
interface BlogProps {
/** Filter by site ID */
site?: string
/** Number of posts to show */
limit?: number
/** Layout variant */
layout?: 'grid' | 'list' | 'featured'
/** Show category filter */
showCategories?: boolean
/** Manual override - pass posts directly */
posts?: any[]
}Basic Usage
import { Blog } from '@mdxui/beacon'
<Blog layout="grid" limit={12} showCategories={true} />Auto-Wiring
The Blog component automatically queries the Payload posts collection:
// Automatic query executed by the component
const { docs: posts } = await payload.find({
collection: 'posts',
limit: 10,
sort: '-publishedAt',
depth: 2
})Layout Variants
Grid Layout - Cards in grid format:
<Blog layout="grid" limit={9} />List Layout - Stacked list format:
<Blog layout="list" limit={10} />Featured Layout - Hero post plus grid:
<Blog layout="featured" limit={7} />Manual Posts Override
Bypass auto-fetching with your own data:
const customPosts = [
{
title: "Building Modern Web Apps",
slug: "modern-web-apps",
excerpt: "Learn how to build...",
publishedAt: "2025-01-15",
author: { name: "Jane Doe", avatar: "/avatars/jane.jpg" },
category: { name: "Tutorials" },
coverImage: "/images/post-cover.jpg"
},
// ... more posts
]
<Blog posts={customPosts} layout="grid" />BlogPost Component
The <BlogPost> component renders individual blog articles with author bio, metadata, and related posts.
Interface
import type { BlogPostProps } from 'mdxui'
interface BlogPostProps {
/** Post slug or ID */
slug?: string
/** Manual override - pass post directly */
post?: any
/** Show author info */
showAuthor?: boolean
/** Show related posts */
showRelated?: boolean
}Basic Usage
import { BlogPost } from '@mdxui/beacon'
<BlogPost slug="getting-started" showAuthor={true} showRelated={true} />MDX Integration
Perfect for MDX blog posts:
import { BlogPost } from '@mdxui/beacon'
<BlogPost slug="intro-to-typescript" showAuthor={true}>
# Introduction to TypeScript
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
## Why TypeScript?
TypeScript adds static typing to JavaScript, which helps catch errors early...
## Getting Started
Install TypeScript globally:
```bash
npm install -g typescript
```
Create your first TypeScript file...
</BlogPost>Features
The BlogPost component includes:
- Hero Image - Cover image with optimized loading
- Author Bio - Author information with avatar and social links
- Reading Time - Estimated reading time calculation
- Publish Date - Formatted publication date
- Category Badge - Post category tag
- Tags - Clickable topic tags
- Social Sharing - Share to Twitter, LinkedIn, Facebook
- Related Posts - Recommended related content based on tags/category
- Table of Contents - Optional sticky TOC for long articles
- Code Syntax Highlighting - Highlighted code blocks
- SEO Optimization - Automatic meta tags and Open Graph
Auto-Wiring to Payload
When you provide a slug, the component automatically fetches the post:
// Automatic query
const post = await payload.findOne({
collection: 'posts',
where: {
slug: { equals: slug }
},
depth: 2 // Include author and category data
})
// Fetch related posts
const { docs: relatedPosts } = await payload.find({
collection: 'posts',
where: {
id: { not_equals: post.id },
or: [
{ category: { equals: post.category.id } },
{ tags: { in: post.tags.map(t => t.id) } }
]
},
limit: 3,
sort: '-publishedAt'
})Payload Collection Schema
Expected schema for the posts collection:
const Posts = {
slug: 'posts',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', required: true, unique: true },
{ name: 'excerpt', type: 'textarea' },
{ name: 'content', type: 'richText', required: true },
{ name: 'coverImage', type: 'upload', relationTo: 'media' },
{ name: 'author', type: 'relationship', relationTo: 'users', required: true },
{ name: 'category', type: 'relationship', relationTo: 'categories' },
{ name: 'tags', type: 'relationship', relationTo: 'tags', hasMany: true },
{ name: 'publishedAt', type: 'date', required: true },
{ name: 'updatedAt', type: 'date' },
{ name: 'featured', type: 'checkbox' },
{ name: 'readTime', type: 'number' },
]
}Docs Component
The <Docs> component provides a documentation listing with sidebar navigation and search.
Interface
import type { DocsProps } from 'mdxui'
interface DocsProps {
/** Filter by project ID */
project?: string
/** Show sidebar */
showSidebar?: boolean
/** Show table of contents */
showToc?: boolean
/** Enable search */
showSearch?: boolean
/** Manual override - pass docs directly */
docs?: any[]
}Basic Usage
import { Docs } from '@mdxui/beacon'
<Docs project="my-docs" showSidebar={true} showSearch={true} />Auto-Wiring
The Docs component automatically queries the docs collection:
// Automatic query
const { docs } = await payload.find({
collection: 'docs',
sort: 'order',
depth: 2
})DocsPage Component
The <DocsPage> component renders individual documentation pages with sidebar, table of contents, and navigation.
Interface
import type { DocsPageProps } from 'mdxui'
interface DocsPageProps {
/** Doc slug or ID */
slug?: string
/** Manual override - pass doc directly */
doc?: any
/** Show edit link */
showEdit?: boolean
/** Show last updated */
showLastUpdated?: boolean
}Basic Usage
import { DocsPage } from '@mdxui/beacon'
<DocsPage slug="getting-started" showEdit={true} showLastUpdated={true} />MDX Integration
Perfect for MDX documentation:
import { DocsPage } from '@mdxui/beacon'
<DocsPage slug="api-reference" showEdit={true}>
# API Reference
Complete reference for all components and utilities.
## Components
### Button
The Button component provides...
```tsx
import { Button } from '@mdxui/beacon'
<Button variant="primary">Click me</Button>
```
### Input
The Input component...
</DocsPage>Features
The DocsPage component includes:
- Sidebar Navigation - Collapsible sidebar with doc tree
- Table of Contents - Auto-generated from h2/h3 headings with active scrollspy
- Breadcrumbs - Navigation breadcrumbs showing current location
- Prev/Next Links - Navigate between related docs
- Edit Link - Link to edit source on GitHub
- Last Updated - Show when doc was last modified
- Search Integration - Built-in search functionality
- Mobile Responsive - Sidebar collapses on mobile
- Keyboard Navigation - Full keyboard support
Three-Column Layout
Desktop layout:
┌─────────┬────────────────┬──────────┐
│ Sidebar │ Main Content │ TOC │
│ (nav) │ (article) │ (headings)│
│ 256px │ flex-1 │ 256px │
└─────────┴────────────────┴──────────┘Auto-Wiring to Payload
// Automatic query
const doc = await payload.findOne({
collection: 'docs',
where: {
slug: { equals: slug }
}
})Payload Collection Schema
Expected schema for the docs collection:
const Docs = {
slug: 'docs',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', required: true, unique: true },
{ name: 'description', type: 'text' },
{ name: 'content', type: 'richText' },
{ name: 'category', type: 'relationship', relationTo: 'doc-categories' },
{ name: 'order', type: 'number' },
{ name: 'lastUpdated', type: 'date' },
{ name: 'author', type: 'relationship', relationTo: 'users' },
]
}Content Rendering Patterns
Server-Side Rendering
Fetch content at build time or on request:
// Next.js App Router
export default async function BlogPostPage({ params }) {
const post = await payload.findOne({
collection: 'posts',
where: { slug: { equals: params.slug } }
})
return <BlogPost post={post} />
}Client-Side Fetching
Load content on the client:
'use client'
import { BlogPost } from '@mdxui/beacon'
import { useEffect, useState } from 'react'
export function BlogPostClient({ slug }) {
const [post, setPost] = useState(null)
useEffect(() => {
fetch(`/api/posts/${slug}`)
.then(r => r.json())
.then(setPost)
}, [slug])
if (!post) return <div>Loading...</div>
return <BlogPost post={post} />
}Hybrid Approach
Use auto-wiring with manual override:
// Auto-fetch in development, static in production
const isDev = process.env.NODE_ENV === 'development'
{isDev ? (
<BlogPost slug="my-post" />
) : (
<BlogPost post={staticPostData} />
)}Static Generation
Pre-render all posts at build time:
// Next.js generateStaticParams
export async function generateStaticParams() {
const { docs: posts } = await payload.find({
collection: 'posts',
limit: 1000
})
return posts.map(post => ({
slug: post.slug
}))
}MDX Integration
Using Content Components in MDX
Content components are designed to work seamlessly with MDX:
import { BlogPost } from '@mdxui/beacon'
<BlogPost slug="my-article">
# My Article
This is **markdown** content that gets rendered inside the BlogPost component.
## Features
- Feature 1
- Feature 2
```tsx
// Code blocks work too
const hello = "world"
```
</BlogPost>MDX Component Provider
Make content components available globally in MDX:
// mdx-components.tsx
import { Blog, BlogPost, Docs, DocsPage } from '@mdxui/beacon'
export function useMDXComponents(components) {
return {
...components,
Blog,
BlogPost,
Docs,
DocsPage,
}
}Frontmatter Integration
Combine frontmatter with content components:
---
title: Getting Started Guide
slug: getting-started
showEdit: true
---
import { DocsPage } from '@mdxui/beacon'
<DocsPage slug="getting-started" showEdit={frontmatter.showEdit}>
# {frontmatter.title}
Your content here...
</DocsPage>Custom MDX Components
Extend content components with custom MDX elements:
const mdxComponents = {
BlogPost,
DocsPage,
// Custom components
Callout: ({ children, type }) => (
<div className={`callout callout-${type}`}>{children}</div>
),
CodeBlock: ({ children, language }) => (
<pre><code className={`language-${language}`}>{children}</code></pre>
)
}SEO Considerations
Meta Tags
Content components should include proper meta tags:
import { Metadata } from 'next'
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await payload.findOne({
collection: 'posts',
where: { slug: { equals: params.slug } }
})
return {
title: post.title,
description: post.excerpt,
authors: [{ name: post.author.name }],
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author.name],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}Structured Data
Add JSON-LD for rich search results:
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": post.title,
"image": post.coverImage,
"datePublished": post.publishedAt,
"dateModified": post.updatedAt,
"author": {
"@type": "Person",
"name": post.author.name
},
"publisher": {
"@type": "Organization",
"name": "Your Site",
"logo": {
"@type": "ImageObject",
"url": "https://yoursite.com/logo.png"
}
}
})}
</script>Canonical URLs
Prevent duplicate content issues:
<link rel="canonical" href={`https://yoursite.com/blog/${post.slug}`} />Sitemap Generation
Include content pages in your sitemap:
// app/sitemap.ts
export default async function sitemap() {
const { docs: posts } = await payload.find({
collection: 'posts',
limit: 1000
})
return posts.map(post => ({
url: `https://yoursite.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly',
priority: 0.8,
}))
}RSS Feed
Generate RSS feed for blog posts:
// app/blog/rss.xml/route.ts
export async function GET() {
const { docs: posts } = await payload.find({
collection: 'posts',
limit: 20,
sort: '-publishedAt'
})
const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Your Blog</title>
<link>https://yoursite.com/blog</link>
<description>Latest blog posts</description>
${posts.map(post => `
<item>
<title>${post.title}</title>
<link>https://yoursite.com/blog/${post.slug}</link>
<description>${post.excerpt}</description>
<pubDate>${new Date(post.publishedAt).toUTCString()}</pubDate>
</item>
`).join('')}
</channel>
</rss>`
return new Response(rss, {
headers: {
'Content-Type': 'application/xml',
},
})
}Auto-Wire Configuration
The auto-wiring system maps components to Payload collections:
import { siteAutoWiring } from 'mdxui'
// Auto-wire configuration
export const siteAutoWiring = {
Blog: {
collection: 'posts',
defaultQuery: {
limit: 10,
sort: '-publishedAt',
},
},
BlogPost: {
collection: 'posts',
},
Docs: {
collection: 'docs',
defaultQuery: {
sort: 'order',
},
},
DocsPage: {
collection: 'docs',
},
}How Auto-Wiring Works
- Component Detection - Framework detects
@autowireJSDoc tags - Collection Mapping - Maps component to Payload collection via
siteAutoWiring - Query Construction - Builds Payload query from props (slug, filters, etc.)
- Data Fetching - Executes query and passes data to component
- Manual Override - If manual data provided, skips auto-fetching
Custom Auto-Wire Queries
Override default queries:
import { createAutoWire } from 'mdxui'
const customAutoWire = createAutoWire({
Blog: {
collection: 'posts',
defaultQuery: {
limit: 20,
sort: '-publishedAt',
where: {
featured: { equals: true }
}
},
},
})TypeScript
Full type definitions for content components:
import type { Component } from 'mdxui'
import type { z } from 'zod'
// Blog types
export const BlogPropsSchema = z.object({
site: z.string().optional(),
limit: z.number().default(10),
layout: z.enum(['grid', 'list', 'featured']).default('grid'),
showCategories: z.boolean().default(true),
posts: z.array(z.any()).optional(),
})
export type BlogProps = z.infer<typeof BlogPropsSchema>
export type Blog = Component<BlogProps>
// BlogPost types
export const BlogPostPropsSchema = z.object({
slug: z.string().optional(),
post: z.any().optional(),
showAuthor: z.boolean().default(true),
showRelated: z.boolean().default(true),
})
export type BlogPostProps = z.infer<typeof BlogPostPropsSchema>
export type BlogPost = Component<BlogPostProps>
// Docs types
export const DocsPropsSchema = z.object({
project: z.string().optional(),
showSidebar: z.boolean().default(true),
showToc: z.boolean().default(true),
showSearch: z.boolean().default(true),
docs: z.array(z.any()).optional(),
})
export type DocsProps = z.infer<typeof DocsPropsSchema>
export type Docs = Component<DocsProps>
// DocsPage types
export const DocsPagePropsSchema = z.object({
slug: z.string().optional(),
doc: z.any().optional(),
showEdit: z.boolean().default(true),
showLastUpdated: z.boolean().default(true),
})
export type DocsPageProps = z.infer<typeof DocsPagePropsSchema>
export type DocsPage = Component<DocsPageProps>Related Components
- BlogPage Component - Blog listing page
- Page Component - Generic page layout
- Site Component - Root site wrapper
- Header Component - Navigation header