@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/siteInstalled for you: @pro-laico/core.
Provide yourself (peers): payload and react come with your app; also install @payloadcms/plugin-nested-docs and the @pro-laico/* plugins it builds on — atomic, images, styles, and zap.
Setup
sitePlugin() registers three collections and two globals, all grouped under a Website section in the admin:
- 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).
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
One call registers the collections and globals listed above:
import { buildConfig } from 'payload'
import { sitePlugin } from '@pro-laico/site'
export default buildConfig({
plugins: [sitePlugin()],
})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, 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 collections and globals listed above to build out your site; 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
Edit the collections and globals listed above, under the Website admin group; nest pages with the parent field to build your URL tree.
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 (fetched through the cache getters below) and it renders that document's children blocks and applies its class name, falling back to an empty header / footer when no document is found. Place them around your page content in the root layout, the way the template does:
// 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>
)
}Options
sitePlugin(options?) accepts a single option. It stays unopinionated about cross-package wiring (the atomic hook, nested-docs, 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,
})Environment variables
@pro-laico/site reads no environment variables directly; two reach it through @pro-laico/core's helpers:
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SERVER_URL | Your site's public origin. Core's URL helpers read it (with a VERCEL_PROJECT_PRODUCTION_URL fallback) to build the live-preview URL and the absolute URLs in sitemap entries. |
PREVIEW_SECRET | Embedded in the live-preview URL that core's generateLivePreviewPath builds for the Pages, Header, and Footer collections, and checked by core's preview route handler. |
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.
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/mux-video
Lets editors upload videos in the Payload admin and drop them onto pages. Mux handles the encoding and adaptive streaming, and a ready-made video block renders the player.