BlogPost Component
Individual blog article layout with author, metadata, and related posts
BlogPost Component
The <BlogPost> component provides a layout for individual blog articles with author bio, publish date, reading time, social sharing, 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
}Usage
Basic Example
import { BlogPost } from '@mdxui/beacon'
<BlogPost slug="getting-started-react">
<h1>Getting Started with React</h1>
<p>React is a JavaScript library for building user interfaces...</p>
<h2>Installation</h2>
<p>First, install React using npm:</p>
<CodeBlock language="bash">
npm install react react-dom
</CodeBlock>
<h2>Your First Component</h2>
<p>Let's create a simple component...</p>
</BlogPost>MDX Usage
Perfect for MDX blog posts:
import { BlogPost } from '@mdxui/beacon'
<BlogPost slug="intro-to-typescript" showAuthor={true} showRelated={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
```
</BlogPost>With Manual Post Data
Override auto-fetching:
const postData = {
title: "Building Modern Web Apps",
slug: "modern-web-apps",
content: "...",
author: {
name: "Jane Doe",
avatar: "/avatars/jane.jpg",
bio: "Software engineer and writer"
},
publishedAt: "2025-01-15",
coverImage: "/images/post-cover.jpg",
category: "Tutorials",
tags: ["React", "TypeScript", "Web Development"]
}
<BlogPost post={postData} showAuthor={true}>
{/* Content rendered from post.content */}
</BlogPost>Auto-Wire to Payload
Automatically fetch from Payload CMS:
<BlogPost slug="getting-started" />Props
slug (optional)
Type: string
Post slug for fetching from Payload CMS.
<BlogPost slug="react-hooks-guide" />post (optional)
Type: any
Manual post data override. Bypasses auto-fetching.
<BlogPost post={{ title: "...", content: "..." }} />showAuthor (optional)
Type: boolean (default: true)
Show author bio section at the end of the post.
<BlogPost slug="guide" showAuthor={true} />showRelated (optional)
Type: boolean (default: true)
Show related posts section at the end.
<BlogPost slug="guide" showRelated={true} />Implementation Details
The reference implementation from @mdxui/beacon includes:
Features
- Hero Image - Cover image with parallax effect
- Author Bio - Author info with avatar and social links
- Reading Time - Estimated reading time
- Publish Date - Formatted publication date
- Category Badge - Post category tag
- Tags - Clickable topic tags
- Social Sharing - Share to Twitter, LinkedIn, etc.
- Related Posts - Recommended related content
- Table of Contents - Optional sticky TOC
- Typography - Beautiful prose styling
- Code Syntax Highlighting - Highlighted code blocks
- Image Zoom - Click to zoom images
- Copy Link - Copy post URL
Structure
<article className="mx-auto max-w-4xl">
{/* Cover Image */}
{post.coverImage && (
<div className="relative aspect-video overflow-hidden rounded-lg">
<img
src={post.coverImage}
alt={post.title}
className="object-cover"
/>
</div>
)}
{/* Header */}
<header className="mt-8 space-y-4">
{/* Category */}
{post.category && (
<Badge variant="secondary">{post.category.name}</Badge>
)}
{/* Title */}
<h1 className="text-4xl font-bold tracking-tight md:text-5xl">
{post.title}
</h1>
{/* Metadata */}
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Avatar>
<AvatarImage src={post.author.avatar} />
<AvatarFallback>{post.author.name[0]}</AvatarFallback>
</Avatar>
{post.author.name}
</div>
<time dateTime={post.publishedAt}>
{formatDate(post.publishedAt)}
</time>
<span>{post.readTime} min read</span>
</div>
{/* Social Share */}
<ShareButtons url={postUrl} title={post.title} />
</header>
{/* Content */}
<div className="prose lg:prose-xl dark:prose-invert mt-12">
{children || post.content}
</div>
{/* Tags */}
{post.tags && (
<div className="mt-12 flex flex-wrap gap-2">
{post.tags.map(tag => (
<Badge key={tag.id} variant="outline">
#{tag.name}
</Badge>
))}
</div>
)}
{/* Author Bio */}
{showAuthor && (
<AuthorBio
name={post.author.name}
avatar={post.author.avatar}
bio={post.author.bio}
social={post.author.social}
className="mt-12"
/>
)}
{/* Related Posts */}
{showRelated && (
<RelatedPosts
posts={relatedPosts}
className="mt-12"
/>
)}
</article>Auto-Wiring
The BlogPost component auto-wires to Payload CMS:
How It Works
- Component receives
slugprop - Queries Payload
postscollection - Renders post metadata and content
- Fetches related posts based on tags/category
- Displays author information
Payload Collection Schema
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' },
]
}Example 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'
})Common Use Cases
Technical Tutorial
<BlogPost slug="react-hooks-tutorial" showAuthor={true}>
<h1>Mastering React Hooks</h1>
<p>
React Hooks revolutionized how we write components. In this guide,
we'll explore the most important hooks...
</p>
<h2>useState Hook</h2>
<p>The useState hook lets you add state to function components:</p>
<CodeBlock language="tsx">
{`const [count, setCount] = useState(0)`}
</CodeBlock>
<h2>useEffect Hook</h2>
<p>The useEffect hook handles side effects:</p>
<CodeBlock language="tsx">
{effectExample}
</CodeBlock>
<Callout type="tip">
Remember to always clean up your effects!
</Callout>
</BlogPost>Product Announcement
<BlogPost slug="product-launch-v2" showRelated={false}>
<h1>Announcing Product 2.0</h1>
<p>
Today we're excited to announce the biggest update to our product
since launch...
</p>
<h2>What's New</h2>
<ul>
<li>Complete UI redesign</li>
<li>3x faster performance</li>
<li>New collaboration features</li>
</ul>
<h2>Migration Guide</h2>
<p>Existing users can upgrade by following these steps...</p>
<Button>
Get Started with 2.0
</Button>
</BlogPost>Company Update
<BlogPost slug="2024-year-in-review" showAuthor={true}>
<h1>2024 Year in Review</h1>
<p>
What a year! Here are some highlights from our journey...
</p>
<h2>Product Milestones</h2>
<ul>
<li>Reached 1 million users</li>
<li>Launched 15 new features</li>
<li>Expanded to 50 countries</li>
</ul>
<h2>Team Growth</h2>
<p>We grew our team from 20 to 50 amazing people...</p>
<h2>Looking Ahead</h2>
<p>Here's what we're excited about for 2025...</p>
</BlogPost>Case Study
<BlogPost slug="customer-success-story" showRelated={true}>
<h1>How Acme Corp Increased Revenue by 300%</h1>
<Callout type="info">
Industry: E-commerce | Company Size: 50-100 employees
</Callout>
<h2>The Challenge</h2>
<p>
Acme Corp was struggling with slow checkout times and cart abandonment...
</p>
<h2>The Solution</h2>
<p>
By implementing our platform, they were able to...
</p>
<h2>The Results</h2>
<ul>
<li>300% increase in revenue</li>
<li>50% reduction in cart abandonment</li>
<li>2x faster checkout process</li>
</ul>
<blockquote>
"This platform transformed our business. We couldn't be happier."
<cite>- CEO, Acme Corp</cite>
</blockquote>
</BlogPost>Author Bio Component
Display author information:
const AuthorBio = ({ name, avatar, bio, social }) => {
return (
<div className="border-t border-b py-8 my-12">
<div className="flex items-start gap-6">
<Avatar className="h-20 w-20">
<AvatarImage src={avatar} alt={name} />
<AvatarFallback>{name[0]}</AvatarFallback>
</Avatar>
<div className="flex-1">
<h3 className="text-xl font-semibold mb-2">
About {name}
</h3>
<p className="text-muted-foreground mb-4">
{bio}
</p>
<div className="flex gap-3">
{social?.twitter && (
<a href={social.twitter} className="text-muted-foreground hover:text-foreground">
<Twitter className="h-5 w-5" />
</a>
)}
{social?.linkedin && (
<a href={social.linkedin} className="text-muted-foreground hover:text-foreground">
<LinkedIn className="h-5 w-5" />
</a>
)}
{social?.github && (
<a href={social.github} className="text-muted-foreground hover:text-foreground">
<Github className="h-5 w-5" />
</a>
)}
</div>
</div>
</div>
</div>
)
}Share Buttons
Social sharing buttons:
const ShareButtons = ({ url, title }) => {
const shareUrls = {
twitter: `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`,
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
}
const copyToClipboard = () => {
navigator.clipboard.writeText(url)
toast.success('Link copied to clipboard!')
}
return (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Share:</span>
<Button
variant="ghost"
size="sm"
onClick={() => window.open(shareUrls.twitter, '_blank')}
>
<Twitter className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => window.open(shareUrls.linkedin, '_blank')}
>
<LinkedIn className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => window.open(shareUrls.facebook, '_blank')}
>
<Facebook className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={copyToClipboard}
>
<Link className="h-4 w-4" />
</Button>
</div>
)
}Related Posts
Show related content:
const RelatedPosts = ({ posts }) => {
if (!posts?.length) return null
return (
<section className="border-t pt-12">
<h2 className="text-2xl font-bold mb-6">Related Articles</h2>
<div className="grid gap-6 md:grid-cols-3">
{posts.map(post => (
<article key={post.id} className="group">
{post.coverImage && (
<Link href={`/blog/${post.slug}`}>
<img
src={post.coverImage}
alt={post.title}
className="aspect-video rounded-lg object-cover mb-3 transition group-hover:scale-105"
/>
</Link>
)}
<h3 className="font-semibold mb-2">
<Link
href={`/blog/${post.slug}`}
className="hover:underline"
>
{post.title}
</Link>
</h3>
<p className="text-sm text-muted-foreground line-clamp-2">
{post.excerpt}
</p>
<time className="text-xs text-muted-foreground mt-2 block">
{formatDate(post.publishedAt)}
</time>
</article>
))}
</div>
</section>
)
}Reading Time Calculation
Calculate estimated reading time:
const calculateReadingTime = (content: string) => {
const wordsPerMinute = 200
const words = content.trim().split(/\s+/).length
const time = Math.ceil(words / wordsPerMinute)
return time
}
// Usage
const readTime = calculateReadingTime(post.content)Table of Contents
Optional sticky TOC:
const TableOfContents = ({ headings }) => {
const [activeId, setActiveId] = useState('')
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id)
}
})
},
{ rootMargin: '-100px 0px -66%' }
)
headings.forEach((heading) => {
const el = document.getElementById(heading.id)
if (el) observer.observe(el)
})
return () => observer.disconnect()
}, [headings])
return (
<aside className="sticky top-24 hidden lg:block">
<div className="text-sm">
<h3 className="font-semibold mb-4">On This Page</h3>
<ul className="space-y-2">
{headings.map((heading) => (
<li
key={heading.id}
className={cn(
heading.level === 3 && "ml-4",
activeId === heading.id && "text-primary"
)}
>
<a
href={`#${heading.id}`}
className="hover:text-foreground"
>
{heading.text}
</a>
</li>
))}
</ul>
</div>
</aside>
)
}Accessibility
The reference implementation includes:
- Semantic HTML - Uses
<article>,<time>,<header> - Heading Hierarchy - Proper h1-h6 structure
- ARIA Labels - Descriptive labels for buttons
- Alt Text - All images have descriptive alt text
- Focus Management - Keyboard navigation support
- Screen Reader Support - Proper announcements
- Color Contrast - WCAG AA compliant
<article
role="article"
aria-labelledby="post-title"
className="mx-auto max-w-4xl"
>
<header>
<h1 id="post-title">{post.title}</h1>
<div className="flex items-center gap-4">
<span className="sr-only">Published on</span>
<time dateTime={post.publishedAt}>
{formatDate(post.publishedAt)}
</time>
<span className="sr-only">by</span>
<span>{post.author.name}</span>
<span className="sr-only">Reading time</span>
<span>{post.readTime} minutes</span>
</div>
</header>
<div className="prose">{children}</div>
</article>SEO Optimization
Include proper meta tags:
<Head>
<title>{post.title} | Blog</title>
<meta name="description" content={post.excerpt} />
<meta name="author" content={post.author.name} />
<meta name="publish-date" content={post.publishedAt} />
{/* Open Graph */}
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<meta property="og:image" content={post.coverImage} />
<meta property="og:type" content="article" />
<meta property="article:published_time" content={post.publishedAt} />
<meta property="article:author" content={post.author.name} />
{/* Twitter Card */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={post.title} />
<meta name="twitter:description" content={post.excerpt} />
<meta name="twitter:image" content={post.coverImage} />
</Head>TypeScript
Full type definitions:
import type { ReactNode } from 'react'
import type { z } from 'zod'
import type { Component } from 'mdxui'
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>Related Components
- BlogPage Component - Blog listing page
- Page Component - Generic page layout
- Avatar Component - Author avatar
- Badge Component - Category and tag badges