payload-dev-tools
A dev-only toolbar, live /dev pages, and a machine-readable app snapshot that make building a Payload site faster.
Dev tools that make building a Payload CMS site easier. You get a floating utility menu on every
page and optional /dev routes that show what's actually in your app.
See your seeded content, icons, fonts, images, and videos rendered in your real layout. Test page, header, footer, and block variants right inside the running site. Point an AI agent at one snapshot URL to learn the whole app.
Works on its own; extra panels light up when you also run the other @pro-laico/* plugins.
Everything is dev-only and disappears in production.
pnpm add @pro-laico/payload-dev-toolsRequirements
Next.js 15+ on the App Router, plus Payload ^3 and React 19. This one is Next-shaped by
nature: the toolbar and the /dev pages are Next components (next/link, next/navigation), and
the draft-mode toggle and chrome resolver read cookies() / draftMode() from next/headers.
There's no framework-agnostic subset to fall back on — the whole surface is the UI.
No next.config changes are needed, though: everything mounts through the plugin, one page file,
and one layout line.
What's included
- The
/devpages. Real routes inside your app from one catch-all file:/dev(overview, seed controls, collection counts),/dev/icons(glyph grid, switch the active set in one click),/dev/fonts(specimens in your actual served fonts),/dev/images,/dev/mux,/dev/revalidate, and/dev/tests/<test>(one page per test, toolbar toggles the version). <DevToolbar />. A floating corner button on every page (admin included) that navigates the dev pages, toggles test versions and Next.js draft mode, seeds, and reads diagnostics. Stays open while you browse. Self-styled: no Tailwind, no CSS import, noisDevconditional.- A location toggle. Pick a region in the toolbar and your app renders as it would for a visitor there — GDPR opt-in, CCPA opt-out, or neither. One resolver call, no per-country branching.
- Environment awareness.
payload-dev-env staging -- pnpm devboots against another env file without touching.env.local, and the toolbar names the env you're in, whether the database is local, and which variables the installed plugins are missing. GET /api/dev. A machine-readable snapshot: environment, installed plugins, seed status and counts, icon misses, font slots, mux readiness, per-collection doc counts. Point an AI agent here first. Browsers get redirected to/dev.- Plugin-aware, import-free. Sibling
@pro-laico/*plugins are discovered through theirconfig.custommarkers. None are dependencies; panels and pages appear for whatever is installed.
Dev tool menu component (bottom right) on Pages view, rendered on the /dev/fonts route
Quickstart
No next.config changes are needed. Everything mounts through the plugin, one page file, and one
layout line.
Add the plugin
import { buildConfig } from 'payload'
import { devToolsPlugin } from '@pro-laico/payload-dev-tools'
export default buildConfig({
// …
plugins: [devToolsPlugin()],
})Registers GET /api/dev (snapshot), GET /api/dev/stage (URL staging), GET /api/dev/region
(location override), GET /api/dev/draft (draft-mode toggle), and POST /api/dev/icons/activate
(set switcher). All 404 outside development.
Drop in the dev pages
import { Suspense } from 'react'
import config from '@payload-config'
import { getPayload } from 'payload'
import { createDevPage } from '@pro-laico/payload-dev-tools/next'
const DevPage = createDevPage({ payload: getPayload({ config }) })
export default function Page(props: Parameters<typeof DevPage>[0]) {
return (
<Suspense fallback={null}>
<DevPage {...props} />
</Suspense>
)
}One file, all views. It lives in your (frontend) group, so the pages inherit your layout,
fonts, and styles. Pass createDevPage a Payload handle — getPayload({ config }) from your
app's @payload-config. The pages read live data, so render them inside <Suspense> — a dynamic
hole that streams at request time. Don't reach for export const dynamic = 'force-dynamic' — these
pages only ever render in development, where nothing prerenders anyway.
Worth knowing if you write your own dev pages: Next marks a subtree dynamic when it sees fetch,
cookies(), or headers() — it cannot see a database read through Payload's Local API, so a
prerender would resolve one and freeze the result. It never bites here because these pages don't run
under NODE_ENV=production, but your own pages have no such clamp.
The Seed view needs the seed plugin's preconditions: ENABLE_SEED=true in .env.local and a
logged-in Payload user. The card tells you which one is missing.
Mount the toolbar
import { DevToolbar } from '@pro-laico/payload-dev-tools/toolbar'
export default function Layout({ children }) {
return (
<html lang="en">
<body>
{children}
<DevToolbar />
</body>
</html>
)
}No isDev check needed; it renders null in production. If you want it on all your pages, admin
included, also add the same line inside <RootLayout> in Payload's app/(payload)/layout.tsx.
Because it lives in the layout, the panel survives client-side navigation.
Register your tests
The test harness is the reason to reach for this plugin in earnest. Define your variants once, then
flip between them from the toolbar. Start with a page test and a header test:
import { defineTest } from '@pro-laico/payload-dev-tools/next'
export const heroTest = defineTest({
key: 'hero',
label: 'Homepage hero',
kind: 'page',
versions: [
{ id: 'bold', label: 'Bold', render: () => <BoldHero /> },
{ id: 'split', label: 'Split', render: async () => <SplitHero data={await load()} /> },
],
})
export const headerTest = defineTest({
key: 'site-header',
label: 'Site header',
kind: 'header',
versions: [
{ id: 'compact', label: 'Compact', render: () => <CompactHeader /> },
{ id: 'mega', label: 'Mega menu', render: () => <MegaHeader /> },
],
})
export const devTests = [heroTest, headerTest]Pass the same devTests array to both frontend pieces:
export default createDevPage({ payload: getPayload({ config }), tests: devTests }) // → the page: /dev/tests/hero
<DevToolbar tests={devTests} /> // → the controls: open it, toggle versionsEach page test is one page. The toolbar's chips set a cookie that picks the version: click Bold,
look; click Split, compare.
header/footer tests go further. With one more line in your layout, picking a version swaps it
into the real layout, site-wide, until you hit Real:
import { DevToolbar, resolveDevChrome } from '@pro-laico/payload-dev-tools/toolbar'
import { devTests } from '@/dev/tests'
export default async function Layout({ children }) {
const { header, footer } = await resolveDevChrome({
tests: devTests,
header: <SiteHeader />,
footer: <SiteFooter />,
})
return (
<html lang="en">
<body>
{header}
{children}
{footer}
<DevToolbar tests={devTests} />
</body>
</html>
)
}In production resolveDevChrome returns exactly what you passed in (before touching cookies, so
static optimization is unaffected). A variant that throws falls back to the real chrome. See
The test harness for the full model.
Plugin options
Zero-config by default — every option has a sensible default. See Reference for options, env vars, endpoints, and exports.
Explore
The /dev pages
Every /dev route and what it shows, plus the GET /api/dev snapshot for AI agents.
The test harness
defineTest, version switching, site-wide header/footer swaps, and the script hook.
Testing by location
Pretend to be in Germany or California, and see the consent behaviour your site shows them.
Environments
payload-dev-env, the env-var checklist, and the "you're on a remote database" warning.
How it works
Marker-based plugin discovery, layered gating, draft mode, and the persistent toolbar.
Reference
Plugin options, environment variables, endpoints, and exports.
Troubleshooting
404s, stale pages, wrong entry points, chrome swaps, and the enabled security note.
Troubleshooting
Fix the common revalidation failures fast — a symptom-first table with the literal dev warnings to grep for, plus the /dev/revalidate map that shows exactly what busts when.
The /dev pages
Every /dev route and what it shows, plus the GET /api/dev snapshot that describes your whole app in one request.