Adapters
Three facts only the site knows, so the package takes each as a prop on RenderBlocks and never guesses: how an image gets optimised (imageComponent), which router renders a link (linkComponent), and what path a page lives at (resolveLink). The package has no next dependency, because next/image needs per-site configuration it cannot supply, so a Next site writes these once and hands them in.
All three live in a "use client" module. Every @bison-lab/ui export is a client reference, and the blocks are interactive anyway. That is also what makes the seams work: a Server Component can hand a client component reference across the boundary, but not a closure, and resolveLink is a closure.
Images
Section titled “Images”"use client";import Image from "next/image";import type { BlockImageProps } from "@bison-lab/payload-blocks/react";
export function NextBlockImage({ src, alt, width, height, ...rest }: BlockImageProps) { if (!width || !height) return <Image src={src} alt={alt} fill {...rest} />; return <Image src={src} alt={alt} width={width} height={height} {...rest} />;}Uploads are served from /api/media/file/…, so widen images.localPatterns (or remotePatterns) in next.config.ts to match before any CMS image renders.
Sample images are inline SVG data: URLs. next/image treats a data: src as unoptimized on its own, so the adapter needs no special case for a preview.
"use client";import Link from "next/link";import type { BlockLinkComponent } from "@bison-lab/payload-blocks/react";
export const NextBlockLink: BlockLinkComponent = ({ newTab, ...props }) => <Link {...props} />;Every href a block emits goes through this component: a hero call to action, a showcase panel’s corner action, a NAP tel: link, a menu, a link typed into a rich text section. newTab arrives as a flag and as target and rel already expanded from it, so the adapter only drops the flag before spreading.
Without an adapter, DefaultBlockLink renders a plain anchor and sets target and rel together whenever newTab is on, and DefaultBlockImage renders a plain <img>.
Page paths
Section titled “Page paths”A link a block asks for is stored as type, page and href; see Links for the rule. resolveLink turns that destination into the href the link component receives, or null when there is nothing to point at. The package’s own resolver makes a published, populated page /<slug>. A site whose routes differ wraps it:
"use client";import { resolveLink, type ResolveLink } from "@bison-lab/payload-blocks/react";import { pagePath } from "@/lib/routes"; // the site's route helper
export const resolveSiteLink: ResolveLink = (link) => link.type === "page" && typeof link.page === "object" && link.page?.slug ? pagePath(link.page.slug) : resolveLink(link);Falling through to the package’s resolveLink keeps its null rule for missing and unpublished pages and its pass-through for external links, so the site only spells the one thing it knows.
The same function serves the rich text block. A link an editor makes to another document arrives as a Lexical node rather than a link row, and richTextBlockRenderer({ resolveLink: resolveSiteLink }) unpacks it and calls this resolver, so the page path is defined once.
Inside a @bison-lab/ui block
Section titled “Inside a @bison-lab/ui block”A library block that emits an anchor takes renderLink for the same reason, and the renderer hands the seam through with renderLinkWith(linkComponent). A site rendering a library block directly, outside the CMS, can use the same helper:
import { renderLinkWith } from "@bison-lab/payload-blocks/react";
<FAQColumnsBlock {...props} renderLink={renderLinkWith(NextBlockLink)} />