Skip to content

Entry points

Both packages straddle the server and client boundary, so each ships more than one entry. Importing from the wrong one is the most common wiring mistake.

Import Contents Runs where
@bison-lab/payload-blocks Block configs, field builders, row types, resolveMedia, blockSamples, sampleImage Node. What payload.config.ts imports. Touches no React.
@bison-lab/payload-blocks/react Renderers, RenderBlocks and BLOCK_ID_ATTRIBUTE, the image and link seams, resolveLink and ResolveLink, headerFromNavigation, headerItemsFromBlocks, the seam constants, the rendering types Client ("use client").
@bison-lab/payload-blocks/rich-text RichTextBlockRenderer, richTextBlockRenderer() to build one that resolves internal links, and internalDocToHrefFrom Client. Split out because it is the only thing that needs @payloadcms/richtext-lexical.
@bison-lab/payload-blocks/admin MinRowsArrayField and LinkField, the admin fields the configs reference by path Client, inside the Payload admin. Resolved through the site’s import map, never imported by hand.

The renderers are client components because the blocks they render are: every @bison-lab/ui export is a client reference, and the blocks are interactive anyway. That is also what makes imageComponent work. A Server Component can hand a client component reference across the boundary, but not a closure.

The admin entry is referenced by string, never imported: a config names MIN_ROWS_ARRAY_FIELD or LINK_FIELD (both exported from the main entry) as a field’s component, and payload generate:importmap turns the string into the import. A missing entry renders the field as nothing, not as the stock field.

Import Contents Runs where
@bison-lab/payload-core seoPlugin, createTheme, createBrandAssets, createRoles, createFeatures, createPages, createUsers, createMedia, createAdminNav, adminNav, createNavigation, adminOnlyApiTab, predicates, noIndexField, SHARE_IMAGE_SIZE, firstImageIn, title and text helpers, types Node. What payload.config.ts imports; it loads the plugin.
@bison-lab/payload-core/metadata pageMetadata, the title helpers, the same types Server. What a page route imports; it does not load the plugin.
@bison-lab/payload-core/theme getPublishedTheme, themeHead Server. What a root layout imports; it does not load the plugin.
@bison-lab/payload-core/navigation getNavigation, the Header and Footer slugs Server. What a site header imports; it does not load the plugin.

The main entry exports the builders the configs are made of, for a site’s own blocks:

  • imageField({ name, required, relationTo, admin }) for a single upload from the media collection.
  • linkFields({ required, pagesCollection }) for a full link: label, the destination row, and newTab. linkField({ name }) is the same as one named group. linkDestinationFields() is the destination row alone: type, page and href, carrying LINK_FIELD so the editor picks a published page or pastes a URL in one box. pagesCollection points a page link at a collection other than pages.
  • resolveLink(link) and linkTypeOf(link) read a destination the way every renderer and the picker do, so a site’s own code agrees with them on every row, including rows from before links had a type.
  • headingFields({ required }) for the eyebrow, title and description every band opens with.
  • emptyRows(n) and MIN_ROWS_ARRAY_FIELD for an array that opens with n rows and will not go below them.
  • resolveMedia(value) narrows an upload value, resolved or not, to { src, alt, width, height } or undefined.

Neither package can import a site’s payload-types, so every row shape is hand-written and structural: every field optional and nullable, no index signatures. A generated title: string is assignable to title?: string | null, never the reverse. Sites pass their generated types in, as BlockRegistryFor<AnyBlock> and seoPlugin<Page>() show.