{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "related-posts",
  "title": "Related Posts",
  "description": "A compact end-of-post list of related articles your template selects; the current post is always excluded.",
  "registryDependencies": [],
  "files": [
    {
      "path": "payload-components/source/components/RelatedPosts/Component.tsx",
      "content": "import React, { type ReactNode, useId } from 'react'\n\nexport type RelatedPost = {\n  title: string\n  /** A site path such as /posts/slug, or an HTTP(S) URL. Other values are skipped. */\n  href: string\n  excerpt?: string | null\n  /** Pass your populated Payload Media or Next Image element, with meaningful alt text. */\n  image?: ReactNode\n  publishedAt?: string | null\n}\n\nexport type RelatedPostsProps = {\n  /** The post this section is placed on, in the same form as each post href. It is never listed. */\n  currentHref: string\n  /** The posts your template selected, in display order. This component never queries or ranks posts. */\n  posts: readonly RelatedPost[]\n  title?: string\n  locale?: string\n  id?: string\n  className?: string\n  disableInnerContainer?: boolean\n}\n\nconst postHref = (value: string | null | undefined) => {\n  if (!value) return undefined\n  const href = value.trim()\n  if (/^\\/(?![\\/\\\\])/.test(href) && !href.includes('\\\\') && !/[\\u0000- ]/.test(href))\n    return href\n  try {\n    const url = new URL(href)\n    return ['https:', 'http:'].includes(url.protocol) && !url.username && !url.password\n      ? url.href\n      : undefined\n  } catch {\n    return undefined\n  }\n}\n\n/* Two hrefs name the same post when they differ only by a trailing slash or a #fragment. */\nconst comparableHref = (href: string) => {\n  const withoutFragment = href.split('#')[0] ?? ''\n  const queryStart = withoutFragment.indexOf('?')\n  const path = queryStart === -1 ? withoutFragment : withoutFragment.slice(0, queryStart)\n  const query = queryStart === -1 ? '' : withoutFragment.slice(queryStart)\n  return `${path.replace(/\\/+$/, '') || '/'}${query}`\n}\n\nconst formatDate = (value: string | null | undefined, locale: string) => {\n  const date = value ? new Date(value) : undefined\n  if (!date || Number.isNaN(date.valueOf())) return undefined\n  const options: Intl.DateTimeFormatOptions = {\n    day: 'numeric',\n    month: 'short',\n    year: 'numeric',\n    timeZone: 'UTC',\n  }\n  let label: string\n  try {\n    label = new Intl.DateTimeFormat(locale, options).format(date)\n  } catch {\n    label = new Intl.DateTimeFormat('en', options).format(date)\n  }\n  return { dateTime: date.toISOString(), label }\n}\n\n/** Compose once at the end of your post template; your template selects the posts. */\nexport function RelatedPosts({\n  currentHref,\n  posts,\n  title = 'Related posts',\n  locale = 'en',\n  id,\n  className,\n  disableInnerContainer = false,\n}: RelatedPostsProps) {\n  const generatedId = useId()\n  const headingId = id ? `${id}-title` : generatedId\n  const current = postHref(currentHref)\n  // The current post and repeated hrefs are skipped; the rest keep the caller's order.\n  const seen = new Set<string>(current ? [comparableHref(current)] : [])\n  const visible = posts.flatMap((post) => {\n    const href = postHref(post.href)\n    const key = href ? comparableHref(href) : undefined\n    if (!href || !key || !post.title.trim() || seen.has(key)) return []\n    seen.add(key)\n    return [{ ...post, date: formatDate(post.publishedAt, locale), href, key }]\n  })\n\n  if (visible.length === 0) return null\n\n  return (\n    <section\n      id={id}\n      aria-labelledby={headingId}\n      className={['bg-background py-12 text-foreground', className].filter(Boolean).join(' ')}\n    >\n      <div className={disableInnerContainer ? undefined : 'container mx-auto px-6'}>\n        <div className=\"mx-auto max-w-3xl\">\n          <h2 id={headingId} className=\"font-serif text-3xl tracking-tight\">\n            {title}\n          </h2>\n          <ul className=\"mt-6 grid gap-x-6 gap-y-8 sm:grid-cols-2 lg:grid-cols-3\">\n            {visible.map((post) => (\n              <li\n                key={post.key}\n                className=\"group relative flex flex-col border-t border-border pt-4\"\n              >\n                {post.image ? (\n                  <div className=\"mb-4 aspect-video overflow-hidden rounded-lg bg-muted [&_img]:h-full [&_img]:w-full [&_img]:object-cover\">\n                    {post.image}\n                  </div>\n                ) : null}\n                <h3 className=\"break-words text-lg font-medium leading-snug tracking-title\">\n                  <a\n                    className=\"rounded-sm underline-offset-4 after:absolute after:inset-0 group-hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n                    href={post.href}\n                  >\n                    {post.title}\n                  </a>\n                </h3>\n                {post.excerpt ? (\n                  <p className=\"mt-2 line-clamp-3 text-sm leading-6 text-muted-foreground\">\n                    {post.excerpt}\n                  </p>\n                ) : null}\n                {post.date ? (\n                  <time className=\"mt-3 text-xs text-muted-foreground\" dateTime={post.date.dateTime}>\n                    {post.date.label}\n                  </time>\n                ) : null}\n              </li>\n            ))}\n          </ul>\n        </div>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:file",
      "target": "~/src/components/RelatedPosts/Component.tsx"
    }
  ],
  "meta": {
    "payloadComponent": {
      "installCommand": "payload-components add related-posts",
      "postInstall": [],
      "requiresPayloadComponentWrapper": false,
      "supportedTargets": [
        "payload-website-starter",
        "payload-blocks-app"
      ]
    }
  },
  "docs": "File-only article template component. Import RelatedPosts from @/components/RelatedPosts/Component at the end of your post template, pass the current post as currentHref, and pass the posts your template selected. It does no querying or recommendation. No Pages or RenderBlocks edits and no generators. Direct install: pnpm dlx shadcn@latest add https://www.payload-components.xyz/r/related-posts.json Optional tracked install in a supported Payload project: payload-components add related-posts.",
  "type": "registry:component"
}