Skip to content

Pages, users, and media

createPages, createUsers, createMedia, slugField, createRedirects, adminOnlyApiTab, documentTitleActions, and listAlign ship from the React-free @bison-lab/payload-core entry.

Pages take the site’s hero and layout blocks, a reserved-slug predicate, a preview path, and optional folderRoots (patients, providers, …). Parent on the page editor is where the page lives: site root, those roots, published CMS pages, folders created by earlier saves, or New folder…. The address is that folder plus a title leaf until first publish, then locks. Every site has one required home page, identified by pageKey /. The Address field and the slug column show /, and the pages list has a Required column with a lock when pageKey is set. Home cannot be deleted, unpublished, or moved. ensureRootPage(payload, data) creates it from onInit (pass the hero and a section). requiredKeys marks other required pages. Preview of home is previewPath("/"); an empty string is still a page with no address. Run payload generate:importmap so the Required cell and the slug cell resolve.

Layout carries the same standing copy as the hero — a page cannot be published without a section — and Publish names that fix instead of only Content → Layout. Title-follow does not wipe the folder prefix. A taken address is refused — never rewritten to slug-2. Unlock opens Parent and a “Page name in address” box (with Use title to reset it from the title); a parent or page-name change writes a 308 the same way a typed slug change does.

import { createPages } from "@bison-lab/payload-core";
import { HeroBlock, HeroCompactBlock, HeroSimpleBlock, HeroSweepBlock, RichTextBlock /* … */ } from "@bison-lab/payload-blocks";
createPages({
heroBlocks: [HeroBlock, HeroSweepBlock, HeroCompactBlock, HeroSimpleBlock],
layoutBlocks: [RichTextBlock /* … */],
isReservedSlug: (slug) => slug === "admin" || slug.startsWith("api/"),
previewPath: (slug) => (slug ? `/${slug}` : "/"),
folderRoots: ["patients", "providers"],
});

Create follows the Content tick. Update without Publish is constrained to _status: draft — an Author can save a draft and cannot publish or edit a live page.

createRedirects() wraps @payloadcms/plugin-redirects for pages only (permanent 308) and collapses chains when a published slug changes. The plugin stores the rows; the site still has to serve them. Pin the plugin to the same version as payload.

Users sit under Settings next to Roles, take secureCookies, and offer whatever rows the Roles Global (or the seed plus the same extras) has. users.roles and users.allowedFeatures set saveToJWT: true. The checklist labels are the editable names. The list’s Roles column (ROLES_CELL) labels from the same live Global, so a renamed role reads its new name and a deleted one is not shown.

createMedia() registers the kit Media list (MEDIA_LIST), a 2560-inside resize, and the share card rendition when imageSizes is omitted — the share size hides from list columns, filters, and group-by. Alt text autofills from the filename on save and in the edit form (MEDIA_ALT_AUTOFILL). Pass your own imageSizes when you need different renditions; the share hide flags stay yours to set.

inUse defaults to true: the list shows an In Use column and calls a package scan endpoint. Set inUse: false to hide the column and skip the scan, or pass { collections: ["pages"], globals: ["theme"] } to narrow it. Bulk delete is admin-only and warns when a selected file is referenced.

The same list opens in an upload field’s Choose existing drawer. There, clicking a file picks it, Create New uploads a file and picks it on save, and a hasMany field gets checkboxes and a Select button. Edit, delete, and Bulk Upload stay on the Media page.

After wiring, run payload generate:importmap so MediaListView and MediaAltAutofill resolve. Sites that previously called createMedia() without imageSizes or resizeOptions gain stored upload fields — run payload migrate:create after upgrading.

adminOnlyApiTab shows the document API tab only to Developer. documentTitleActions is one admin provider that puts every document’s primary actions on the title row — the same slot collection lists use for Create New — and hides the dead Edit tab when it has no sibling. One-list Globals pass documentCreateNew("roles") (or a site field path) so Create New uses those same words, with no icon.

listAlign is one admin provider that keeps a list column’s header and cells on the same alignment. Declare it on the field, never as a per-column selector in a site stylesheet:

payload.config.ts
import { listAlign } from "@bison-lab/payload-core";
plugins: [listAlign()],
// any collection field
{ name: "views", type: "number", admin: { custom: { listAlign: "end" } } }

listAlign is "start" (the default), "center", or "end". The plugin reads every collection field at config time, including fields inside rows, collapsibles, tabs and named groups, and writes one stylesheet. A centered header puts the label word itself on the cells’ center line, with the sort arrows hanging beside it; an end-aligned header moves the arrows to the label’s leading side so the label ends where the values end. The Pages Required column is centered this way. Run payload generate:importmap once after adding the plugin.

Overlay settings (Better Editor) take developerOnlyAccess and hideUnlessDeveloper on the site — the package does not add that plugin as a peer.