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

@celar/git-cms

v1.5.2

Published

Git-based CMS for Next.js - Store content as markdown in GitHub

Readme

@celar/git-cms

A Git-based CMS for Next.js. Content lives as markdown files in your GitHub repo — no database, no separate deployment.


What this package provides

  • Admin UI at /admin for creating and editing content
  • GitHub OAuth authentication (via NextAuth v5)
  • Email/password login as an alternative auth path
  • Role-based access control (admin, editor)
  • User management UI built into the admin (admin-only)
  • Extensible adapter system for user storage — Vercel Edge Config ships out of the box
  • REST API for reading/writing markdown files to GitHub
  • Schema system to define your content structure
  • Draft / publish workflow with per-page status
  • Release manager — batch-publish multiple draft pages in one git commit (admin-only)
  • Utilities to read and render content in your frontend

You wire it into your Next.js app by creating a few files and defining your content schemas.


In your Next.js project

1. Install

npm install @celar/git-cms

If you plan to use the Vercel Edge Config adapter for user storage, also install:

npm install @vercel/edge-config

2. Files to create

your-app/
├── app/
│   └── admin/
│       ├── [[...cms]]/
│       │   └── page.tsx                    # renders the admin UI
│       └── api/
│           └── [...cms]/
│               └── route.ts                # all CMS routes (auth + content + users)
├── auth.ts                                 # createAuth setup — shared across files
├── cms.config.ts                           # your content schemas
└── .env.local

Note: The two separate auth and CMS route files from v1 are now consolidated into a single [...cms] catch-all route. The dispatcher handles the auth/*, cms/*, and users/* segments internally.

3. Auth setup

Create auth.ts at your project root. This is where you configure the UserStore adapter and create the NextAuth helpers. Import from this file wherever you need auth, handlers, signIn, or signOut.

With Vercel Edge Config (recommended):

auth.ts

import { createAuth } from '@celar/git-cms/auth'
import { VercelEdgeConfigAdapter } from '@celar/git-cms/adapters'

export const userStore = new VercelEdgeConfigAdapter({
  connectionString: process.env.EDGE_CONFIG!,
  token: process.env.VERCEL_API_TOKEN!,
  projectId: process.env.VERCEL_PROJECT_ID!,
  edgeConfigId: process.env.VERCEL_EDGE_CONFIG_ID!,
})

export const { handlers, auth, signIn, signOut } = createAuth(userStore)

Without a user store (GitHub-only, no email login):

If you only need the GIT_CMS_ADMIN bootstrap admin and no user management, you can omit the adapter:

import { createAuth } from '@celar/git-cms/auth'

export const { handlers, auth, signIn, signOut } = createAuth()

4. Admin page

app/admin/[[...cms]]/page.tsx

import AdminPage from '@celar/git-cms/core'
import { auth } from '../../../auth'
import { blockSchemas, pageSchemas } from '../../../cms.config'

export default function Page() {
  return AdminPage({ blockSchemas, pageSchemas, auth })
}

5. CMS API route

A single catch-all route handles GitHub OAuth, content operations, and user management.

app/admin/api/[...cms]/route.ts

import { createDispatcher } from '@celar/git-cms/core'
import { handlers, auth, userStore } from '../../../../auth'

export const { GET, POST, PATCH, DELETE } = createDispatcher({
  handlers,
  auth,
  userStore, // omit if not using user management
})

6. Define your schemas

cms.config.ts

import type { BlockSchema, PageSchema } from '@celar/git-cms'

export const blockSchemas: BlockSchema[] = [
  {
    type: 'hero',
    label: 'Hero',
    fields: [
      { name: 'heading', label: 'Heading', fieldType: 'text', required: true },
      { name: 'body',    label: 'Body',    fieldType: 'richtext' },
      { name: 'image',   label: 'Image',   fieldType: 'image' },
    ],
  },
]

export const pageSchemas: PageSchema[] = [
  {
    type: 'page',
    label: 'Page',
    contentPath: 'content/pages',  // folder in your GitHub repo
    allowedBlocks: 'any',
  },
]

7. Environment variables

.env.local

# GitHub repo to store content in
GITHUB_OWNER=your-username
GITHUB_REPO=your-repo

# GitHub OAuth app
AUTH_GITHUB_ID=your-oauth-app-id
AUTH_GITHUB_SECRET=your-oauth-app-secret
AUTH_SECRET=random-secret-string        # openssl rand -base64 32

# Bootstrap admin — GitHub user ID (numeric) of the first admin
# Find your ID at: https://api.github.com/users/<your-username>
GIT_CMS_ADMIN=1234567

# GitHub token for email/password users (required if you have any email users)
# These users have no GitHub OAuth session, so the CMS uses this token to
# read and write content on their behalf. A fine-grained PAT scoped to this
# repo with Contents: Read and Write is sufficient.
GIT_CMS_GITHUB_TOKEN=github_pat_...

# Vercel Edge Config adapter (only needed if using VercelEdgeConfigAdapter)
EDGE_CONFIG=ecfg_...                    # connection string from Vercel dashboard
VERCEL_API_TOKEN=...                    # token with Edge Config write permission
VERCEL_PROJECT_ID=prj_...
VERCEL_EDGE_CONFIG_ID=ecfg_...

# Optional: enable live preview
GIT_CMS_PREVIEW_KEY=random-secret-string
GIT_CMS_PUBLIC_URL=http://localhost:3000

Here's exactly where to find each one in the Vercel dashboard:


EDGE_CONFIG — connection string

  1. Go to your Vercel dashboard → Storage tab (top nav)
  2. Click Create → Edge Config → give it a name → Create
  3. Once created, click the store → Tokens tab
  4. Copy the value under Connection String — it starts with https://edge-config.vercel.com/ecfg_...

VERCEL_EDGE_CONFIG_ID

Same page as above. The Edge Config ID is visible in the URL of the store page: vercel.com//stores/edge-config/ecfg_xxxxxxxxxxxx

Or copy it from the connection string — it's the ecfg_... part at the end.


VERCEL_PROJECT_ID

  1. Go to your project in the Vercel dashboard
  2. Settings → General
  3. Scroll down to Project ID — copy the prj_... value

VERCEL_API_TOKEN

  1. Click your account avatar (top right) → Account Settings
  2. Go to Tokens in the left sidebar
  3. Click Create Token
  4. Give it a name (e.g. git-cms-edge-config), set scope to your team, set an expiry
  5. Copy the token — you only see it once

▎ The token needs permission to write to Edge Config. A full-scope account token works, but if you want to limit ▎ it: Vercel doesn't offer per-resource token scopes on the free plan, so a team-scoped token is the minimum.


Connecting the Edge Config store to your project

One extra step people miss: the store must be linked to your project or the connection string won't work at runtime.

  1. Go to your project → Storage tab
  2. Click Connect Store → select your Edge Config store → Connect

After linking, Vercel automatically injects EDGE_CONFIG into your project's environment variables. You can verify this under Settings → Environment Variables.

User management

How access control works

When a user tries to sign in:

  1. GitHub path — after OAuth, the system checks if the GitHub user ID matches GIT_CMS_ADMIN. If yes, admin access is granted immediately. Otherwise, the user must be registered in the UserStore with a role.
  2. Email path — the email and password are checked against the UserStore. No account means no access.

Users not found in either check see an access denied screen.

The bootstrap admin

Set GIT_CMS_ADMIN to your GitHub user ID (the numeric ID, not your username — usernames can change). This grants you permanent admin access regardless of the UserStore and lets you add other users through the CMS UI.

GIT_CMS_ADMIN=1234567

Find your GitHub user ID:

curl https://api.github.com/users/<your-username>
# look for the "id" field

Once you have added yourself (or another admin) to the UserStore through the UI, you can remove GIT_CMS_ADMIN from your environment — but keeping it as a fallback is fine.

Adding users

Log in as admin, navigate to Users in the CMS header, and click Add user. Two types are supported:

GitHub user — enter their GitHub username. The CMS resolves it to a stable GitHub user ID server-side and stores that (so renames don't break access). They log in via the usual "Sign in with GitHub" button.

Email user — enter a display name, email address, and password. The password is bcrypt-hashed server-side before being stored. They log in with email and password on the sign-in page.

Roles

| Role | Access | |------|--------| | admin | Full content access + user management (add, remove, change roles) | | editor | Full content access, no user management |

Roles can be changed at any time from the Users view. Changes take effect on the user's next login.

Custom adapters

Any object that implements the UserStore interface works as an adapter:

import type { UserStore, CmsUser, CmsUserPublic, NewCmsUser, UserUpdate } from '@celar/git-cms'

class MyCustomAdapter implements UserStore {
  async getUserByGithubId(githubId: string): Promise<CmsUser | null> { ... }
  async getUserByEmail(email: string): Promise<CmsUser | null> { ... }
  async getAllUsers(): Promise<CmsUserPublic[]> { ... }
  async addUser(user: NewCmsUser): Promise<CmsUserPublic> { ... }
  async updateUser(id: string, update: UserUpdate): Promise<CmsUserPublic> { ... }
  async removeUser(id: string): Promise<void> { ... }
}

Security contract for custom adapters:

  • getUserByEmail is the only method that may return passwordHash — it is needed internally by the Credentials provider for bcrypt comparison.
  • All other methods (getAllUsers, addUser, updateUser) must never include passwordHash in their return values. Use the CmsUserPublic type (which omits passwordHash) for all responses.
  • Hash passwords with bcrypt (cost factor 12 or higher) before storing. Never store plaintext passwords.

GitHub token for email users

Email/password users have no GitHub OAuth session, so they cannot use their own credentials to read and write content. When the CMS receives a request from an email user, it falls back to GIT_CMS_GITHUB_TOKEN — a server-side token used on their behalf.

What to create

Use a fine-grained personal access token scoped to only this repo:

  1. Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
  2. Click Generate new token
  3. Set Resource owner to the account or org that owns the content repo
  4. Under Repository access, select Only select repositories → choose your content repo
  5. Under Permissions → Repository permissions, set:
    • Contents: Read and Write
    • Everything else: No access
  6. Copy the token → GIT_CMS_GITHUB_TOKEN

What this means for Git history

All writes by email users appear in the GitHub commit history under the PAT owner's account. The CMS still records the actual user's name in the file's frontmatter (metadata.author), so content attribution is preserved — only the Git committer identity is shared.

If accurate per-user Git history matters for your project, stick to GitHub OAuth accounts for editors.


Setup GitHub OAuth

  1. Go to GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
  2. Fill in:
    • Homepage URL: http://localhost:3000
    • Authorization callback URL: http://localhost:3000/admin/api/auth/callback/github
  3. Copy the Client IDAUTH_GITHUB_ID
  4. Generate a Client SecretAUTH_GITHUB_SECRET
  5. For production, create a second OAuth App with your live domain, or update the callback URL

Setup Vercel Edge Config

  1. In your Vercel dashboard, go to your project → StorageCreate Edge Config store
  2. Copy the Connection StringEDGE_CONFIG
  3. Copy the Edge Config ID (starts with ecfg_) → VERCEL_EDGE_CONFIG_ID
  4. Go to Account Settings → Tokens → create a token with at least Edge Config write access → VERCEL_API_TOKEN
  5. Copy your project ID from the project settings → VERCEL_PROJECT_ID

The Edge Config store is independent from your deployments. Adding or removing a user through the CMS admin takes effect instantly without a redeploy.


Live Preview

When GIT_CMS_PREVIEW_KEY and GIT_CMS_PUBLIC_URL are set, the editor shows a Preview button that opens your public page in a new window or a side-by-side pane. The page re-renders live as you edit without saving.

How it works

  1. The editor opens your public page at <publicUrl><slug>?preview=true&key=<previewKey>
  2. PreviewProvider (server component) detects the query params and activates PreviewClient
  3. PreviewClient sends preview:ready back to the editor via postMessage
  4. The editor responds with preview:data containing the current PageContent
  5. Client components that call usePreviewContext() receive live data and re-render

Setup in your public app

PreviewProvider must live in your page component, not the layout. Layouts do not receive searchParams in Next.js App Router, so the preview query params would never be detected.

app/[[...slug]]/page.tsx

import { cache } from 'react'
import { getPageContent, getSettings } from '@celar/git-cms'
import { PreviewProvider } from '@celar/git-cms/preview'

type SearchParams = Record<string, string | string[] | undefined>

interface PageProps {
  params: Promise<{ slug?: string[] }>
  searchParams: Promise<SearchParams>
}

const getPageData = cache(async (
  slug: string[] | undefined,
  searchParams: SearchParams | undefined
) => {
  const cwd = process.cwd()
  const slugMapPath = path.join(cwd, 'content', 'slug-map.json')
  const settingsPath = path.join(cwd, 'content', 'settings.json')

  const includeDrafts =
    searchParams?.preview === 'true' &&
    searchParams?.key === process.env.GIT_CMS_PREVIEW_KEY

  const content = getPageContent(slugMapPath, cwd, slug, { includeDrafts })
  const settings = getSettings(settingsPath)

  return { content, settings, includeDrafts }
})

export default async function Page({ params, searchParams }: PageProps) {
  const [p, sp] = await Promise.all([params, searchParams])
  const { content } = await getPageData(p.slug, sp)
  if (!content) notFound()

  return (
    <PreviewProvider searchParams={sp}>
      <main>
        {content.blocks.map(renderBlock)}
      </main>
    </PreviewProvider>
  )
}

Making blocks preview-aware — no block changes needed

Use PreviewBlocks to handle data substitution at the page level. Your block components stay untouched.

import { PreviewProvider, PreviewBlocks } from '@celar/git-cms/preview'

export default async function Page({ params, searchParams }: PageProps) {
  const { slug = [] } = await params
  const content = readContent(slug)
  if (!content) notFound()

  return (
    <PreviewProvider searchParams={await searchParams}>
      <main className="flex-1">
        <PreviewBlocks serverBlocks={content.blocks}>
          {(blocks) => blocks.map((block) => (
            block.type !== 'hero' ? (
              <div key={`${block.type}-${block.id}`} className="container mx-auto max-w-5xl px-4 mb-8">
                {renderBlock(block)}
              </div>
            ) : renderBlock(block)
          ))}
        </PreviewBlocks>
      </main>
    </PreviewProvider>
  )
}

PreviewBlocks is a client component that reads PreviewContext and passes live blocks to its render-prop children when preview data is available, or falls through to serverBlocks for normal page loads.


Draft and publish

Every page has a status of draft or published. Status is stored in the file's YAML frontmatter:

---
title: My page
status: draft
---

Pages without a status field are treated as published (backward compatible with existing content).

Editing workflow

The editor shows two save actions:

  • Save Draft — saves the page with status: draft. The page is excluded from public reads unless you explicitly request drafts.
  • Publish — saves the page with status: published.

If you edit a published page, the status automatically flips to draft when you start typing. You must explicitly click Publish to make changes live.

A status badge (amber for draft, green for published) is shown in both the editor toolbar and the file list.

Reading published content only

getPageContent() filters out drafts by default. In preview mode, drafts are included:

import { getPageContent } from '@celar/git-cms'

// Public page — drafts excluded automatically
const content = getPageContent(slug)

// Preview — pass includeDrafts to show draft content
const content = getPageContent(slug, { includeDrafts: true })

Release manager

The release manager lets you group multiple draft pages into a named release and publish them all in a single git commit. Releases are admin-only.

How it works

  1. While editing a page, open the Page settings panel and assign the page to a release (or create a new one).
  2. Navigate to Releases in the admin header to see all releases and the pages in each one.
  3. Click Publish on a release to publish all its pages in one atomic commit and delete the release.

Releases are stored as JSON files in {contentBase}/content/releases/ in your GitHub repo. Publishing uses the GitHub Git Trees API to write all changed files and the commit in one round trip.

Release operations

From the Releases view you can:

  • Create a named release with an optional description
  • Add / remove pages — pages can be moved between releases or removed without being published
  • Rename a release
  • Publish — all pages in the release are set to published and committed atomically
  • Delete — removes the release without publishing any of its pages

Access control

Only admin users can access the Releases view and the release API endpoints.


Content is stored as markdown with YAML frontmatter in your GitHub repo. Read it server-side using the markdown utility:

import { parseMarkdown } from '@celar/git-cms/markdown'
import fs from 'fs'

const raw = fs.readFileSync('content/pages/home.md', 'utf-8')
const page = parseMarkdown(raw)
// page.title, page.slug, page.blocks[]

Navigation

Pages opt in to navigation via frontmatter:

---
title: About
slug: /about
navEnabled: true
navTitle: About Us
navOrder: 2
navParent: /company    # optional: nests under another page's slug
---

Build a nav tree server-side:

import { buildNav } from '@celar/git-cms/nav'

const nav = buildNav(['content/pages'])
// nav.items[] sorted by navOrder, nested by navParent

Field types

| Type | Description | |------|-------------| | text | Single-line text | | textarea | Multi-line text | | richtext | Tiptap rich text editor | | number | Numeric input | | boolean | Toggle | | image | Single image | | imagelist | Multiple images | | dropdown | Select — requires options: [{ label, value }] | | pagepicker | Pick a page by slug — requires contentPath |


Package exports

| Import | Use | |--------|-----| | @celar/git-cms | Types, markdown utilities, settings utilities | | @celar/git-cms/core | AdminPage, createDispatcher | | @celar/git-cms/auth | createAuth — call with a UserStore to get handlers, auth, signIn, signOut | | @celar/git-cms/adapters | VercelEdgeConfigAdapter and the UserStore interface | | @celar/git-cms/markdown | parseMarkdown, serializeToMarkdown | | @celar/git-cms/nav | buildNav | | @celar/git-cms/preview | PreviewProvider (server), usePreviewContext (client) | | @celar/git-cms/styles | Scoped admin CSS |


License

MIT