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

scribe-cms

v0.0.12

Published

Typed, file-based CMS for multilingual MDX sites

Downloads

1,788

Readme

Scribe

Typed, file-based CMS for multilingual MDX. English source files on disk, locale translations in SQLite, Zod schemas, Gemini-powered translation, and a framework-agnostic runtime API.

Scribe has no framework dependency — it reads files and SQLite in-process and works with any Node-based stack (Next.js, Astro, Remix, SvelteKit, a static-site script, …). Examples in these docs use Next.js, but nothing about Scribe is Next-specific.

Docs: Getting started · Configuration · Writing content · Runtime API · Translation

Install

pnpm add scribe-cms zod better-sqlite3

Set GEMINI_API_KEY when using scribe translate.

Quickstart

1. Define the config

// scribe.config.ts (at your project root)
import { z } from "zod";
import { defineConfig, defineContentType, field } from "scribe-cms";

const blogSchema = z.object({
  title: field.translatable(z.string().min(1)),
  description: field.translatable(z.string().min(50)),
  author: field.relation("author"),
  tags: field.structural(z.array(z.string()).default([])),
});

const authorSchema = z.object({
  name: field.structural(z.string().min(1)),
});

export default defineConfig({
  rootDir: ".", // relative to this file (CLI) / process.cwd() (runtime)
  // contentDir: "content"            (default)
  // store: ".scribe/store.sqlite"     (default)
  locales: ["en", "fr"],
  // defaultLocale: "en"              (default)
  types: [
    defineContentType({
      id: "blog",
      path: "/blog/{slug}",            // routable: gets URLs, sitemap, redirects
      schema: blogSchema,
      slugStrategy: "localized",       // translated slugs per locale (default: "fixed")
      orderBy: "-publishedAt",         // default sort for list()
    }),
    defineContentType({
      id: "author",                    // no path: reference-only type
      contentDir: "authors",           // default would be "author"
      schema: authorSchema,
    }),
  ],
});

Content lives in content/blog/*.mdx and content/authors/*.mdx. The file name is the EN slug. Frontmatter is validated against the schema; the built-in fields publishedAt, updatedAt, noindex, and canonicalPath are available on every type without declaring them. Redirects live in content/<type>/_redirects.json.

2. Field markers

  • field.translatable(schema) — sent to the translator for each locale.
  • field.structural(schema) — EN-only; merged from EN into every locale document.
  • field.relation(typeId, options?) — EN slug reference(s) to another type. Constraints go in the options (not chained Zod methods): field.relation("glossary", { multiple: true, max: 8, optional: true }). Validated by scribe validate, dereferenced with related().

3. Read content

import { createScribe } from "scribe-cms/runtime"; // bundler-safe entry; plain "scribe-cms" works in scripts
import config from "./scribe.config";

const scribe = createScribe(config);

// Lists & lookups
scribe.blog.list("fr");                       // sorted docs for a locale
scribe.blog.get("my-post");                   // exact slug lookup, no fallback
const r = scribe.blog.resolve("my-post", "fr"); // cross-locale slug fix + EN fallback
// r = { document, actualLocale, shouldRedirectTo?, canonicalPath? }

// Routing helpers
scribe.blog.staticParams();                   // all { locale, slug } pairs to prerender
scribe.blog.alternates(doc);                  // hreflang map: locale → path
scribe.blog.translation(doc, "fr");           // the same doc in another locale (or null)
scribe.blog.url(doc.slug, "fr");              // path from the type's template

// Relations (fully typed from the schema)
scribe.blog.related(doc, "author");           // AuthorDoc — non-null, validated at build time

// Sitemap
await scribe.sitemap({ baseUrl: "https://example.com" }); // entries with hreflang alternates

Typed accessors (scribe.blog, scribe.author, …) and related() return types are inferred from the config — no codegen.

4. Translate & validate

scribe status                  # EN docs + translation coverage
scribe validate                # schemas, relations, redirects, sqlite consistency
scribe translate --locale fr   # translate stale/missing pages (Gemini)
scribe translate --preset active
scribe history blog my-post fr # revision timeline
scribe studio                  # read-only local admin UI

Translations are stored in .scribe/store.sqlite keyed by a hash of the EN translatable content, so scribe translate only re-translates what changed. Commit .scribe/ — do not add it to .gitignore.

Framework integration

Scribe runs anywhere Node does — see Runtime API → Framework integration. The short version:

  • Use scribe-cms/runtime in app code, scribe-cms in build scripts/CLI.
  • Keep better-sqlite3 (a native module) external to your bundler (e.g. Next.js serverExternalPackages, Vite ssr.external).
  • Gate builds: "build": "scribe validate && <your framework build>".
  • Redirects: buildAllContentRedirects(project) produces { source, destination, permanent } rules from _redirects.json and cross-locale slugs — map them to your framework's redirect config.

Site & examples: scribe.genlook.app · Example Next.js app