@graphicscove/content
v0.1.0
Published
Embeddable CMS admin for Next.js 15+ and Supabase
Downloads
54
Readme
@graphicscove/content
Embeddable CMS admin for Next.js 15+ and Supabase. Install the package, add four small host files, and get a full content admin (posts, pages, media, tags, categories, settings) mounted at /cms — no copied page trees, upgrades via npm install.
Built with TypeScript, React 19 server components, server actions, Tailwind-styled UI, and Supabase (Postgres + Auth + Storage).
Install
npm install @graphicscove/content @supabase/supabase-js @supabase/ssrPeer dependencies: next >= 15, react >= 19, react-dom >= 19.
Integration
Full copy-paste steps: docs/CLIENT-INTEGRATION.md. The short version:
cms.config.ts— host config at the project root:
import { defineCMSConfig } from "@graphicscove/content/config";
export default defineCMSConfig({
siteName: "My Site",
basePath: "/cms",
auth: { roleSource: "user_roles.role", roleKeyColumn: "user_id", allowedRoles: ["admin"] },
});src/app/cms/layout.tsx— auth shell:
import type { ReactNode } from "react";
import cmsConfig from "@cms.config";
import { CmsRootLayout } from "@graphicscove/content";
export default function CmsLayout({ children }: { children: ReactNode }) {
return <CmsRootLayout config={cmsConfig}>{children}</CmsRootLayout>;
}src/app/cms/[[...cmsPath]]/page.tsx— all admin routes:
import cmsConfig from "@cms.config";
import { CmsApp } from "@graphicscove/content";
export const dynamic = "force-dynamic";
export default async function CmsCatchAllPage({
params,
searchParams,
}: {
params: Promise<{ cmsPath?: string[] }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const { cmsPath } = await params;
const raw = await searchParams;
const flat: Record<string, string | undefined> = {};
for (const [key, value] of Object.entries(raw)) {
flat[key] = Array.isArray(value) ? value[0] : value;
}
return <CmsApp config={cmsConfig} cmsPath={cmsPath} searchParams={flat} />;
}src/app/auth/callback/route.ts— Supabase auth callback (setCmsRuntimeConfig+@graphicscove/content/auth/callback)src/middleware.ts— session refresh viaupdateSessionfrom@graphicscove/content/supabase/middlewarenext.config.ts— one#host-configalias (cold-start config for server actions)- Tailwind —
@source "../node_modules/@graphicscove/content/dist/**/*.js"
Add a @cms.config path alias in tsconfig.json (see the integration guide §3).
Database setup
Apply the single migration migrations/001_init.sql to your Supabase project. It is idempotent and contains everything: content tables, roles, row level security, and the cms-media storage bucket with policies.
Roles live in user_roles (user_id, role) — created if it doesn't exist, left untouched if it does (users may hold several roles). No row means no CMS access:
insert into user_roles (user_id, role) values ('<auth-user-uuid>', 'admin');Row level security is part of the migration — do not deploy without applying it. Public readers see only published content whose published_at has passed (which is also how scheduling works); only CMS roles can write. Role table, key column, and accepted roles are configurable via auth.roleSource, auth.roleKeyColumn, and auth.allowedRoles.
Configuration
import { defineCMSConfig } from "@graphicscove/content/config";
export default defineCMSConfig({
siteName: "My Site",
siteUrl: "/", // public site root for preview links
basePath: "/cms", // admin mount path
publicPaths: { posts: "blog" }, // public URL prefixes for previews
auth: {
roleSource: "user_roles.role",
roleKeyColumn: "user_id",
allowedRoles: ["admin"],
allowEditorPublish: false,
},
});Content format
Entry body fields are Markdown (CommonMark). The editor toolbar emits markdown syntax; render it on your public site with the markdown library of your choice (e.g. react-markdown).
Previews and scheduling
- Scheduled publishing: publish an entry with a future publish date and it stays invisible to public readers (enforced by RLS and by the content API) until that time passes. The editor shows it as Scheduled. No cron needed.
- Draft previews: set a
CMS_PREVIEW_SECRETenv var and the editor's Preview link for drafts carries a signed, expiring token (?preview=…). Your public route validates it withverifyPreviewTokenand fetches the draft viagetEntryPreviewusing a service-role client. See the integration guide for the route snippet.
Fetching content on your public site
import { getPublishedPosts, getPostBySlug } from "@graphicscove/content/content";
const { items, total } = await getPublishedPosts(supabase, { page: 1, tag: "news" });
const post = await getPostBySlug(supabase, "my-post"); // null if missing/unpublishedAlso available: getPublishedPages, getPageBySlug, getSettings, getCategories, getMediaById, getEntryPreview, verifyPreviewToken. All published-content helpers are schedule-aware.
Exports
| Import | Use |
|--------|-----|
| @graphicscove/content | CmsApp, CmsRootLayout, setCmsRuntimeConfig, cmsHref, parseCmsPath, types |
| @graphicscove/content/config | defineCMSConfig |
| @graphicscove/content/content | Public-site content fetchers + preview verification |
| @graphicscove/content/auth/callback | Supabase callback GET handler |
| @graphicscove/content/supabase/{client,server,middleware} | Supabase helpers |
| @graphicscove/content/actions | Server actions (internal; rarely imported) |
Architecture details: docs/package-export-map.md.
Development
npm install
npm run test # vitest unit suite
npm run typecheck # strict TS, includes tests
npm run lint
npm run build # compiles src/ → dist/ (tsc + tsc-alias)
npm run smoke # packs the tarball, installs it into a scaffolded
# Next.js app in a temp dir, and runs `next build`There is no host app in this repo — npm run smoke (also run in CI) verifies real-world integration, and for visual development install the package into a Next.js site via a file: dependency.
Publishing
npm publish # prepublishOnly runs typecheck + tests + buildThe tarball ships dist/ (compiled JS + type declarations) and migrations/.
