@pro-laico/site
The ready-made site shape for Atomic Payload: Pages, Header, and Footer collections plus site-wide Settings and SEO metadata, all from one sitePlugin().
@pro-laico/site gives you the bones of a website out of the box: a Pages collection, editable Header and Footer, site-wide Settings, and SEO / metadata defaults. Instead of remodeling the same collections on every project, you start with a sensible, ready-to-edit site structure. It's a Tool package, so most people get it through the atomic-payload template, which wires it up alongside the other plugins, rather than installing it on its own.
@pro-laico/site is a Tool package, a building block of the Atomic Payload stack, meant to be used alongside @pro-laico/core and the other @pro-laico/* packages (most easily via the atomic-payload template), not on its own.
Installation
pnpm add @pro-laico/sitenpm install @pro-laico/siteyarn add @pro-laico/sitepayload and react are peers you already have in a Payload + Next.js app. @payloadcms/plugin-nested-docs is also required: the Pages collection uses it for parent/child page nesting (it powers the page tree and breadcrumbs). Add it and wire it for the pages collection (see Setup).
Setup
Adding the site shape to your own Payload + Next.js project.
@pro-laico/core comes bundled with this plugin — it's a dependency, not a separate install. Its cached reads reach Payload through a config you register once in your Next.js instrumentation hook (registerPayloadConfig); see @pro-laico/core → Setup to wire it up.
Add the plugin to your Payload config
import { buildConfig } from 'payload'
import { sitePlugin } from '@pro-laico/site'
export default buildConfig({
plugins: [sitePlugin()],
})This registers everything in one go:
- Pages: your routable pages, with a title, slug, derived
href, a published-at date, parent/child nesting, page content, an SEO tab, and per-page settings. - Header and Footer: single-purpose collections for the site chrome you render around every page, each with its own content and an
activetoggle. - Settings: a site-wide global for the persistent store version (draft and published).
- Site MetaData: site-wide SEO fallbacks (site name, default description, default Open Graph image, and light/dark favicons).
All four collections and both globals show up under a Website admin group.
Wire up nested-docs for pages
The Pages collection uses @payloadcms/plugin-nested-docs for its parent field and breadcrumbs, so add that plugin and point it at pages:
import { buildConfig } from 'payload'
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
import { sitePlugin } from '@pro-laico/site'
export default buildConfig({
plugins: [
sitePlugin(),
nestedDocsPlugin({
collections: ['pages'],
parentFieldSlug: 'parent',
breadcrumbsFieldSlug: 'breadcrumbs',
generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''),
generateLabel: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''),
}),
],
})@pro-laico/site stays deliberately unopinionated about this and other cross-package choices (which collections run the atomic hook, the live-preview URL, JSON-schema type generation) so it doesn't force a wiring on you. Those depend on what other plugins you've enabled.
Add content in the admin
Open the Pages, Header, and Footer collections to build out your site, fill in Site MetaData for your SEO fallbacks, and leave Settings at its defaults unless you need to reset the persistent store.
Render the header, footer, and pages in your app
Use it in your app. See Using it in your app below.
The atomic-payload template already includes @pro-laico/site and wires it up for you: the plugin, the nested-docs configuration for pages, and the frontend layout that renders the header, footer, and page content. You just edit content in the admin.
Build your site in the admin
Add pages under the Pages collection (nest them with the parent field to build your URL tree), edit the Header and Footer, and set your SEO fallbacks in Site MetaData, all under the Website admin group.
Let the layout render it
The template's frontend layout already pulls the active header and footer and the page content and renders them, so your edits show up on the site without any extra wiring.
Using it in your app
The package ships ready-made Header and Footer renderers at @pro-laico/site/components/frontend. They're server components: hand each one the matching document and it renders that document's blocks. Place them around your page content in the root layout:
// app/(frontend)/layout.tsx
import { Footer, Header } from '@pro-laico/site/components/frontend'
export default async function RootLayout({
children,
header,
footer,
}: {
children: React.ReactNode
header: HeaderDoc
footer: FooterDoc
}) {
return (
<html lang="en">
<body>
<Header header={header} />
{children}
<Footer footer={footer} />
</body>
</html>
)
}The template fetches the active header and footer documents through the cache getters below and passes them in. Each component renders that document's children blocks and applies its class name; if no document is found it falls back to an empty header / footer:
// app/(frontend)/layout.tsx
import { getCachedFooter, getCachedHeader } from '@pro-laico/site/cache'
import { Footer, Header } from '@pro-laico/site/components/frontend'
import { draftMode } from 'next/headers'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const { isEnabled: draft } = await draftMode()
const header = await getCachedHeader(draft)
const footer = await getCachedFooter(draft)
return (
<html lang="en">
<body>
<Header header={header} />
{children}
<Footer footer={footer} />
</body>
</html>
)
}Render page content the same way. A [...slug] route resolves the page by its href and hands its children blocks to the atomic renderer:
// app/(frontend)/[...slug]/page.tsx
import { getCachedPageByHref, getCachedPages } from '@pro-laico/site/cache'
import { RenderChildren } from '@pro-laico/atomic/children/render'
import { draftMode } from 'next/headers'
import { notFound } from 'next/navigation'
export default async function Page({ params }: { params: Promise<{ slug?: string[] }> }) {
const { isEnabled: draft } = await draftMode()
const { slug } = await params
const pages = await getCachedPages(draft)
const page = await getCachedPageByHref(`/${slug?.join('/') || ''}`, draft, pages)
if (!page) return notFound()
return (
<main className={page.mainClassName || undefined}>
<RenderChildren blocks={page.children} />
</main>
)
}Caching & revalidation
@pro-laico/site ships server-side getters at @pro-laico/site/cache so a page reads its chrome and content once instead of re-querying Payload on every render:
getCachedHeader(draft)/getCachedFooter(draft): the active header / footer documents.getCachedPages(draft): every pagehref(used forgenerateStaticParamsand to validate a requested path).getCachedPageByHref(href, draft, pages): a single page resolved by itshref.getCachedSitemap(draft): the sitemap entries for every indexable page.getCachedSiteMetadata(draft): the site-wide SEO fallbacks from thesiteMetaDataglobal.
Saving a page, header, footer, or your site metadata in the admin revalidates the matching tag (page / pages, header, footer, sitemap, site-metadata), and the collections also revalidate on delete, so the next read serves fresh content. See Caching & revalidation for how the tags and withCache work.
Options
sitePlugin(options?) accepts a single option. It stays unopinionated about cross-package wiring (the atomic hook, nested-docs, live preview, type generation), so there's nothing else to configure here:
Prop
Type
All options at their defaults, as a working starting point:
sitePlugin({
enabled: true,
})Exports
Grouped by what they're for.
The plugin
Export
Type
sitePluginplugin
Parameters
options?:SitePluginOptionsOnly enabled (defaults to true). See the Options table above.Returns
PluginA Payload config plugin you add to buildConfig({ plugins: [...] }).Example
import { buildConfig } from 'payload'import { sitePlugin } from '@pro-laico/site'export default buildConfig({plugins: [sitePlugin()],})Location
@pro-laico/siteSitePluginOptionstype
Location
@pro-laico/siteCollections
Export
Type
Pagescollection
Location
@pro-laico/siteHeadercollection
Location
@pro-laico/siteFootercollection
Location
@pro-laico/siteFields
Export
Type
SEOTabfield
noIndex / priority / changeFrequency sitemap settings). Compose it into your own page-like collections so they share the same SEO shape the built-in Pages collection uses.Parameters
Returns
TabA Payload tabs field entry (named meta, with interfaceName: PageSEO) you drop into a collection’s tabs array.Example
import type { CollectionConfig } from 'payload'import { SEOTab } from '@pro-laico/site'// give your own page-like collection the same SEO tab the built-in Pages usesexport const Articles: CollectionConfig = {slug: 'articles',fields: [ { name: 'title', type: 'text' }, { type: 'tabs', tabs: [SEOTab()] },],}Location
@pro-laico/siteSettingsTabfield
Parameters
apFunctions:APFunction[]The page’s atomic page-function list, passed in from the collection so the admin controls and the generated storage fields can’t drift.Returns
TabA Payload tabs field entry (labelled Settings) you drop into a collection’s tabs array.Example
import type { CollectionConfig } from 'payload'import { SettingsTab } from '@pro-laico/site'const apFunctions = [/* the collection’s atomic page functions */]export const Articles: CollectionConfig = {slug: 'articles',fields: [{ type: 'tabs', tabs: [SettingsTab(apFunctions)] }],}Location
@pro-laico/siteFrontend
Export
Type
Header (component)component
Parameters
header:HeaderThe active header document, usually from getCachedHeader(draft).Returns
Promise<JSX.Element>A <header> element rendering the document’s children blocks.Example
import { getCachedHeader } from '@pro-laico/site/cache'import { Header } from '@pro-laico/site/components/frontend'import { draftMode } from 'next/headers'// app/(frontend)/layout.tsxexport default async function RootLayout({ children }) {const { isEnabled: draft } = await draftMode()const header = await getCachedHeader(draft)return ( <body> <Header header={header} /> {children} </body>)}Location
@pro-laico/site/components/frontendFooter (component)component
Parameters
footer:FooterThe active footer document, usually from getCachedFooter(draft).Returns
Promise<JSX.Element>A <footer> element rendering the document’s children blocks.Example
import { getCachedFooter } from '@pro-laico/site/cache'import { Footer } from '@pro-laico/site/components/frontend'const footer = await getCachedFooter(false)return <Footer footer={footer} />Location
@pro-laico/site/components/frontendCache getters
Export
Type
getCachedHeaderfunction
Parameters
draft:booleanRead the draft (true) or published (false) variant.Returns
Promise<Header>The active header document.Example
import { getCachedHeader } from '@pro-laico/site/cache'import { Header } from '@pro-laico/site/components/frontend'import { draftMode } from 'next/headers'// app/(frontend)/layout.tsxconst { isEnabled: draft } = await draftMode()const header = await getCachedHeader(draft)return <Header header={header} />Location
@pro-laico/site/cachegetCachedFooterfunction
Parameters
draft:booleanDraft or published variant.Returns
Promise<Footer>The active footer document.Example
import { getCachedFooter } from '@pro-laico/site/cache'const footer = await getCachedFooter(false)Location
@pro-laico/site/cachegetCachedPagesfunction
href. Use it to pre-build routes (generateStaticParams) and to check a requested path before fetching the page. createGetCachedPages(slug) binds it to a non-default pages collection.Parameters
draft:booleanAll pages for draft (true), live pages for published (false).Returns
Promise<string[]>Every page href.Example
import { getCachedPages } from '@pro-laico/site/cache'// app/(frontend)/[...slug]/page.tsxexport async function generateStaticParams() {const routes = await getCachedPages(false)return routes.filter((href) => href !== '/').map((href) => ({ slug: href.split('/').slice(1) }))}Location
@pro-laico/site/cachegetCachedPageByHreffunction
href. Returns the slice a route renders (children blocks, mainClassName, meta, id), or undefined when the path isn’t in the page list. createGetCachedPageByHref(slug) binds it to a non-default pages collection.Parameters
href:stringThe page path to resolve, e.g. /about.draft:booleanDraft or published variant.pages:string[]The known page hrefs, usually from getCachedPages(draft), used to skip a Payload query for unknown paths.Returns
Promise<PageReturn | undefined>The page slice, or undefined if not found.Example
import { getCachedPageByHref, getCachedPages } from '@pro-laico/site/cache'import { RenderChildren } from '@pro-laico/atomic/children/render'import { draftMode } from 'next/headers'import { notFound } from 'next/navigation'// app/(frontend)/[...slug]/page.tsxexport default async function Page({ params }) {const { isEnabled: draft } = await draftMode()const { slug } = await paramsconst pages = await getCachedPages(draft)const page = await getCachedPageByHref(`/${slug?.join('/') || ''}`, draft, pages)if (!page) return notFound()return <RenderChildren blocks={page.children} />}Location
@pro-laico/site/cachegetCachedSitemapfunction
createGetCachedSitemap(slug) binds it to a non-default pages collection.Parameters
draft:booleanDraft or published variant.Returns
Promise<SiteMapEntry[]>One entry per indexable page.Example
import { getCachedSitemap } from '@pro-laico/site/cache'// app/(frontend)/sitemap.tsexport default async function sitemap() {const entries = await getCachedSitemap(false)return entries.map(({ url, lastModified, changeFrequency, priority }) => ({ url, lastModified, changeFrequency, priority }))}Location
@pro-laico/site/cachegetCachedSiteMetadatafunction
siteMetaData global. Revalidated when the global is saved.Parameters
draft:booleanDraft or published variant.Returns
Promise<SiteMetaDatum | undefined>The site-metadata document.Example
import { getCachedPageByHref, getCachedPages, getCachedSiteMetadata } from '@pro-laico/site/cache'import { GenerateMetaData } from '@pro-laico/core'// app/(frontend)/[...slug]/page.tsxexport async function generateMetadata({ params }) {const { slug } = await paramsconst pages = await getCachedPages(false)const page = await getCachedPageByHref(`/${slug?.join('/') || ''}`, false, pages)const siteMetadata = await getCachedSiteMetadata(false)return GenerateMetaData({ page, siteMetadata })}Location
@pro-laico/site/cachecreateGetCachedPagesfunction
pages. getCachedPages is this called with the default slug.Parameters
pagesSlug?:stringThe pages collection slug. Defaults to pages.Returns
(draft: boolean) => Promise<string[]>A pages-list getter bound to your slug.Example
import { createGetCachedPages } from '@pro-laico/site/cache'// routable docs live in an `articles` collectionconst getArticleHrefs = createGetCachedPages('articles')const hrefs = await getArticleHrefs(false)Location
@pro-laico/site/cachecreateGetCachedPageByHreffunction
getCachedPageByHref is this called with the default slug.Parameters
pagesSlug?:stringThe pages collection slug. Defaults to pages.Returns
(href: string, draft: boolean, pages: string[]) => Promise<PageReturn | undefined>A page-by-href getter bound to your slug.Example
import { createGetCachedPageByHref } from '@pro-laico/site/cache'const getArticle = createGetCachedPageByHref('articles')const article = await getArticle('/articles/hello', false, hrefs)Location
@pro-laico/site/cachecreateGetCachedSitemapfunction
getCachedSitemap is this called with the default slug.Parameters
pagesSlug?:stringThe pages collection slug. Defaults to pages.Returns
(draft: boolean) => Promise<SiteMapEntry[]>A sitemap getter bound to your slug.Example
import { createGetCachedSitemap } from '@pro-laico/site/cache'const getArticleSitemap = createGetCachedSitemap('articles')const entries = await getArticleSitemap(false)Location
@pro-laico/site/cachePageReturn (type)type
getCachedPageByHref returns (children, mainClassName, meta, id), the part a route renders.Location
@pro-laico/site/cacheSiteMapEntry (type)type
url, priority, lastModified, changeFrequency).Location
@pro-laico/site/cacheSchemas
Export
Type
CollectionSchemasobject
Location
@pro-laico/site/zapTypes
Export
Type
Page (type)type
Location
@pro-laico/site/schemaHeader (type)type
Location
@pro-laico/site/schemaFooter (type)type
Location
@pro-laico/site/schemaSiteMetaDatum (type)type
Location
@pro-laico/site/schemaRelated
@pro-laico/atomic
Turns the nested blocks editors build in the Payload admin into a working, interactive website: reusable content blocks, point-and-click actions, and a complete form pipeline.
@pro-laico/images
Adds the image and favicon upload collections to your Payload admin: web-ready WebP sizes, a favicon picker field, and an image block for your pages.