Atomic Payload
Examples

styles-only

Minimal template that exercises @pro-laico/styles in isolation: the full back-to-front loop from admin-authored atomic classes to a compiled stylesheet.

A minimal Atomic Payload template that demonstrates the @pro-laico/styles plugin in isolation, and the full back-to-front loop it enables.

What it shows

The template wires up @pro-laico/styles (plus @pro-laico/core, and unocss as a styles peer) and nothing else, so you can see the styling pipeline end to end:

  • stylesPlugin({ getCached, generateLivePreviewPath }) in payload.config.ts registers the designSet + shortcutSet collections and the draftStorage / publishedStorage CSS globals. The getCached is createCssGetCached() (from @pro-laico/styles/cache); passing it (and no atomicHook) attaches the standalone cssHook, so CSS is processed without the rest of the Atomic runtime.
  • A pages collection houses example blocks (hero, buttonRow, cardGrid, prose, palette). Every visual choice in a block is a ClassNameField (the styles plugin's atomic-classes input), not a hard-coded class. The same standalone cssHook is attached to pages: on save it walks the document for every *ClassName value (including those nested in blocks), stores them in storedAtomicClasses, and regenerates the stylesheet.
  • getCachedAtomicClasses reads those storedAtomicClasses back off the pages, so authored classes become generated CSS with no safelist.
  • A theme toggle flips the dark class to swap every token's light/dark value.

No stylesheet lives in the repo. A designSet (theme) and shortcutSet are authored in the admin, the standalone UnoCSS cssHook collects the classes and compiles the stylesheet, and the frontend renders the blocks with the result.

This template intentionally drops two things that used to be required:

  • No @pro-laico/zap. stylesPlugin auto-appends zap's schema extension to typescript.schema (its registerTypescriptSchema option, default on), so generate:types resolves the designSet field type $refs without the app importing or wiring zap.
  • No @pro-laico/fonts. No fontField is passed, so the designSet's Fonts tab has no upload fields and the config needs no font collection. To enable font uploads, add @pro-laico/fonts, register fontsPlugin, and pass designSet: { fontField: fontUploadField() } to stylesPlugin.

Scaffold it

pnpm dlx @pro-laico/create-atomic-payload my-styles --template styles-only
npx @pro-laico/create-atomic-payload my-styles --template styles-only
yarn dlx @pro-laico/create-atomic-payload my-styles --template styles-only

Then set up and run it:

cp .env.example .env       # set long PAYLOAD_SECRET + PREVIEW_SECRET (DATABASE_URI is optional)
cp gitignore.template .gitignore
pnpm install
pnpm generate:types        # generates src/payload-types.ts + augment
pnpm generate:importmap    # populates src/app/(payload)/admin/importMap.js
pnpm dev

The demo ships with the SQLite adapter (@payloadcms/db-sqlite) wired to a local file at ./styles-only.db, with no database server required. Swap to Postgres or MongoDB by changing the import + db: call in src/payload.config.ts and installing the matching @payloadcms/db-* package.

Once it's running:

  1. Open http://localhost:3000/admin and create your first user.
  2. Go back to http://localhost:3000. The seed button is now visible: click it to create the design set, shortcut set, and home page.
  3. The page renders, styled by the generated CSS. Edit a block's class names (or a design-set color) in the admin and the page restyles after the save.

How it works

When a page (or the design set / shortcut set) is saved, the cssHook runs the full back-to-front loop:

page (blocks) ─save─▶ cssHook (beforeChange)
                        ├─ collectClassNames(data)  → storedAtomicClasses on the page
                        └─ createCssProcessor → UnoCSS generate(
                              defaultClasses + getCachedAtomicClasses       ◀─ reads every
                              + active designSet tokens                        page's stored
                              + active shortcutSet shortcuts )                 classes
                                                     └─▶ writes layoutCSS to
                                                         draft/published storage globals

   frontend: getCachedSiteCSS → <style> in <head>  ◀────────────────┘
             RenderBlocks(page.layout)  → components apply the stored *ClassName values

POST /api/seed creates the shortcut set, the design set, and a home page made of one of each block (auth-gated, idempotent). / loads the home page, injects the generated stylesheet into <head>, and renders the blocks: every utility, shortcut, token color, radius, and prose style was authored in the admin and compiled on save. Live preview is wired for designSet / shortcutSet / pages edits via <LivePreviewListener> + the /next/preview route handler, with local afterChange hooks adding the revalidateTag calls that bust the frontend cache.

What to look at

src/
  payload.config.ts                     # buildConfig — styles plugin, page cssHook, revalidation
  collections/
    users.ts                            # auth collection (required by Payload)
    pages.ts                            # createPages(cssHook) — blocks + storedAtomicClasses + revalidation
  blocks/
    configs.ts                          # Block configs (ClassNameField-driven) + data types — no React
    components.tsx                      # the matching render components (frontend only)
    RenderBlocks.tsx                    # blockType → component dispatcher
  instrumentation.ts                    # registerPayloadConfig — config for the cache getters
  seed/sampleSets.ts                    # authored designSet + shortcutSet + home page (blocks)
  app/
    (frontend)/
      layout.tsx                        # injects generated CSS + demo chrome; theme provider
      page.tsx                          # demo toolbar + RenderBlocks(home page)
      ThemeToggle.tsx                   # client light/dark toggle
    (payload)/
      admin/importMap.js                # `pnpm generate:importmap` populates this
      api/
        seed/route.ts                   # POST /api/seed → sets + home page
        reset/route.ts                  # POST /api/reset → deletes pages + sets

Configs vs components. blocks/configs.ts is pulled into the Payload config graph (via pages), so it stays free of React / server-only imports. The render components live in blocks/components.tsx and are imported only by the frontend. Keeping them apart is what lets payload generate:types run.

On this page