light-cms
v0.9.1
Published
Generate Light CMS pages and admin UI for nextjs
Maintainers
Readme
light-cms (CLI)
light-cms generates and maintains CMS editing infrastructure for Next.js apps using Zod schemas and Drizzle.
It can generate:
- Admin auth and admin shell
- Admin forms from
pageSchema.ts - Marketing page renderers
- Sidebar navigation grouped by category
- Role-based edit permissions
- Per-page change logs (recent list + view-all modal)
Install
Run directly with npx:
npx light-cms <command>Requirements
Your app should have:
- Next.js
app/directory - Drizzle DB setup (
db/index.ts,db/schema.ts) lightcms.config.tsin project root
Quick Start
- Initialize typed config:
npx light-cms init- Create a new page schema scaffold:
npx light-cms create about/team- Generate CMS/admin files:
npx light-cms generate- Open admin:
/admin/login- then
/admin/<slug>
lightcms.config.ts
init generates a typed starter with:
orm: "drizzle"enableSidebarIcons?: booleanadminCredentials: { username; password; role? }[]RoleTypePageDataType
Example:
import type { LucideIcon } from "lucide-react";
import type { ZodObject, ZodRawShape } from "zod";
export type RoleType = "admin" | "seo-manager" | (string & {});
type LightCmsConfig = {
orm: "drizzle";
enableSidebarIcons?: boolean;
adminCredentials: Array<{
username: string;
password: string;
role?: RoleType;
}>;
};
export type PageDataType = {
slug: string;
title: string;
category?: string;
allowedRoles?: RoleType[];
icon?: LucideIcon;
schema: ZodObject<ZodRawShape>;
};
const config: LightCmsConfig = {
orm: "drizzle",
enableSidebarIcons: false,
adminCredentials: [
{
username: process.env.ADMIN_USERNAME!,
password: process.env.ADMIN_PASSWORD!,
role: "admin",
},
],
};
export default config;pageSchema.ts Authoring
Export one page data object with:
slugtitleschema(Zod object)- optional:
category,allowedRoles,icon
Example:
import type { PageDataType } from "@/lightcms.config";
import { FileText } from "lucide-react";
import { z } from "zod";
const aboutPageSchema = z.object({
hero: z.object({
title: z.string(),
subheading: z.string().meta({ field: "textarea" }),
}),
about: z.object({
description: z
.string()
.describe("Main about copy")
.meta({
field: "text-area",
info: "Shown on About page",
placeholder: "Write about text...",
}),
}),
});
export const aboutPageData: PageDataType = {
slug: "about",
title: "About",
category: "marketing",
icon: FileText,
allowedRoles: ["admin", "seo-manager"],
schema: aboutPageSchema,
};Field metadata support
.describe("...")-> helper description text.meta({ info: "..." })-> tooltip on label.meta({ placeholder: "..." })-> input placeholder.meta({ field: "textarea" | "text-area" | "text" })-> explicit input control- Optional fields are marked
(optional)
String fields default to input, with textarea inference for common keys:
description, summary, subheading, subtitle, content, body.
Commands
init
Create lightcms.config.ts:
npx light-cms init
npx light-cms init --dry-run
npx light-cms init -ycreate
Create route directory + pageSchema.ts scaffold:
npx light-cms create blog/post
npx light-cms create blog/post --category content
npx light-cms create blog/post --dry-runNotes:
- Uses
app/(marketing)if it exists, otherwiseapp/ - Nested route slug becomes hyphenated (
blog/post->blog-post) - Checks slug collisions
generate
Generate/update CMS files:
npx light-cms generate
npx light-cms generate --slug about
npx light-cms generate --slug about --slug products
npx light-cms generate --dry-run
npx light-cms generate -y
npx light-cms generate --skip-existingBehavior:
- Shared admin/auth files are generated from all discovered schemas
--sluglimits page-specific generation target- Unchanged files are skipped automatically (no overwrite prompt)
remove
Remove generated admin artifacts for a page:
npx light-cms remove about
npx light-cms remove about --dry-run
npx light-cms remove about --delete-schema
npx light-cms remove about -yRemoves:
app/(admin)/admin/<slug>/page.tsxapp/(admin)/admin/<slug>/_components/*-admin-form.tsx- marketing
actions.tsxfor that page
By default it does not remove pageSchema.ts (unless --delete-schema).
Sidebar Grouping and Icons
- Pages are grouped by
category - Missing/empty category falls back to
pages - Matching is case-insensitive
- If
enableSidebarIcons: true, desktop sidebar uses icon-collapse mode pageData.icon(Lucide icon) is shown when provided
Role-Based Editing
allowedRolescontrols who can edit a page- If
allowedRolesis missing, all authenticated users can edit - User role missing in credentials defaults to
admin - Unauthorized users:
- see read-only form controls
- cannot submit (server rejects with explicit permission error)
Change Logs (Audit)
Each page save records a log row in logs table:
idusernamepageSlugtimestampchanges: { field, old, new }[]
UI behavior:
- Admin page footer shows latest 5 logs
- "View all" opens modal with paginated history (load more)
Diff behavior:
- Leaf field paths only (for example
hero.title) - Arrays logged as whole JSON value changes
Expected DB schema:
export const logs = sqliteTable("logs", {
id: text("id").primaryKey(),
username: text("username").notNull(),
pageSlug: text("page_slug").notNull(),
timestamp: text("timestamp").notNull(),
changes: text("changes", { mode: "json" }).notNull(),
});Troubleshooting
- No schemas found: ensure
pageSchema.tsfiles exist underapp/ - Ambiguous remove target: pass a more specific slug/route
- Auth inside cached function: do auth checks outside
"use cache"functions - Unexpected route disappearance: rerun
generatewithout slug filter to rebuild all page admin artifacts
Typical Workflow
initcreate <route>- Edit
pageSchema.ts generate --slug <slug>- Repeat as schema evolves
