Catch CMS bugs before deploy.
cms-lab catches the bugs that show up when your headless CMS and your Next.js routes drift apart - broken routes, duplicate slugs, links to unpublished content, missing SEO/OG/canonical/JSON-LD, image alt and dimensions, and locale gaps - and fails CI before they reach production. Works with Prismic, Strapi, Directus, WordPress, Contentful, Sanity, and Payload.
Run it with no install:
npx @cms-lab/cli scanIt reads your config, fetches CMS entries, probes your running site, and writes terminal, JSON, Markdown, JUnit, Slack, and HTML report output. There is no hosted cms-lab service. The CLI runs inside your project and talks directly to the CMS endpoints you configure.
pnpm add -D @cms-lab/cli @cms-lab/coreYou can also run it without adding it to the project:
npx @cms-lab/cli scanOpen a public example before wiring cms-lab into your own CMS:
The default browser starter is also available at https://cmslab.afaqrashid.com/new.
Create cms-lab.config.ts in a Next.js project:
import { defineConfig } from "@cms-lab/core";
export default defineConfig({
site: { url: "http://localhost:3000" },
framework: { type: "next", router: "app" },
cms: {
provider: "prismic",
repositoryName: "my-repo",
accessToken: process.env.PRISMIC_ACCESS_TOKEN,
},
routes: [
{ type: "page", pattern: "/:uid", getPath: (doc) => `/${doc.uid}` },
{
type: "blog_post",
pattern: "/blog/:uid",
getPath: (doc) => `/blog/${doc.uid}`,
},
],
checks: {
fields: {
required: [
{ type: "page", path: "headline" },
{ type: "blog_post", path: "author.name", severity: "warning" },
],
},
relationships: [
{
from: "blog_post",
to: "author",
where: { fromField: "author.id", toField: "id" },
min: 1,
severity: "warning",
},
],
},
});For localized apps where / is not the page you want to probe first, keep
routes relative to site.url and set a health route separately:
site: {
url: "http://localhost:3000",
healthPath: "/en",
}Run your Next.js app, then scan it:
pnpm next dev
pnpm cms-lab scanFor CI:
pnpm cms-lab scan --ci --report --markdown --junitOr use the GitHub Action:
- uses: i-afaqrashid/cms-lab@v1
with:
config: cms-lab.config.ts
report: true
# node-version: "20" # default; override to "22" or "24" if you needThe action installs Node 20 by default to match @cms-lab/cli's
engines.node >= 20.10. Override with node-version if your workflow
runs on a newer Node line.
Prismic:
cms: {
provider: "prismic",
repositoryName: "my-repo",
accessToken: process.env.PRISMIC_ACCESS_TOKEN,
}Strapi:
import { strapiRelationSlug } from "@cms-lab/core";
cms: {
provider: "strapi",
url: "http://localhost:1337",
token: process.env.STRAPI_TOKEN,
locale: "en",
collections: [
{
type: "page",
endpoint: "pages",
uidField: "routing.slug",
urlField: "routing.url",
},
],
singleTypes: [
{
type: "navbar",
endpoint: "navbar",
},
],
},
routes: [
{ type: "page", pattern: "/:slug", getPath: (doc) => `/${doc.uid}` },
{
type: "article",
pattern: "/blog/:topic/:slug",
getPath: (doc) => {
const topic = strapiRelationSlug(doc.data, "topic") ?? "uncategorized";
return `/blog/${topic}/${doc.uid}`;
},
},
]Directus:
cms: {
provider: "directus",
url: "http://localhost:8055",
token: process.env.DIRECTUS_TOKEN,
collections: [
{
type: "page",
collection: "pages",
uidField: "routing.slug",
urlField: "routing.url",
},
],
}WordPress:
cms: {
provider: "wordpress",
url: "http://localhost:8080",
contentTypes: [
{ type: "page", endpoint: "pages" },
{
type: "post",
endpoint: "posts",
uidField: "acf.handle",
urlField: "acf.permalink",
},
],
}Contentful:
cms: {
provider: "contentful",
spaceId: "my-space",
environment: "master",
accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN,
contentTypes: [
{
type: "page",
contentType: "page",
uidField: "routing.slug",
urlField: "routing.url",
},
],
}Sanity:
cms: {
provider: "sanity",
projectId: "my-project",
dataset: "production",
apiVersion: "2025-02-19",
token: process.env.SANITY_READ_TOKEN,
contentTypes: [
{
type: "page",
documentType: "page",
uidField: "slug.current",
urlField: "seo.canonical",
},
],
}Payload:
cms: {
provider: "payload",
url: "http://localhost:3000",
apiPath: "/api",
token: process.env.PAYLOAD_TOKEN,
collections: [
{ type: "page", collection: "pages", uidField: "slug" },
{ type: "post", collection: "posts", uidField: "slug" },
],
}All adapters normalize content into the same scan model, so route checks, field
checks, SEO checks, report output, and CI behavior stay consistent. Use
uidField when your CMS does not expose a plain uid or slug field. Use
urlField when the CMS already stores the public permalink. Both fields read
dotted paths from document.data.
cms-lab init
cms-lab init --cms strapi --router pages
cms-lab init --cms directus --router pages
cms-lab init --cms payload
cms-lab init --cms wordpress
cms-lab init --cms contentful --space-id my-space
cms-lab init --cms sanity --project-id my-project
cms-lab doctor
cms-lab scan
cms-lab agent-context
cms-lab explain CMS-ROUTE-404Useful scan options:
cms-lab scan --url https://staging.example.com
cms-lab scan --config ./cms-lab.config.ts
cms-lab scan --json
cms-lab scan --ci
cms-lab scan --report
cms-lab scan --report --share-report
cms-lab scan --markdown
cms-lab scan --junit
cms-lab scan --slack-webhook "$CMS_LAB_SLACK_WEBHOOK"
cms-lab scan --type page
cms-lab scan --only routes
cms-lab scan --only relationships
cms-lab scan --skip seo --skip a11y
cms-lab scan --fail-on warning
cms-lab scan --max-warnings 0
cms-lab scan --strict
cms-lab scan --timeout 10000
cms-lab scan --concurrency 4
cms-lab scan --retries 2
cms-lab scan --debug --verbose 2Generate context files for coding agents:
cms-lab agent-context
cms-lab agent-context --preset all
cms-lab agent-context --preset claude
cms-lab agent-context --preset gemini
cms-lab agent-context --preset copilot
cms-lab agent-context --force
cms-lab agent-context --no-agents-md
cms-lab agent-context --out .cms-labThe default preset writes AGENTS.md, .cms-lab/agent-context.md, and
.cms-lab/agent-prompt.md. Tool-specific presets can also write CLAUDE.md,
GEMINI.md, .github/copilot-instructions.md, and
.github/prompts/cms-lab-fix.prompt.md.
The generated files point agents to the cms-lab GitHub repository, npm package, docs, local command examples, configured route patterns, and safe project facts without including tokens, raw CMS payloads, private URLs, webhook URLs, or local absolute paths.
The public test matrix lives at
/docs/tested-with. It lists
only paths covered by a fixture, adapter test, public demo, or repeatable smoke
test, and it marks adapter maturity limits explicitly.
Current coverage includes Prismic with Next.js App Router, Strapi v4 with Next.js Pages Router, Strapi single types, and adapter fixture checks for Directus, WordPress, Contentful, Sanity, and Payload.
See
/docs/bug-examples for the
ordinary CMS failures cms-lab is built to catch.
See /docs/comparison for how
cms-lab fits with link checkers, Playwright, Lighthouse CI, and custom route
crawls.
See /docs/troubleshooting
for common first-run failures and fixes.
See /docs/large-catalogs
for baseline, filtering, and concurrency guidance on larger CMS inventories.
See /docs/providers for
provider-specific setup notes for Prismic, Strapi, Directus, WordPress,
Contentful, and Sanity.
See
/docs/examples/directus-restaurant
for a generic Directus restaurant/catalog configuration.
cms-lab currently checks:
- CMS documents that cannot produce a configured route
- Expected CMS routes that return
404 - Expected CMS routes that return
5xx - Route probes that fail after the site is reachable
- Missing SEO titles and descriptions
- Missing or placeholder image alt text
- Custom required fields declared in
checks.fields.required - Cross-document relationship minimums declared in
checks.relationships
The scanner keeps the original CMS payload in document.data, preserves public permalinks when a CMS exposes them, uses slug-like fields as uid where available, and treats non-public entries such as drafts, archived content, and scheduled WordPress posts as draft.
By default, --json redacts raw CMS document data, document URLs, document UIDs, and absolute project paths. Use --include-sensitive-output only for private automation that needs full payloads.
Report outputs:
--reportwrites.cms-lab/report.html--share-reportredacts CMS source IDs and local project paths from the HTML report, while keeping diagnostic codes, severity, route paths, and field paths visible--markdownwrites.cms-lab/summary.md--junitwrites.cms-lab/junit.xml--slack-webhooksends a compact redacted Slack summary
Terminal, JSON, Markdown, and HTML reports include repeated-finding summaries when the same content type/template produces the same diagnostic many times. Raw row-level diagnostics stay available for debugging.
Slack summaries include counts and diagnostic codes. They do not include CMS tokens, webhook URLs, raw CMS payloads, local project paths, or full JSON output.
cms-lab scan exits:
0when the scan completed under the configured failure threshold1when diagnostics exceed the threshold2for config, load, or validation errors3when the CMS is unreachable or authentication fails4when the site is unreachable130when interrupted
Use --fail-on never when you want artifacts without failing the job. Use --strict when warnings and info diagnostics should fail CI.
pnpm install
pnpm test
pnpm bench
pnpm typecheck
pnpm build
pnpm site:build
pnpm lint
pnpm verify
pnpm smoke:packRun the docs site locally:
pnpm site:devRun the public Prismic smoke fixture:
pnpm build
pnpm live:doctor
pnpm live:scan
pnpm smoke:pack:livepnpm smoke:pack packs the publishable packages, installs them into a clean temporary app, and runs the installed cms-lab binary. pnpm smoke:pack:live does the same package smoke and then scans the public Prismic fixture.
packages/core config, types, diagnostics, checks
packages/cli cms-lab binary and output
packages/next Next.js project detection
packages/prismic Prismic adapter
packages/strapi Strapi adapter
packages/directus Directus adapter
packages/wordpress WordPress adapter
packages/contentful Contentful adapter
packages/sanity Sanity adapter
packages/payload Payload adapter
packages/reporter local HTML report renderer
apps/site marketing site and docs
test-fixtures/ public Prismic fixture- Discussions for questions, ideas for new checks, and show-and-tell.
- Roadmap for what is shipped, planned, and under research.
- Issues for bugs and concrete feature requests.
Contributing or reviewing? See CONTRIBUTING.md and .github/CODEOWNERS for ownership and review paths.
Read CHANGELOG.md and GitHub Releases for version-by-version release history.
Read the versioning and stability policy before using cms-lab as a strict deploy gate in a production project.
Read LAUNCH.md for the release checklist, npm publish flow, post-release verification, and launch-day notes.
Read TESTING.md for local tester workflows. Read CONTRIBUTING.md, SECURITY.md, SUPPORT.md, and CODE_OF_CONDUCT.md before opening public issues or PRs.
cms-lab is MIT licensed. See LICENSE.
