@pro-laico/core
The shared foundation every Atomic Payload package builds on: end-to-end type safety, tag-based caching and revalidation, JSON-schema type generation, and reusable admin and frontend building blocks.
@pro-laico/core is the shared foundation the rest of Atomic Payload is built on. You rarely install it on its own (the other plugins bring it along), but it's the common ground they all rely on: end-to-end type safety, tag-based caching with correct revalidation, and a set of reusable fields, hooks, and UI pieces. This page is mostly a reference for what it provides.
Installation
pnpm add @pro-laico/corenpm install @pro-laico/coreyarn add @pro-laico/core@base-ui/react, @payloadcms/live-preview-react, @payloadcms/ui, next, payload, react, and server-only are required peers you already have in a Payload + Next.js app. The sibling @pro-laico/* packages (atomic, images, site, styles, tracking) are optional peers: install only the ones you use.
Setup
You usually get @pro-laico/core transitively, and the atomic-payload template wires up everything below for you. When you reach for it directly, these are the pieces worth knowing.
Compose your plugins with pluginComposer
pluginComposer is the recommended way to assemble an Atomic Payload app's plugins. You pass it every plugin (including third-party ones), plus the shared atomicHook, and it returns a Plugin[] you drop straight into buildConfig: the plugins you gave it, followed by a finalizer it appends. Because the finalizer runs last, it sees the fully-assembled config and wires the cross-cutting concerns for you: it attaches the shared atomicHook to the atomic-content collections, and attaches the revalidation hooks to every collection and global. There are no slug lists to keep in sync.
import { buildConfig } from 'payload'
import { pluginComposer } from '@pro-laico/core'
import { atomicHook } from '@pro-laico/atomic/hook'
import { sitePlugin } from '@pro-laico/site'
import { formsPlugin } from '@pro-laico/atomic/forms'
export const plugins = pluginComposer({
atomicHook,
plugins: [
sitePlugin(),
formsPlugin(),
// ...every other plugin, including third-party ones
],
})
export default buildConfig({
plugins,
})This is how the atomic-payload template wires its plugins. The finalizer running last means it also reaches collections registered by third-party plugins (such as @payloadcms/plugin-form-builder's forms and form-submissions), so they get revalidation without you naming them.
Or wire revalidationPlugin directly
When you want explicit control instead of the broad composer wiring, reach for revalidationPlugin: it attaches cache-revalidation hooks to only the collection and global slugs you name. pluginComposer uses these same hooks under the hood, so this is the lower-level building block. Add it to buildConfig like any other plugin:
import { buildConfig } from 'payload'
import { revalidationPlugin } from '@pro-laico/core'
export default buildConfig({
plugins: [
revalidationPlugin({
collectionSlugs: ['pages'],
deleteCollectionSlugs: ['pages'],
globalSlugs: ['settings'],
}),
],
})Generate accurate block types with jsonSchemaPlugin
jsonSchemaPlugin extends payload generate:types so block fields get accurate generated types. Add it to your plugins array (or pass it through pluginComposer with the rest), giving it the block lists each @pro-laico plugin contributes so the generated types stay precise. The template registers it this way.
Generate types with the augmentation step
@pro-laico/core includes a core-augment-types command that writes a payload-types.augment.d.ts file next to your generated payload-types.ts. It connects your project's concrete generated types into the shared type system, so every plugin's schema stubs resolve to your real shapes instead of permissive fallbacks. Chain it after Payload's own type generation, exactly what the template's generate:types script does:
{
"scripts": {
"generate:types": "payload generate:types && core-augment-types"
}
}The generated payload-types.augment.d.ts is rewritten every time you run this, so add it to your .gitignore next to the rest of the generated types:
# .gitignore — generated by payload generate:types
src/payload-types.ts
src/payload-types.augment.d.tsThe rest of the package (fields, hooks, cache helpers, URL and metadata utilities, and the admin / frontend components) is imported directly where you need it. See the Exports table for the full surface and the import path for each item.
Caching & revalidation
@pro-laico/core ships the caching machinery the rest of Atomic Payload reads through, but no data getters of its own. The pieces it owns:
withCacheandmt(from@pro-laico/core/cache): the primitive every package getter wraps its fetch with, plus the merge-tags helper that builds the dependency-tag strings. Server-only.- The revalidation hooks and factories (
revalidateCacheCollection,revalidateCacheCollectionAfterChange,revalidateCacheOnDelete,revalidateCacheGlobalAfterChange, and thecreateRevalidateCache*factories):revalidationPluginattaches them to the slugs you name so saving or deleting a document revalidates the matching tags. revalidateTag: the safenext/cachewrapper used in server actions and the admin deploy control.
See Caching & revalidation for how the tags are derived and how a read-side tag matches the revalidate side.
Options
pluginComposer(options)
Prop
Type
All options at their defaults, as a working starting point:
pluginComposer({
plugins: [], // required (every plugin to compose)
atomicHook, // attaches to the atomic-content collections when set
atomicHookSlugs: ['designSet', 'shortcutSet', 'pages', 'header', 'footer', 'iconSet'], // DEFAULT_ATOMIC_HOOK_SLUGS
// additionalAtomicHookSlugs is unset by default
// revalidate defaults to attaching the core hooks to every collection and global
enabled: true,
})revalidationPlugin(options?)
Prop
Type
All options at their defaults, as a working starting point:
revalidationPlugin({
enabled: true,
collectionSlugs: [],
deleteCollectionSlugs: [],
globalSlugs: [],
})jsonSchemaPlugin(options)
Prop
Type
The blocks option is an object of named block-reference lists (each a (string | undefined)[] of block slugs). Pass the .options array each plugin exports:
All options at their defaults, with the block lists wired the way the template does it:
jsonSchemaPlugin({
enabled: true,
toJSONSchemaExtensions, // required (from @pro-laico/zap)
generateBlocksType, // required (from @pro-laico/zap)
blocks: {
ChildBlocks: ChildBlockType.options, // required
BackdropChildren: BackdropChildSlug.options, // optional
ActionBlocks: ActionBlockType.options, // required
FormRateLimitBlocks: FormRateLimitBlockType.options, // required
FormSanitationBlocks: FormSanitationBlockType.options, // required
FormValidationBlocks: FormValidationBlockType.options, // required
InputSanitationBlocks: InputSanitationBlockType.options, // required
InputValidationBlocks: InputValidationBlockType.options, // required
},
// extraDefinitions is unset by default
})Environment variables
@pro-laico/core's URL and preview helpers read these when present. They're optional (each has a sensible fallback), but setting them keeps generated links and preview routes pointing at the right place.
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SERVER_URL | The site's base URL, used by getServerSideURL for absolute links and metadata. Falls back to http://localhost:3000. |
VERCEL_PROJECT_PRODUCTION_URL | Used as the base URL on Vercel when NEXT_PUBLIC_SERVER_URL isn't set. |
PREVIEW_SECRET | Validates draft-preview requests handled by the preview route handlers and generateLivePreviewPath. |
VERCEL_DEPLOY_WEBHOOK_URL | The deploy hook the admin "trigger deploy" control posts to, when you use the site triggers component. |
Exports
@pro-laico/core is large and spans several import subpaths, so its surface is grouped below by what each piece does and where it comes from.
Plugins & composition
Export
Type
pluginComposerfunction
Parameters
opts:PluginComposerOptionsSee the Options table above. plugins is required; atomicHook, revalidate, atomicHookSlugs, and enabled tune the cross-cutting wiring.Returns
Plugin[]The passed plugins followed by the appended finalizer. Assign it to plugins and spread into buildConfig({ plugins }).Example
import { buildConfig } from 'payload'import { pluginComposer } from '@pro-laico/core'import { atomicHook } from '@pro-laico/atomic/hook'import { sitePlugin } from '@pro-laico/site'import { formsPlugin } from '@pro-laico/atomic/forms'export const plugins = pluginComposer({atomicHook,plugins: [sitePlugin(), formsPlugin()],})export default buildConfig({ plugins })Location
@pro-laico/coreDEFAULT_ATOMIC_HOOK_SLUGSconstant
Location
@pro-laico/corecomposer typestype
Location
@pro-laico/corerevalidationPluginplugin
Parameters
options?:RevalidationPluginOptionsSee the Options table above (collectionSlugs, deleteCollectionSlugs, globalSlugs, enabled). Defaults to an empty config, which is a no-op.Returns
PluginA Payload config plugin you add to buildConfig({ plugins: [...] }).Example
import { buildConfig } from 'payload'import { revalidationPlugin } from '@pro-laico/core'export default buildConfig({plugins: [ revalidationPlugin({ collectionSlugs: ['pages'], deleteCollectionSlugs: ['pages'], globalSlugs: ['settings'], }),],})Location
@pro-laico/corejsonSchemaPluginplugin
Parameters
options:JSONSchemaPluginOptionsSee the Options table above. toJSONSchemaExtensions, generateBlocksType, and blocks are required; pass each plugin's block-slug .options arrays.Returns
PluginA Payload config plugin that appends the schema extension to config.typescript.schema.Example
import { jsonSchemaPlugin } from '@pro-laico/core'import { generateBlocksType, toJSONSchemaExtensions } from '@pro-laico/zap'import { ChildBlockType } from '@pro-laico/atomic/children/zap'import { ActionBlockType } from '@pro-laico/atomic/actions/zap'export const jsonSchemaPluginConfig = jsonSchemaPlugin({toJSONSchemaExtensions,generateBlocksType,blocks: { ChildBlocks: ChildBlockType.options, ActionBlocks: ActionBlockType.options, // ...the form / input block lists},})Location
@pro-laico/corecreateJSONSchemaExtensionsfunction
Parameters
options:CreateJSONSchemaExtensionsOptionsSame toJSONSchemaExtensions, generateBlocksType, blocks, and optional extraDefinitions the plugin takes (minus enabled).Returns
(args: { jsonSchema }) => JSONSchema4A schema-extension function you push onto config.typescript.schema.Example
import { buildConfig } from 'payload'import { createJSONSchemaExtensions } from '@pro-laico/core'import { generateBlocksType, toJSONSchemaExtensions } from '@pro-laico/zap'const schemaExtension = createJSONSchemaExtensions({toJSONSchemaExtensions,generateBlocksType,blocks: { /* ...the block-slug lists... */ },})export default buildConfig({typescript: { schema: [schemaExtension] },})Location
@pro-laico/coreFields
Export
Type
slugFieldfield
Parameters
slugPath:stringThe admin component path your project provides for the custom Slug UI (e.g. SlugPath, or your own).fieldToUse?:stringThe field the slug is formatted from. Defaults to title.overrides?:{ slugOverrides?, checkboxOverrides? }Partial field overrides merged onto the slug and slugLock fields.Returns
[TextField, CheckboxField]The slug field and its hidden slugLock checkbox.Example
import type { CollectionConfig } from 'payload'import { slugField, SlugPath } from '@pro-laico/core'export const Pages: CollectionConfig = {slug: 'pages',fields: [ { name: 'title', type: 'text' }, ...slugField(SlugPath, 'title'),],}Location
@pro-laico/coreStorageTabfield
Parameters
args?:{ filter?: ('classes' | 'forms' | 'actions')[] }Limit which storage groups the tab renders. Defaults to all three.Returns
TabAsFieldA tab field you add to a collection or global fields array.Example
import { StorageTab } from '@pro-laico/core'export const Pages = {slug: 'pages',fields: [ { type: 'tabs', tabs: [/* your content tabs */] }, StorageTab(),],}Location
@pro-laico/coreDevModeFieldfield
Returns
CheckboxFieldA devMode checkbox field.Example
import { DevModeField } from '@pro-laico/core'const fields = [DevModeField()]Location
@pro-laico/coreTestPathFieldfield
pages collection. Equivalent to createTestPathField(); use createTestPathField(slug) for a non-default pages slug.Location
@pro-laico/corecreateTestPathFieldfunction
testPath relationship field, bound to the pages collection slug you pass.Parameters
pagesSlug?:stringThe pages collection slug the relationship points at. Defaults to pages.Returns
RelationshipFieldA testPath relationship field for that pages slug.Example
import { createTestPathField } from '@pro-laico/core'const fields = [createTestPathField('docs')]Location
@pro-laico/coreUniqueTitleFieldfield
Parameters
defaultValue?:stringThe title default. Defaults to New Title.Returns
TextFieldA unique title text field.Example
import { UniqueTitleField } from '@pro-laico/core'const fields = [UniqueTitleField('Untitled')]Location
@pro-laico/coreActiveFieldfield
active APF flag.Parameters
args?:Partial<APArgs<'checkbox'>>Field overrides (label, admin, defaultValue, …) merged onto the base active field.Returns
CheckboxFieldAn active checkbox field.Example
import { ActiveField } from '@pro-laico/core'const fields = [ActiveField({ defaultValue: false })]Location
@pro-laico/coreAPFieldfunction
Parameters
args:APArgs<'text' | 'select' | 'number' | 'textarea' | 'checkbox'>A field config plus an apf array naming the functions it drives.Returns
FieldThe field config with the APF admin components and hooks wired in.Example
import { APField } from '@pro-laico/core'const activeField = APField({type: 'checkbox',name: 'active',apf: ['active'],})Location
@pro-laico/coregenerateAPFFieldsfunction
Parameters
apFunctions:APFunction[]The function names to generate storage fields for (e.g. active, form, page).Returns
CheckboxField[]The virtual APF storage fields.Example
import { generateAPFFields } from '@pro-laico/core'const fields = [...generateAPFFields(['active', 'page'])]Location
@pro-laico/corerunAPFfunction
Parameters
args:{ context, id, apf }The request context, the document id, and the apf function name to check.Returns
booleanTrue when the per-id/apf flag is set for the request.Example
import { runAPF } from '@pro-laico/core'// inside an afterChange hook:if (runAPF({ context, id: doc.id, apf: 'page' })) {// the page APF ran for this save}Location
@pro-laico/coreHooks
Export
Type
revalidate hooksfunction
Location
@pro-laico/corecreateRevalidateCache* factoriesfunction
{ [slug]: handler } record and returns the matching Payload hook.Parameters
handlers:Record<string, (ctx) => void | Promise<void>>Maps a collection slug to the revalidation handler that runs for its writes.Returns
CollectionBeforeChangeHook | CollectionAfterChangeHook | CollectionAfterDeleteHookA hook that dispatches to the matching slug handler (and skips while seeding).Example
import type { CollectionConfig } from 'payload'import { createRevalidateCacheAfterChange, revalidateTag } from '@pro-laico/core'const revalidate = createRevalidateCacheAfterChange({articles: async ({ data }) => { await revalidateTag('article', data?.slug)},})export const Articles: CollectionConfig = {slug: 'articles',hooks: { afterChange: [revalidate] },fields: [/* ... */],}Location
@pro-laico/coreupdateHrefHookfunction
href in sync with its latest breadcrumb as its slug or parents change. Attach it to an href field's beforeChange.Example
import { updateHrefHook } from '@pro-laico/core'const hrefField = {name: 'href',type: 'text',hooks: { beforeChange: [updateHrefHook] },}Location
@pro-laico/coreupdatePublishedAtHookfunction
publishedAt the first time a document is published. Attach it to a date field's beforeChange.Example
import { updatePublishedAtHook } from '@pro-laico/core'const publishedAtField = {name: 'publishedAt',type: 'date',hooks: { beforeChange: [updatePublishedAtHook] },}Location
@pro-laico/coreURL & metadata helpers
Export
Type
getServerSideURLfunction
Returns
stringThe absolute base URL for the current environment.Example
import { getServerSideURL } from '@pro-laico/core'// in a server component or sitemap route:const canonical = `${getServerSideURL()}/about`Location
@pro-laico/coreGenerateMetaDatafunction
Parameters
args:{ page?, siteMetadata? }The page meta slice and the site-wide fallback metadata. Both are structural shapes, so any doc carrying the expected fields works.Returns
MetadataA finished Next.js Metadata object to return from generateMetadata.Example
import { GenerateMetaData } from '@pro-laico/core'// app/(frontend)/[slug]/page.tsxexport async function generateMetadata({ params }) {const page = await getPage(params.slug)const siteMetadata = await getSiteMetadata()return GenerateMetaData({ page, siteMetadata })}Location
@pro-laico/coregenerateLivePreviewPathfunction
admin.livePreview.url. The host project must also mount the /next/preview route handler.Parameters
args:{ data, req }The in-flight document data and the Payload req, as Payload passes them to the live-preview URL builder.options?:{ pagesSlug?: string; fallbackPath?: string }Override the pages collection slug (pagesSlug, defaults to pages) and the path the preview falls back to when the document has no resolvable href or testPath (fallbackPath, defaults to /testing). Set fallbackPath to point a global-like document or a single-page site at a fixed page, e.g. (args) => generateLivePreviewPath(args, { fallbackPath: "/" }).Returns
Promise<string>The absolute preview URL (falls back to fallbackPath, default /testing).Example
import { generateLivePreviewPath } from '@pro-laico/core'export const Pages = {slug: 'pages',admin: { livePreview: { url: generateLivePreviewPath },},}Location
@pro-laico/coreUtilities
Export
Type
mergeCollectionfunction
access / admin shallow-merge, fields append, hooks merge per-phase. The composition helper the plugins use internally to let you extend their collections.Parameters
base:CollectionConfigThe default collection to extend.override?:Partial<CollectionConfig>Your additions. When undefined, base is returned unchanged.Returns
CollectionConfigThe merged collection.Example
import { mergeCollection } from '@pro-laico/core'const Pages = mergeCollection(basePages, {admin: { group: 'Content' },fields: [{ name: 'subtitle', type: 'text' }],})Location
@pro-laico/coremergeGlobalfunction
upload). Shallow-merges access / admin, appends fields, merges hooks per-phase.Parameters
base:GlobalConfigThe default global to extend.override?:Partial<GlobalConfig>Your additions. When undefined, base is returned unchanged.Returns
GlobalConfigThe merged global.Example
import { mergeGlobal } from '@pro-laico/core'const Settings = mergeGlobal(baseSettings, {fields: [{ name: 'announcement', type: 'text' }],})Location
@pro-laico/coremergeHooksfunction
extra is appended after the base array. Works for collection, global, and field hooks.Parameters
base:TThe base hooks object.extra?:THooks to append per phase. When undefined, base is returned unchanged.Returns
TThe merged hooks map (base hooks run before extra within each phase).Example
import { mergeHooks } from '@pro-laico/core'const hooks = mergeHooks({ beforeChange: [baseHook] },{ beforeChange: [myHook], afterDelete: [cleanup] },)Location
@pro-laico/coredeepMergefunction
source onto target, returning a new object.Parameters
target:TThe base object.source:RThe object whose values win on conflict.Returns
TA new deeply-merged object.Example
import { deepMerge } from '@pro-laico/core'const config = deepMerge(defaults, overrides)Location
@pro-laico/coresanitizeDatafunction
Parameters
incomingData:TThe data to clean.Returns
TA cleaned clone of the input.Example
import { sanitizeData } from '@pro-laico/core'const clean = sanitizeData(doc)return <ClientBlock data={clean} />Location
@pro-laico/corestring helpersfunction
Parameters
value:stringThe string to recase.Returns
stringThe recased string.Example
import { toKebabCase, toTitleCase } from '@pro-laico/core'toTitleCase('hero block') // 'Hero Block'toKebabCase('Hero Block') // 'hero-block'Location
@pro-laico/coremanualLoggerfunction
[INFO], [WARNING], [ERROR], …) and only prints when the LOGS env var is true.Parameters
message:stringThe message, optionally prefixed with a recognized tag.Returns
voidLogs to the console; returns nothing.Example
import { manualLogger } from '@pro-laico/core'manualLogger('[INFO] Regenerated stylesheet')Location
@pro-laico/corerevalidateTagfunction
pages also revalidates sitemap, etc.). Marked use server, so it is callable from a server action.Parameters
tag:stringThe primary cache tag to revalidate.id?:stringAn id to scope the tag to (id-bound entries like image / page).draft?:booleanRevalidate the draft variant.Returns
Promise<void>Revalidates the derived tags (and, for some tags, their dependents).Example
'use server'import { revalidateTag } from '@pro-laico/core'export async function refreshPages() {await revalidateTag('pages', false)}Location
@pro-laico/coreCache
Export
Type
withCachefunction
Parameters
fetcher:() => Promise<T>The data fetch to cache (typically a Local API read).options:WithCacheOptionstag (required), optional tid (id), draft, and extraTags for cross-tag dependencies.Returns
Promise<T>The fetcher result, cached and tagged.Example
import { withCache } from '@pro-laico/core/cache'import { getPayloadInstance } from '@pro-laico/core/payload'export const getCachedDesignSet = (draft: boolean) =>withCache( async () => { const payload = await getPayloadInstance() const { docs } = await payload.find({ collection: 'designSet', where: { active: { equals: true } }, draft }) return docs[0] }, { tag: 'designSet', draft },)Location
@pro-laico/core/cachemtfunction
Parameters
stringArray:(string | number | undefined)[]The parts to join (empty / nullish parts are dropped).Returns
stringThe colon-joined tag string (e.g. image:42:draft).Example
import { mt } from '@pro-laico/core/cache'mt(['image', id, draft ? 'draft' : undefined]) // 'image:42:draft'Location
@pro-laico/core/cacheComponents
Export
Type
Toastercomponent
Example
import { Toaster } from '@pro-laico/core/components/frontend/Toaster'// app/(frontend)/layout.tsxexport default function RootLayout({ children }) {return ( <html> <body> {children} <Toaster /> </body> </html>)}Location
@pro-laico/core/components/frontend/ToasterLivePreviewListenercomponent
Example
import { draftMode } from 'next/headers'import { LivePreviewListener } from '@pro-laico/core/components/frontend/LivePreviewListener'// app/(frontend)/layout.tsxexport default async function RootLayout({ children }) {const { isEnabled: draft } = await draftMode()return ( <html> <body> {draft && <LivePreviewListener />} {children} </body> </html>)}Location
@pro-laico/core/components/frontend/LivePreviewListenerslug field componentcomponent
Location
@pro-laico/core/ui/fields/slugsiteTriggers componentcomponent
Location
@pro-laico/core/ui/root/siteTriggersAPF admin componentscomponent
Location
@pro-laico/core/admin/controlspath constantsconstant
Location
@pro-laico/coreKernel & config
Export
Type
kernel typestype
Location
@pro-laico/coreregisterPayloadConfigfunction
Parameters
config:PayloadConfigPromiseYour project config (or its resolution promise), typically import configPromise from '@payload-config'.Returns
voidStores the config on a process-global registry.Example
// src/instrumentation.tsexport async function register(): Promise<void> {if (process.env.NEXT_RUNTIME !== 'nodejs') returnconst { registerPayloadConfig } = await import('@pro-laico/core/config')const { default: configPromise } = await import('@payload-config')registerPayloadConfig(configPromise)}Location
@pro-laico/core/configgetPayloadInstancefunction
Returns
Promise<Payload>The host project's Payload instance.Example
import { getPayloadInstance } from '@pro-laico/core/payload'const payload = await getPayloadInstance()const { docs } = await payload.find({ collection: 'pages' })Location
@pro-laico/core/payloadcreatePreviewRouteHandlerfunction
Parameters
args:{ configPromise }The host Payload config (or its promise), typically import configPromise from '@payload-config'.Returns
(req: NextRequest) => Promise<Response>The route handler. Export it as GET.Example
// app/(frontend)/next/preview/route.tsimport configPromise from '@payload-config'import { createPreviewRouteHandler } from '@pro-laico/core/next/preview'export const GET = createPreviewRouteHandler({ configPromise })Location
@pro-laico/core/next/previewexitPreviewRouteHandlerfunction
Returns
Promise<Response>Disables draft mode and returns a confirmation response.Example
// app/(frontend)/next/exit-preview/route.tsimport { exitPreviewRouteHandler } from '@pro-laico/core/next/exit-preview'export const GET = exitPreviewRouteHandlerLocation
@pro-laico/core/next/exit-previewRelated
Plugins overview
The @pro-laico/* family of Payload plugins and the standard plugin shape.
@pro-laico/styles
Manage your whole design system from the Payload admin: write Tailwind on your blocks and author colors, tokens, reusable shortcuts, and swappable design sets as content, with no config file to edit and no redeploy to restyle your site.