mdxui

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:

  1. Blog Components - For content publishing and articles

    • Blog - Blog listing page (auto-wires to posts collection)
    • BlogPost - Individual blog article
  2. Documentation Components - For technical documentation

    • Docs - Documentation listing (auto-wires to docs collection)
    • 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

  1. Component Detection - Framework detects @autowire JSDoc tags
  2. Collection Mapping - Maps component to Payload collection via siteAutoWiring
  3. Query Construction - Builds Payload query from props (slug, filters, etc.)
  4. Data Fetching - Executes query and passes data to component
  5. 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>

On this page