mdxui

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

  1. Component receives slug prop
  2. Queries Payload posts collection
  3. Renders post metadata and content
  4. Fetches related posts based on tags/category
  5. 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>
  )
}

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>

On this page