# Post Detail

> Full post view with cover image, author info, content, tags and related posts section.

A shadcn/ui block from the [@manifest](https://ui.manifest.build) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/manifest/post-detail).

- Page, with a live preview: https://shadcn.exchange/r/manifest/post-detail
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/manifest/post-detail-light-e0018583a4362b2b.png
- Registry item: https://ui.manifest.build/r/post-detail.json
- Categories: blogging
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.manifest.build/r/post-detail.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `tooltip`, `https://ui.manifest.build/r/manifest-types.json`

## Files

### `registry/blogging/post-detail.tsx`

````tsx
'use client';

import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Calendar, Clock, ExternalLink, Maximize2 } from 'lucide-react';
import { useMemo } from 'react';
import type { Post } from './types';
import { demoPostDetailData } from './demo/blogging';

// DOM-based allowlist HTML sanitizer for post content
const ALLOWED_TAGS = new Set([
  'p', 'br', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li',
  'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre',
  'span', 'div', 'img', 'figure', 'figcaption', 'hr',
]);
const ALLOWED_ATTRS: Record<string, Set<string>> = {
  a: new Set(['href', 'target', 'rel', 'title']),
  img: new Set(['src', 'alt', 'width', 'height']),
  '*': new Set(['class', 'id']),
};
const DANGEROUS_URL = /^\s*(javascript|data):/i;

function sanitizeNode(node: Node, doc: Document): void {
  const children = Array.from(node.childNodes);
  for (const child of children) {
    if (child.nodeType === 3 /* TEXT */) continue;
    if (child.nodeType !== 1 /* ELEMENT */) {
      child.remove();
      continue;
    }
    const el = child as Element;
    const tag = el.tagName.toLowerCase();
    if (!ALLOWED_TAGS.has(tag)) {
      // Unwrap: keep text content, discard the tag
      while (el.firstChild) node.insertBefore(el.firstChild, el);
      el.remove();
      continue;
    }
    // Strip disallowed attributes
    const tagAllowed = ALLOWED_ATTRS[tag];
    const globalAllowed = ALLOWED_ATTRS['*'];
    for (const attr of Array.from(el.attributes)) {
      const name = attr.name.toLowerCase();
      if (!tagAllowed?.has(name) && !globalAllowed?.has(name)) {
        el.removeAttribute(attr.name);
      }
    }
    // Block dangerous URL schemes on href/src
    for (const urlAttr of ['href', 'src']) {
      const val = el.getAttribute(urlAttr);
      if (val && DANGEROUS_URL.test(val)) el.removeAttribute(urlAttr);
    }
    sanitizeNode(el, doc);
  }
}

function sanitizeHtml(html: string): string {
  if (typeof document === 'undefined') return html;
  const doc = new DOMParser().parseFromString(html, 'text/html');
  sanitizeNode(doc.body, doc);
  return doc.body.innerHTML;
}

function TagList({
  tags,
  maxVisible = 2,
  size = 'default',
}: {
  tags: string[];
  maxVisible?: number;
  size?: 'small' | 'default';
}) {
  const visibleTags = tags.slice(0, maxVisible);
  const remainingTags = tags.slice(maxVisible);
  const hasMore = remainingTags.length > 0;

  const tagClass =
    size === 'small'
      ? 'rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium'
      : 'rounded-full bg-muted px-3 py-1 text-xs font-medium';

  return (
    <>
      {visibleTags.map((tag) => (
        <span key={tag} className={tagClass}>
          {tag}
        </span>
      ))}
      {hasMore && (
        <TooltipProvider delayDuration={0}>
          <Tooltip>
            <TooltipTrigger asChild>
              <span className={`${tagClass} cursor-default`}>+{remainingTags.length}</span>
            </TooltipTrigger>
            <TooltipContent>
              <p>{remainingTags.join(', ')}</p>
            </TooltipContent>
          </Tooltip>
        </TooltipProvider>
      )}
    </>
  );
}

/**
 * ═══════════════════════════════════════════════════════════════════════════
 * PostDetailProps
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * Props for the PostDetail component, a full post detail view with Medium-style typography.
 */
export interface PostDetailProps {
  data?: {
    /** The main blog post to display. */
    post?: Post;
    /** HTML content of the post body. */
    content?: string;
    /** Related posts to show at the bottom of the article. */
    relatedPosts?: Post[];
  };
  actions?: {
    /** Called when the back button is clicked. */
    onBack?: () => void;
    /** Called when the read more button is clicked (inline mode). */
    onReadMore?: () => void;
    /** Called when a related post is clicked. */
    onReadRelated?: (post: Post) => void;
  };
  appearance?: {
    /**
     * Whether to show the cover image.
     * @default true
     */
    showCover?: boolean;
    /**
     * Whether to show author information.
     * @default true
     */
    showAuthor?: boolean;
    /**
     * Display mode for the component.
     * - inline: Compact card view with truncated content
     * - pip: Picture-in-picture view with truncated content
     * - fullscreen: Full article view with complete content
     * @default "fullscreen"
     */
    displayMode?: 'inline' | 'pip' | 'fullscreen';
  };
}

/**
 * A full post detail component with Medium-style typography.
 * Supports inline preview and fullscreen reading modes.
 *
 * Features:
 * - Medium-style typography and spacing
 * - Cover image display
 * - Author info with avatar
 * - Tag list with overflow tooltip
 * - Related posts section
 * - Inline (truncated) and fullscreen modes
 * - MCP Apps display mode integration
 *
 * @component
 * @example
 * ```tsx
 * <PostDetail
 *   data={{
 *     post: {
 *       id: "1",
 *       title: "Getting Started",
 *       excerpt: "Learn the basics...",
 *       coverImage: "https://example.com/cover.jpg",
 *       author: { name: "Sarah Chen", avatar: "https://example.com/avatar.jpg" },
 *       publishedAt: "2024-01-15",
 *       readTime: "5 min read",
 *       tags: ["Tutorial", "Components"],
 *       category: "Tutorial"
 *     },
 *     content: "<p>Full post content here...</p>",
 *     relatedPosts: [...]
 *   }}
 *   actions={{
 *     onReadMore: () => console.log("Expand to fullscreen"),
 *     onReadRelated: (post) => console.log("Read related:", post.title)
 *   }}
 *   appearance={{
 *     showCover: true,
 *     showAuthor: true,
 *     displayMode: "fullscreen"
 *   }}
 * />
 * ```
 */
export function PostDetail({ data, actions, appearance }: PostDetailProps) {
  const resolved: NonNullable<PostDetailProps['data']> = data ?? demoPostDetailData;
  const post = resolved.post;
  const rawContent = resolved.content;
  const content = useMemo(() => rawContent ? sanitizeHtml(rawContent) : undefined, [rawContent]);
  const relatedPosts = resolved.relatedPosts ?? [];
  const onReadMore = actions?.onReadMore;
  const showCover = appearance?.showCover ?? true;
  const showAuthor = appearance?.showAuthor ?? true;

  const displayMode = appearance?.displayMode ?? 'inline';

  const handleReadMore = () => {
    onReadMore?.();
  };

  const formatDate = (dateStr: string) => {
    return new Date(dateStr).toLocaleDateString('en-US', {
      month: 'long',
      day: 'numeric',
      year: 'numeric',
    });
  };

  // Inline mode - card view with truncated content
  if (displayMode === 'inline') {
    return (
      <div className="flex flex-col sm:flex-row gap-4 rounded-lg border bg-card p-3">
        {showCover && post?.coverImage && (
          <div className="aspect-video sm:aspect-square sm:h-24 sm:w-24 shrink-0 overflow-hidden rounded-md">
            <img
              src={post.coverImage}
              alt={post?.title || ''}
              className="h-full w-full object-cover"
            />
          </div>
        )}

        <div className="flex flex-1 flex-col justify-between min-w-0">
          <div>
            <div className="flex items-start justify-between gap-2">
              <div className="flex-1 min-w-0">
                {post?.category && (
                  <p className="mb-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
                    {post.category}
                  </p>
                )}

                {post?.title && (
                  <h1 className="line-clamp-2 text-sm font-bold leading-tight">{post.title}</h1>
                )}
              </div>
              <button
                onClick={handleReadMore}
                className="shrink-0 p-1 rounded-md hover:bg-muted transition-colors text-muted-foreground hover:text-foreground cursor-pointer"
                aria-label="Expand to fullscreen"
              >
                <Maximize2 className="h-4 w-4" />
              </button>
            </div>

            {post?.excerpt && (
              <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{post.excerpt}</p>
            )}

            {post?.tags && post.tags.length > 0 && (
              <div className="mt-1.5 flex flex-wrap gap-1">
                <TagList tags={post.tags} maxVisible={2} size="small" />
              </div>
            )}
          </div>

          <div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
            <div className="flex items-center gap-2 text-xs text-muted-foreground">
              {showAuthor && post?.author?.avatar && (
                <img
                  src={post.author.avatar}
                  alt={post?.author?.name || ''}
                  className="h-4 w-4 rounded-full"
                />
              )}
              {showAuthor && post?.author?.name && <span>{post.author.name}</span>}
              {post?.publishedAt && (
                <span className="flex items-center gap-1">
                  <Calendar className="h-3 w-3" />
                  {formatDate(post.publishedAt)}
                </span>
              )}
              {post?.readTime && (
                <span className="flex items-center gap-1">
                  <Clock className="h-3 w-3" />
                  {post.readTime}
                </span>
              )}
            </div>

            <Button size="sm" onClick={handleReadMore}>
              Read
            </Button>
          </div>
        </div>
      </div>
    );
  }

  // PiP mode - horizontal layout with image on left, similar to post-card horizontal
  if (displayMode === 'pip') {
    return (
      <div className="flex flex-col sm:flex-row gap-4 rounded-lg border bg-card p-3">
        {showCover && post?.coverImage && (
          <div className="aspect-video sm:aspect-square sm:h-24 sm:w-24 shrink-0 overflow-hidden rounded-md">
            <img
              src={post.coverImage}
              alt={post?.title || ''}
              className="h-full w-full object-cover"
            />
          </div>
        )}

        <div className="flex flex-1 flex-col justify-between min-w-0">
          <div>
            {post?.category && (
              <p className="mb-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
                {post.category}
              </p>
            )}

            {post?.title && (
              <h1 className="line-clamp-2 text-sm font-bold leading-tight">{post.title}</h1>
            )}

            {post?.excerpt && (
              <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{post.excerpt}</p>
            )}

            {post?.tags && post.tags.length > 0 && (
              <div className="mt-1.5 flex flex-wrap gap-1">
                <TagList tags={post.tags} maxVisible={2} size="small" />
              </div>
            )}
          </div>

          <div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
            <div className="flex items-center gap-2 text-xs text-muted-foreground">
              {showAuthor && post?.author?.avatar && (
                <img
                  src={post.author.avatar}
                  alt={post?.author?.name || ''}
                  className="h-4 w-4 rounded-full"
                />
              )}
              {showAuthor && post?.author?.name && <span>{post.author.name}</span>}
              {post?.publishedAt && (
                <span className="flex items-center gap-1">
                  <Calendar className="h-3 w-3" />
                  {formatDate(post.publishedAt)}
                </span>
              )}
              {post?.readTime && (
                <span className="flex items-center gap-1">
                  <Clock className="h-3 w-3" />
                  {post.readTime}
                </span>
              )}
            </div>
            <Button size="sm" onClick={handleReadMore}>
              Read
            </Button>
          </div>
        </div>
      </div>
    );
  }

  // Fullscreen mode
  return (
    <div className="min-h-screen fs-mode bg-background">
      <article className="mx-auto w-full max-w-[680px] px-6 py-10">
        {showCover && post?.coverImage && (
          <div className="aspect-video w-full overflow-hidden rounded-lg mb-8">
            <img
              src={post.coverImage}
              alt={post?.title || ''}
              className="h-full w-full object-cover"
            />
          </div>
        )}
        {post?.category && (
          <p className="mb-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
            {post.category}
          </p>
        )}

        {post?.title && (
          <h1 className="text-[32px] font-bold leading-[1.25] tracking-tight md:text-[42px]">
            {post.title}
          </h1>
        )}

        {post?.tags && post.tags.length > 0 && (
          <div className="mt-4 flex flex-wrap items-center gap-2">
            <TagList tags={post.tags} maxVisible={2} size="default" />
          </div>
        )}

        {showAuthor && post?.author && (
          <div className="mt-8 flex items-center gap-4 border-b pb-8">
            {post.author.avatar && (
              <img
                src={post.author.avatar}
                alt={post.author.name || ''}
                className="h-12 w-12 rounded-full"
              />
            )}
            <div>
              {post.author.name && <p className="font-medium">{post.author.name}</p>}
              {(post?.publishedAt || post?.readTime) && (
                <div className="flex items-center gap-3 text-sm text-muted-foreground">
                  {post?.publishedAt && (
                    <span className="flex items-center gap-1">
                      <Calendar className="h-3.5 w-3.5" />
                      {formatDate(post.publishedAt)}
                    </span>
                  )}
                  {post?.readTime && (
                    <span className="flex items-center gap-1">
                      <Clock className="h-3.5 w-3.5" />
                      {post.readTime}
                    </span>
                  )}
                </div>
              )}
            </div>
          </div>
        )}

        {/* Medium-style content */}
        <div className="mt-10">
          {post?.excerpt && (
            <p className="text-[21px] leading-[1.8] text-muted-foreground mb-8">{post.excerpt}</p>
          )}
          {content && (
            <div
              className="
                text-[21px] leading-[1.8] tracking-[-0.003em]
                [&>p]:mb-8
                [&>h2]:text-[26px] [&>h2]:font-bold [&>h2]:mt-12 [&>h2]:mb-4 [&>h2]:leading-[1.3]
                [&>h3]:text-[22px] [&>h3]:font-bold [&>h3]:mt-10 [&>h3]:mb-3 [&>h3]:leading-[1.3]
                [&>ul]:mb-8 [&>ul]:pl-6 [&>ul>li]:mb-2
                [&>ol]:mb-8 [&>ol]:pl-6 [&>ol>li]:mb-2
                [&>blockquote]:border-l-4 [&>blockquote]:border-foreground [&>blockquote]:pl-6 [&>blockquote]:my-8 [&>blockquote]:italic
              "
              dangerouslySetInnerHTML={{ __html: content }}
            />
          )}
        </div>

        {relatedPosts && relatedPosts.length > 0 && (
          <div className="mt-16 border-t pt-10">
            <h3 className="mb-6 text-lg font-semibold">Related Posts</h3>
            <div className="space-y-4">
              {relatedPosts.map((related) => (
                <a
                  key={related.title || related.url}
                  href={related.url || '#'}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="flex w-full items-center gap-4 rounded-lg p-3 text-left transition-colors hover:bg-muted cursor-pointer"
                >
                  {related.coverImage && (
                    <div className="h-16 w-16 shrink-0 overflow-hidden rounded-lg">
                      <img
                        src={related.coverImage}
                        alt={related.title || ''}
                        className="h-full w-full object-cover"
                      />
                    </div>
                  )}
                  <div className="min-w-0 flex-1">
                    {related.title && <p className="font-medium">{related.title}</p>}
                    {related.excerpt && (
                      <p className="mt-1 line-clamp-1 text-sm text-muted-foreground">
                        {related.excerpt}
                      </p>
                    )}
                    {related.readTime && (
                      <p className="mt-1 text-xs text-muted-foreground">{related.readTime}</p>
                    )}
                  </div>
                  <ExternalLink className="h-4 w-4 shrink-0 text-muted-foreground" />
                </a>
              ))}
            </div>
          </div>
        )}
      </article>
    </div>
  );
}
````

### `registry/blogging/post-card.tsx`

````tsx
'use client'

import { Button } from '@/components/ui/button'

// Import types from shared types file to avoid circular dependencies
import type { Post } from './types'
// Re-export for backward compatibility
export type { Post } from './types'

import { demoPost } from './demo/blogging'


/**
 * ═══════════════════════════════════════════════════════════════════════════
 * PostCardProps
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * Props for the PostCard component, a blog post card with multiple layout variants.
 */
export interface PostCardProps {
  data?: {
    /** The blog post to display. */
    post?: Post
  }
  actions?: {
    /** Called when the read more button is clicked. */
    onReadMore?: (post: Post) => void
  }
  appearance?: {
    /**
     * Card layout variant.
     * @default "default"
     */
    variant?: 'default' | 'compact' | 'horizontal' | 'covered'
    /**
     * Whether to show the cover image.
     * @default true
     */
    showImage?: boolean
    /**
     * Whether to show author information.
     * @default true
     */
    showAuthor?: boolean
    /**
     * Whether to show the category label.
     * @default true
     */
    showCategory?: boolean
  }
}

/**
 * A blog post card component with multiple layout variants.
 * Supports default, compact, horizontal, and covered (overlay) styles.
 *
 * Features:
 * - Four layout variants (default, compact, horizontal, covered)
 * - Cover image with hover zoom effect
 * - Author avatar and info display
 * - Category and tags display
 * - Read more action button
 * - Responsive design
 *
 * @component
 * @example
 * ```tsx
 * <PostCard
 *   data={{
 *     post: {
 *       title: "Getting Started Guide",
 *       excerpt: "Learn the basics of our component library.",
 *       coverImage: "https://example.com/image.jpg",
 *       author: { name: "Sarah Chen", avatar: "https://example.com/avatar.jpg" },
 *       publishedAt: "2024-01-15",
 *       readTime: "5 min read",
 *       tags: ["Tutorial", "Components"],
 *       category: "Tutorial"
 *     }
 *   }}
 *   actions={{
 *     onReadMore: (post) => console.log("Read more:", post.title)
 *   }}
 *   appearance={{
 *     variant: "default",
 *     showImage: true,
 *     showAuthor: true,
 *     showCategory: true
 *   }}
 * />
 * ```
 */
export function PostCard({ data, actions, appearance }: PostCardProps) {
  const resolved: NonNullable<PostCardProps['data']> = data ?? { post: demoPost }
  const post = resolved.post
  if (!post) {
    return null
  }
  const onReadMore = actions?.onReadMore
  const variant = appearance?.variant ?? 'default'
  const showImage = appearance?.showImage ?? true
  const showAuthor = appearance?.showAuthor ?? true
  const showCategory = appearance?.showCategory ?? true

  // Handle "Read more" click - only call callback if provided, otherwise do nothing
  // This lets users decide to use an external link or open fullscreen mode
  const handleReadMore = () => {
    if (onReadMore) {
      onReadMore(post)
    }
  }

  const formatDate = (dateStr: string) => {
    return new Date(dateStr).toLocaleDateString('en-US', {
      month: 'short',
      day: 'numeric',
      year: 'numeric'
    })
  }

  if (variant === 'covered') {
    return (
      <div className="relative overflow-hidden rounded-lg border">
        <div className="min-h-[280px] sm:aspect-[16/9] sm:min-h-0 w-full">
          {post.coverImage ? (
            <img
              src={post.coverImage}
              alt={post.title || ''}
              className="absolute inset-0 h-full w-full object-cover"
            />
          ) : (
            <div className="absolute inset-0 h-full w-full bg-muted" />
          )}
        </div>
        {/* Minimal overlay - solid color instead of gradient per ChatGPT guidelines */}
        <div className="absolute inset-0 bg-black/60" />
        <div className="absolute inset-0 flex flex-col justify-end p-4 text-white">
          <div>
            {showCategory && post.category && (
              <p className="text-[10px] font-medium uppercase tracking-wide text-white/70">
                {post.category}
              </p>
            )}
            {post.title && (
              <h2 className="mt-1 text-lg font-semibold leading-tight">
                {post.title}
              </h2>
            )}
            {post.excerpt && (
              <p className="mt-1 line-clamp-2 text-sm text-white/80">
                {post.excerpt}
              </p>
            )}
            {post.tags && post.tags.length > 0 && (
              <div className="mt-2 flex flex-wrap gap-1">
                {post.tags.slice(0, 2).map((tag) => (
                  <span
                    key={tag}
                    className="rounded-md bg-white/20 px-2 py-0.5 text-xs"
                  >
                    {tag}
                  </span>
                ))}
              </div>
            )}
            <div className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              {showAuthor && (
                <div className="flex items-center gap-2">
                  {post.author?.avatar && (
                    <img
                      src={post.author.avatar}
                      alt={post.author?.name || ''}
                      className="h-6 w-6 rounded-full ring-1 ring-white/30"
                    />
                  )}
                  <div className="text-xs">
                    {post.author?.name && (
                      <p className="font-medium">{post.author.name}</p>
                    )}
                    {post.publishedAt && (
                      <p className="text-white/60">
                        {formatDate(post.publishedAt)}
                      </p>
                    )}
                  </div>
                </div>
              )}
              <Button
                size="sm"
                variant="secondary"
                className="w-full sm:w-auto"
                onClick={handleReadMore}
              >
                Read article
              </Button>
            </div>
          </div>
        </div>
      </div>
    )
  }

  if (variant === 'horizontal') {
    return (
      <div className="flex flex-col sm:flex-row gap-4 rounded-lg border bg-card p-3">
        {showImage && post.coverImage && (
          <div className="aspect-video sm:aspect-square sm:h-24 sm:w-24 shrink-0 overflow-hidden rounded-md">
            <img
              src={post.coverImage}
              alt={post.title || ''}
              className="h-full w-full object-cover"
            />
          </div>
        )}
        <div className="flex flex-1 flex-col justify-between">
          <div>
            {showCategory && post.category && (
              <p className="mb-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
                {post.category}
              </p>
            )}
            {post.title && (
              <h3 className="line-clamp-2 text-sm font-medium leading-tight">
                {post.title}
              </h3>
            )}
            {post.excerpt && (
              <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
                {post.excerpt}
              </p>
            )}
            {post.tags && post.tags.length > 0 && (
              <div className="mt-1.5 flex flex-wrap gap-1">
                {post.tags.slice(0, 2).map((tag) => (
                  <span
                    key={tag}
                    className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
                  >
                    {tag}
                  </span>
                ))}
              </div>
            )}
          </div>
          <div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
            <div className="flex items-center gap-2 text-xs text-muted-foreground">
              {showAuthor && post.author?.avatar && (
                <img
                  src={post.author.avatar}
                  alt={post.author?.name || ''}
                  className="h-4 w-4 rounded-full"
                />
              )}
              {post.publishedAt && (
                <span>{formatDate(post.publishedAt)}</span>
              )}
              {post.readTime && (
                <>
                  <span>·</span>
                  <span>{post.readTime}</span>
                </>
              )}
            </div>
            <Button
              size="sm"
              className="w-full sm:w-auto"
              onClick={handleReadMore}
            >
              Read
            </Button>
          </div>
        </div>
      </div>
    )
  }

  if (variant === 'compact') {
    return (
      <div className="flex h-full flex-col justify-between rounded-lg border bg-card p-3">
        <div>
          {showCategory && post.category && (
            <p className="mb-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
              {post.category}
            </p>
          )}
          {post.title && (
            <h3 className="line-clamp-2 text-sm font-medium">{post.title}</h3>
          )}
          {post.excerpt && (
            <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
              {post.excerpt}
            </p>
          )}
          {post.tags && post.tags.length > 0 && (
            <div className="mt-1.5 flex flex-wrap gap-1">
              {post.tags.slice(0, 2).map((tag) => (
                <span
                  key={tag}
                  className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
                >
                  {tag}
                </span>
              ))}
            </div>
          )}
        </div>
        <div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
          <div className="flex items-center gap-2">
            {showAuthor && post.author?.avatar && (
              <img
                src={post.author.avatar}
                alt={post.author?.name || ''}
                className="h-5 w-5 rounded-full"
              />
            )}
            {post.publishedAt && (
              <span className="text-xs text-muted-foreground">
                {formatDate(post.publishedAt)}
              </span>
            )}
          </div>
          <Button size="sm" onClick={handleReadMore}>
            Read more
          </Button>
        </div>
      </div>
    )
  }

  // Default variant
  return (
    <div className="flex h-full flex-col overflow-hidden rounded-lg border bg-card">
      {showImage && post.coverImage && (
        <div className="aspect-video overflow-hidden">
          <img
            src={post.coverImage}
            alt={post.title || ''}
            className="h-full w-full object-cover transition-transform hover:scale-105"
          />
        </div>
      )}
      <div className="flex flex-1 flex-col justify-between p-4">
        <div>
          {showCategory && post.category && (
            <p className="mb-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
              {post.category}
            </p>
          )}
          {post.title && (
            <h3 className="line-clamp-2 font-medium">{post.title}</h3>
          )}
          {post.excerpt && (
            <p className="mt-2 line-clamp-2 text-sm text-muted-foreground">
              {post.excerpt}
            </p>
          )}
          {post.tags && post.tags.length > 0 && (
            <div className="mt-2 flex flex-wrap gap-1">
              {post.tags.slice(0, 2).map((tag) => (
                <span
                  key={tag}
                  className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
                >
                  {tag}
                </span>
              ))}
            </div>
          )}
        </div>
        <div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
          {showAuthor && (
            <div className="flex items-center gap-2">
              {post.author?.avatar && (
                <img
                  src={post.author.avatar}
                  alt={post.author?.name || ''}
                  className="h-6 w-6 rounded-full"
                />
              )}
              <div className="text-xs">
                {post.author?.name && (
                  <p className="font-medium">{post.author.name}</p>
                )}
                {post.publishedAt && (
                  <p className="text-muted-foreground">
                    {formatDate(post.publishedAt)}
                  </p>
                )}
              </div>
            </div>
          )}
          <Button size="sm" onClick={handleReadMore}>
            Read
          </Button>
        </div>
      </div>
    </div>
  )
}
````

### `registry/blogging/demo/blogging.ts`

```ts
// Demo data for Blogging category components
// This file contains sample data used for component previews and documentation

import type { Post } from '../types';

// Single post for PostCard default
export const demoPost: Post = {
  title: 'Getting Started with Agentic UI Components',
  excerpt:
    'Learn how to build conversational interfaces with our comprehensive component library designed for AI-powered applications.',
  coverImage: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=800',
  author: {
    name: 'Sarah Chen',
    avatar: 'https://i.pravatar.cc/150?u=sarah',
  },
  publishedAt: '2024-01-15',
  readTime: '5 min read',
  tags: ['Tutorial', 'Components'],
  category: 'Tutorial',
};

// Demo content for PostDetail (HTML content for full article view)
export const demoPostContent = `
  <p>Building modern AI-powered applications requires a new approach to UI design. Traditional web components don't always translate well to conversational interfaces, where context and flow are paramount.</p>

  <p>Our Agentic UI component library provides a collection of purpose-built components that work seamlessly within chat interfaces. From payment flows to product displays, each component is designed with the unique constraints of conversational UIs in mind.</p>

  <h2>Key Features</h2>
  <p>Each component supports three display modes: inline (within the chat flow), fullscreen (for complex interactions), and picture-in-picture (persistent visibility). This flexibility allows you to create rich, interactive experiences without breaking the conversational flow.</p>

  <p>Components are designed mobile-first and touch-friendly, ensuring a great experience across all devices. They automatically adapt to light and dark themes, and integrate seamlessly with MCP tools for backend communication.</p>
`;

// Related posts for PostDetail
export const demoRelatedPosts: Post[] = [
  {
    title: 'Designing for Conversational Interfaces',
    excerpt:
      'Best practices for creating intuitive UI components that work within chat environments.',
    coverImage: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800',
    author: { name: 'Alex Rivera', avatar: 'https://i.pravatar.cc/150?u=alex' },
    publishedAt: '2024-01-12',
    readTime: '8 min read',
    tags: ['Design', 'UX'],
    category: 'Design',
    url: 'https://example.com/posts/designing-conversational-interfaces',
  },
  {
    title: 'MCP Integration Patterns',
    excerpt: 'How to leverage Model Context Protocol for seamless backend communication.',
    coverImage: 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800',
    author: { name: 'Jordan Kim', avatar: 'https://i.pravatar.cc/150?u=jordan' },
    publishedAt: '2024-01-10',
    readTime: '12 min read',
    tags: ['MCP', 'Backend'],
    category: 'Development',
    url: 'https://example.com/posts/mcp-integration-patterns',
  },
];

// Full PostDetail demo data (combines post, content, and relatedPosts)
export const demoPostDetailData = {
  post: {
    ...demoPost,
    tags: ['Tutorial', 'Components', 'AI', 'React', 'TypeScript'],
  },
  content: demoPostContent,
  relatedPosts: demoRelatedPosts,
};

// 15 posts for PostList default
export const demoPosts: Post[] = [
  {
    title: 'Getting Started with Agentic UI Components',
    excerpt:
      'Learn how to build conversational interfaces with our comprehensive component library designed for AI-powered applications.',
    coverImage: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=800',
    author: {
      name: 'Sarah Chen',
      avatar: 'https://i.pravatar.cc/150?u=sarah',
    },
    publishedAt: '2024-01-15',
    readTime: '5 min read',
    tags: ['Tutorial', 'Components', 'AI'],
    category: 'Tutorial',
  },
  {
    title: 'Designing for Conversational Interfaces with Manifest UI',
    excerpt:
      'Best practices for creating intuitive UI components that work within chat environments.',
    coverImage: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800',
    author: {
      name: 'Alex Rivera',
      avatar: 'https://i.pravatar.cc/150?u=alex',
    },
    publishedAt: '2024-01-12',
    readTime: '8 min read',
    tags: ['Design', 'UX'],
    category: 'Design',
  },
  {
    title: 'MCP Integration Patterns',
    excerpt:
      'How to leverage Model Context Protocol for seamless backend communication in your agentic applications.',
    coverImage: 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800',
    author: {
      name: 'Jordan Kim',
      avatar: 'https://i.pravatar.cc/150?u=jordan',
    },
    publishedAt: '2024-01-10',
    readTime: '12 min read',
    tags: ['MCP', 'Backend', 'Integration'],
    category: 'Development',
  },
  {
    title: 'Building Payment Flows in Chat',
    excerpt:
      'A complete guide to implementing secure, user-friendly payment experiences within conversational interfaces.',
    coverImage: 'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=800',
    author: {
      name: 'Morgan Lee',
      avatar: 'https://i.pravatar.cc/150?u=morgan',
    },
    publishedAt: '2024-01-08',
    readTime: '10 min read',
    tags: ['Payments', 'Security'],
    category: 'Tutorial',
  },
  {
    title: 'Real-time Collaboration in AI Apps',
    excerpt:
      'Implementing WebSocket connections and real-time updates for collaborative agentic experiences.',
    coverImage: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800',
    author: {
      name: 'Casey Taylor',
      avatar: 'https://i.pravatar.cc/150?u=casey',
    },
    publishedAt: '2024-01-06',
    readTime: '15 min read',
    tags: ['WebSocket', 'Real-time', 'Collaboration'],
    category: 'Development',
  },
  {
    title: 'Accessibility in Chat Interfaces',
    excerpt:
      'Making your conversational UI accessible to all users with screen readers and keyboard navigation.',
    coverImage: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=800',
    author: {
      name: 'Jamie Park',
      avatar: 'https://i.pravatar.cc/150?u=jamie',
    },
    publishedAt: '2024-01-04',
    readTime: '9 min read',
    tags: ['Accessibility', 'A11y', 'UX'],
    category: 'Design',
  },
  {
    title: 'State Management for Complex Workflows',
    excerpt:
      'Managing complex multi-step workflows in agentic applications using modern state patterns.',
    coverImage: 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=800',
    author: {
      name: 'Drew Martinez',
      avatar: 'https://i.pravatar.cc/150?u=drew',
    },
    publishedAt: '2024-01-02',
    readTime: '11 min read',
    tags: ['State', 'Workflow', 'Architecture'],
    category: 'Development',
  },
  {
    title: 'Testing Conversational Components',
    excerpt: 'Strategies for unit testing and integration testing of chat-based UI components.',
    coverImage: 'https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=800',
    author: {
      name: 'Riley Johnson',
      avatar: 'https://i.pravatar.cc/150?u=riley',
    },
    publishedAt: '2023-12-30',
    readTime: '8 min read',
    tags: ['Testing', 'Quality', 'CI/CD'],
    category: 'Development',
  },
  {
    title: 'Theming and Dark Mode Support',
    excerpt: 'Implementing flexible theming systems with dark mode for agentic UI components.',
    coverImage: 'https://images.unsplash.com/photo-1475924156734-496f6cac6ec1?w=800',
    author: {
      name: 'Avery Williams',
      avatar: 'https://i.pravatar.cc/150?u=avery',
    },
    publishedAt: '2023-12-28',
    readTime: '7 min read',
    tags: ['Theming', 'Dark Mode', 'CSS'],
    category: 'Design',
  },
  {
    title: 'Performance Optimization Techniques',
    excerpt: 'Optimizing render performance and reducing bundle size in chat applications.',
    coverImage: 'https://images.unsplash.com/photo-1518173946687-a2e8a36af77a?w=800',
    author: {
      name: 'Quinn Anderson',
      avatar: 'https://i.pravatar.cc/150?u=quinn',
    },
    publishedAt: '2023-12-25',
    readTime: '13 min read',
    tags: ['Performance', 'Optimization', 'React'],
    category: 'Development',
  },
  {
    title: 'Error Handling and Recovery',
    excerpt:
      'Graceful error handling patterns and user-friendly recovery flows in conversational UIs.',
    coverImage: 'https://images.unsplash.com/photo-1509316975850-ff9c5deb0cd9?w=800',
    author: {
      name: 'Sage Thompson',
      avatar: 'https://i.pravatar.cc/150?u=sage',
    },
    publishedAt: '2023-12-22',
    readTime: '10 min read',
    tags: ['Error Handling', 'UX', 'Resilience'],
    category: 'Development',
  },
  {
    title: 'Internationalization Best Practices',
    excerpt: 'Making your agentic UI components work across languages and locales.',
    coverImage: 'https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=800',
    author: {
      name: 'Blake Garcia',
      avatar: 'https://i.pravatar.cc/150?u=blake',
    },
    publishedAt: '2023-12-20',
    readTime: '9 min read',
    tags: ['i18n', 'Localization', 'Global'],
    category: 'Design',
  },
  {
    title: 'Mobile-First Chat Design',
    excerpt: 'Designing conversational interfaces that work beautifully on mobile devices.',
    coverImage: 'https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=800',
    author: {
      name: 'Charlie Brown',
      avatar: 'https://i.pravatar.cc/150?u=charlie',
    },
    publishedAt: '2023-12-18',
    readTime: '8 min read',
    tags: ['Mobile', 'Responsive', 'Design'],
    category: 'Design',
  },
  {
    title: 'Analytics and User Insights',
    excerpt: 'Tracking user interactions and deriving insights from conversational UI usage.',
    coverImage: 'https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?w=800',
    author: {
      name: 'Sydney Chen',
      avatar: 'https://i.pravatar.cc/150?u=sydney',
    },
    publishedAt: '2023-12-15',
    readTime: '11 min read',
    tags: ['Analytics', 'Insights', 'Data'],
    category: 'Tutorial',
  },
  {
    title: 'Building Reusable Component Libraries',
    excerpt: 'Creating a scalable component library for agentic UIs that teams can share.',
    coverImage: 'https://images.unsplash.com/photo-1465056836900-8f1e940f2114?w=800',
    author: {
      name: 'Taylor Swift',
      avatar: 'https://i.pravatar.cc/150?u=taylor',
    },
    publishedAt: '2023-12-12',
    readTime: '14 min read',
    tags: ['Components', 'Library', 'Scalability'],
    category: 'Development',
  },
];
```
