npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@harnonlabs/cms

v0.6.0

Published

Headless CMS library for React with Cloudflare D1, inline editing, and admin components

Downloads

579

Readme

@harnonlabs/cms

A headless CMS library for React applications backed by Cloudflare D1 databases. Provides server-side CRUD API handlers and client-side inline editing components with preview/production mode support.

Features

  • Server-side API handler — Drop-in Next.js App Router route handlers for schema sync, CRUD, pagination, and publish
  • Inline editing — contentEditable text editing and image picking with hover overlays
  • Admin components — Entity list, record table, and record form components for building admin panels
  • Preview/production modes — Edit in preview, publish to production with a single API call
  • D1 integration — Built for Cloudflare D1 with parameterized queries and automatic schema management
  • Dual ESM/CJS — Ships both ESM and CJS bundles with full TypeScript declarations

Installation

npm install @harnonlabs/cms

Peer dependencies

npm install react react-dom

Supports React 18 and 19.

Database Setup

Create two D1 databases — one for preview and one for production:

npx wrangler d1 create cms-preview
npx wrangler d1 create cms-production

Add the bindings to your wrangler.toml:

[[d1_databases]]
binding = "CMS_PREVIEW_DB"
database_name = "cms-preview"
database_id = "<your-preview-database-id>"

[[d1_databases]]
binding = "CMS_PRODUCTION_DB"
database_name = "cms-production"
database_id = "<your-production-database-id>"

API Route Setup

Create a catch-all API route in your Next.js App Router project:

// app/api/cms/[...path]/route.ts
import { createCMSHandler } from "@harnonlabs/cms";
import type { CMSConfig } from "@harnonlabs/cms";

const config: CMSConfig = {
  getDB: (mode) => {
    // Return the appropriate D1 binding based on mode
    const env = process.env as unknown as {
      CMS_PREVIEW_DB: D1Database;
      CMS_PRODUCTION_DB: D1Database;
    };
    return mode === "production" ? env.CMS_PRODUCTION_DB : env.CMS_PREVIEW_DB;
  },
};

const handler = createCMSHandler(config);

export const GET = handler.GET;
export const POST = handler.POST;
export const PUT = handler.PUT;
export const DELETE = handler.DELETE;

API Routes

| Method | Path | Description | |--------|------|-------------| | POST | /api/cms/sync | Sync entity schemas to both databases | | GET | /api/cms/entities | List all CMS tables | | GET | /api/cms/entities/:name | Get entity schema columns | | GET | /api/cms/entities/:name/records | List records (supports ?limit= and ?offset=) | | POST | /api/cms/entities/:name/records | Create a record | | PUT | /api/cms/entities/:name/records/:id | Update a record | | DELETE | /api/cms/entities/:name/records/:id | Delete a record | | POST | /api/cms/entities/:name/publish | Copy preview data to production |

The database mode is read from the X-CMS-Mode header, then the ?mode= query parameter, defaulting to "preview".

Layout Integration

Wrap your app with CMSProvider to enable inline editing:

// app/layout.tsx
import { CMSProvider } from "@harnonlabs/cms/client";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <CMSProvider enabled={true} apiPath="/api/cms">
          {children}
        </CMSProvider>
      </body>
    </html>
  );
}

CMSProvider Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | children | ReactNode | — | Child components | | apiPath | string | "/api/cms" | Base path for CMS API requests | | enabled | boolean | true | Enable/disable CMS overlays |

useCMS Hook

Access CMS state and actions from any child component:

import { useCMS } from "@harnonlabs/cms/client";

function MyComponent() {
  const {
    entities,    // Map<string, EntityMeta>
    mode,        // "preview" | "production"
    enabled,     // boolean
    setMode,     // (mode: CMSMode) => void
    syncSchema,  // (entities: EntityMeta[]) => Promise<void>
    addRecord,   // (entityName, data?) => Promise<Record>
    updateField, // (entityName, recordId, field, value) => Promise<Record>
    deleteRecord,// (entityName, recordId) => Promise<void>
    publish,     // (entityName) => Promise<{ count: number }>
  } = useCMS();
}

Page CMS Attributes

Annotate your page elements with data attributes to make them CMS-editable:

// app/page.tsx
export default function HomePage() {
  return (
    <section data-cms-en="blog_post">
      <article data-cms-id="1">
        <h2 data-cms-prop="title">My First Post</h2>
        <p data-cms-prop="body">Post content goes here...</p>
        <img data-cms-prop="hero_image" src="/placeholder.jpg" alt="Hero" />
      </article>
    </section>
  );
}

Attribute Reference

| Attribute | Description | |-----------|-------------| | data-cms-en | Marks an element as a CMS entity container. Value is the entity name (e.g., "blog_post"). | | data-cms-id | Identifies a specific record within an entity. Value is the record ID. | | data-cms-prop | Marks an element as an editable CMS field. Value is the field/column name. |

Entity names are automatically converted to table names with a cms_ prefix (e.g., blog_post becomes cms_blog_post). Field types are inferred from the DOM element:

  • <img> or elements with background-image style are stored as TEXT with the image flag
  • <input type="number"> or <input type="range"> are stored as REAL
  • Text content that parses as a number is stored as REAL
  • Everything else is stored as TEXT

Admin Panel Setup

Build an admin page using the provided components:

// app/admin/page.tsx
"use client";

import { useState } from "react";
import { AdminEntityList } from "@harnonlabs/cms/client";
import { AdminRecordTable } from "@harnonlabs/cms/client";
import { AdminRecordForm } from "@harnonlabs/cms/client";

export default function AdminPage() {
  const [selectedEntity, setSelectedEntity] = useState<string | null>(null);
  const [mode, setMode] = useState<"preview" | "production">("preview");

  return (
    <div style={{ display: "flex" }}>
      <aside style={{ width: 240 }}>
        <AdminEntityList
          selectedEntity={selectedEntity}
          onSelectEntity={setSelectedEntity}
          mode={mode}
        />
      </aside>
      <main style={{ flex: 1 }}>
        <label>
          <input
            type="checkbox"
            checked={mode === "production"}
            onChange={(e) =>
              setMode(e.target.checked ? "production" : "preview")
            }
          />
          Production mode
        </label>
        {selectedEntity && (
          <AdminRecordTable entityName={selectedEntity} mode={mode} />
        )}
      </main>
    </div>
  );
}

Admin Component Props

AdminEntityList — Sidebar listing discovered CMS entities.

| Prop | Type | Default | Description | |------|------|---------|-------------| | selectedEntity | string \| null | — | Currently selected entity name | | onSelectEntity | (name: string) => void | — | Called when an entity is clicked | | apiPath | string | "/api/cms" | API base path | | mode | string | "preview" | Database mode |

AdminRecordTable — Table displaying records for a selected entity.

| Prop | Type | Default | Description | |------|------|---------|-------------| | entityName | string | — | Entity to display records for | | onEditRecord | (record) => void | — | Called when edit button is clicked | | apiPath | string | "/api/cms" | API base path | | mode | string | "preview" | Database mode |

AdminRecordForm — Modal form for creating and editing records.

| Prop | Type | Default | Description | |------|------|---------|-------------| | entityName | string | — | Entity name | | columns | ColumnInfo[] | — | Schema columns for the entity | | record | Record \| null | null | Record to edit, or null for create mode | | onClose | () => void | — | Called when modal is closed | | onSaved | (record) => void | — | Called after successful save | | apiPath | string | "/api/cms" | API base path | | mode | string | "preview" | Database mode |

CLI

The package includes a CLI tool (rlooper-cms) to scaffold CMS files into a Next.js project.

Quick Start

npx rlooper-cms init

Or install globally:

npm install -g @harnonlabs/cms
rlooper-cms init

init Command

The init command runs an interactive wizard that detects your project setup and generates boilerplate files:

  1. Detects Next.js, TypeScript, and the app directory (src/app or app)
  2. Prompts you to configure:
    • API route base path (default: /api/cms)
    • Whether to generate an API route handler
    • Whether to wrap your root layout with CMSProvider
    • Whether to generate an admin panel page
    • Whether to set up Cloudflare D1 databases
  3. Generates the selected files, creating directories as needed

Generated Files

| File | Description | |------|-------------| | app/api/cms/[...path]/route.ts | Catch-all API route handler with D1 integration | | app/layout.tsx | Root layout wrapped with CMSProvider | | app/admin/page.tsx | Admin panel with entity list and record table | | wrangler.toml | Cloudflare D1 database bindings (if D1 setup selected) |

File extensions are .ts/.tsx or .js/.jsx depending on whether TypeScript is detected. If a file already exists, you are prompted before overwriting.

Build and Deploy

# Build the library
npm run build

# Deploy your consuming application
npx wrangler deploy

Types

All types are exported from the server entry point:

import type {
  CMSMode,       // "preview" | "production"
  FieldMeta,     // { name, columnName, columnType, isImage }
  EntityMeta,    // { name, tableName, fields: FieldMeta[] }
  CMSConfig,     // { getDB: (mode: CMSMode) => D1Database }
  CMSHandlerResult, // { GET, POST, PUT, DELETE }
  SyncPayload,   // { entities: EntityMeta[] }
} from "@harnonlabs/cms";

License

MIT