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

@drigosihombinga/astro-scribe

v0.3.7

Published

Scribe — a git-backed CMS for Astro. Staff edit content on the live page, see it update as they type, and publish with one button.

Readme

Scribe

A git-backed CMS for Astro. Non-technical staff edit content on the live page, watch it update as they type, and publish with one button. The repository is the truth; the server keeps nothing.

npm install @drigosihombinga/astro-scribe

Status: 0.3.0, pre-release. The architecture runs in production on one site; the public API is not yet stable.


What it is

Most CMSs give you an admin panel that renders an approximation of your site. Scribe has no preview renderer. When you type, the server re-renders the real page with your draft applied and morphs the result into the DOM you are looking at. Fidelity is exact because there is nothing to keep in sync.

Content lives in your git repository. Every save is a commit on a shared draft branch (scribe/draft), merged per field so two teachers editing one document never see a conflict. Publishing squashes the draft branch onto main as one commit; your CI builds and deploys the static site from there. The server holds no state between requests, uses only Web APIs (plus node:async_hooks), and runs on any Astro adapter.

Requirements

  • Astro 7, @astrojs/react (the editor is a React island), Node ≥ 22.12
  • Zod 4 — Scribe reads .meta() through z.toJSONSchema() to build forms
  • A GitHub repository (other providers implement the same nine-method interface)

Two deployments from one codebase

A prerendered route cannot also be rendered on demand, and previews must render the real pages with drafts applied — so the same commit is built twice by the same CI:

| Deployment | output | Serves | | --- | --- | --- | | Public — example.com | static | the site; carries the scribe-build meta | | Editor — studio.example.com | server | the site again (for preview), the Studio UI, /studio/* |

The public build carries no Scribe route, middleware or script. astro dev always runs as the editor, against an in-memory repository when none is configured.

Setup

1. Add the integration, switching output on one variable.

// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
import react from '@astrojs/react';
import scribe from '@drigosihombinga/astro-scribe';

const editor = process.env.SCRIBE_MODE === 'editor';

export default defineConfig({
  output: editor ? 'server' : 'static',
  adapter: editor ? node({ mode: 'standalone' }) : undefined,
  integrations: [react(), scribe()],
});

2. Describe your content and name your repository.

// src/scribe.config.ts
import { z } from 'zod';
import { defineScribeConfig, editor } from '@drigosihombinga/astro-scribe';

const Article = z.object({
  title: z.string(),
  img: editor(z.string(), { input: 'media', label: 'Cover photo' }),
  body: editor(z.string(), { input: 'markdown', rows: 16 }),
});

export default defineScribeConfig({
  collections: [
    { name: 'articles', label: 'News', kind: 'list', schema: Article, formSource: 'schema' },
  ],
  pages: { '/': 'Home', '/news': 'News' },
  repository: { provider: 'github', owner: 'acme', repo: 'site', branch: 'main', draftBranch: 'scribe/draft' },
  media: { dir: 'public/media', maxBytes: 5_000_000 },
});

3. Put the build sha on the public site, so the editor can tell when a publish is live:

<meta name="scribe-build" content={import.meta.env.SCRIBE_BUILD_SHA ?? ''} />

and have CI export SCRIBE_BUILD_SHA=$GITHUB_SHA into both builds.

4. Set the environment on the editor host.

| Variable | Purpose | | --- | --- | | SCRIBE_SESSION_SECRET | 32+ chars; encrypts the session cookie | | SCRIBE_PUBLIC_ORIGIN | the editor host, https://studio.example.com; required behind a TLS proxy | | SCRIBE_SITE_ORIGIN | the public host, read for the scribe-build meta | | SCRIBE_GITHUB_CLIENT_ID / _SECRET | GitHub OAuth app; callback <PUBLIC_ORIGIN>/studio/auth/github/callback | | SCRIBE_GOOGLE_CLIENT_ID / _SECRET | Google OAuth client; callback <PUBLIC_ORIGIN>/studio/auth/callback | | SCRIBE_GIT_TOKEN | fine-grained PAT (Contents read/write) held by a dedicated editorial account; used for Google sign-ins | | SCRIBE_GIT_COMMITTER_NAME / _EMAIL | the editorial account's identity (default Scribe <scribe@localhost>) | | SCRIBE_EDITORS | comma-separated addresses that may draft | | SCRIBE_PUBLISHERS | comma-separated addresses that may draft and publish |

Either sign-in method alone is enough. SCRIBE_ALLOWED_EMAILS is still read as an alias for SCRIBE_EDITORS for one release. Empty lists fail closed: nobody publishes until a publisher is named.

Sign-in and roles

  • GitHub — the teacher's own OAuth token is sealed into the httpOnly cookie and commits are made as them. A token that can push to the repository makes its owner an editor even when absent from the lists; publishing still requires SCRIBE_PUBLISHERS.
  • Google — identity only. Commits use the editorial token; the teacher is the author and the commit carries Co-authored-by.

Tokens never reach browser JavaScript; GET /studio/session returns { email, name, role } and nothing else. Roles are re-read from the environment on every request.

How it works

| Concern | Approach | | --- | --- | | Forms | Zod schemas → JSON Schema → a form tree. .meta({ editor }) carries labels and widgets. | | Drafts | One shared branch. Each save is a commit with a Scribe-Fields: trailer naming the fields it touched; the browser keeps only a typing buffer (IndexedDB). | | Merging | Three-way, per field. My changed fields win; everything else follows the branch. A field a colleague also changed is written as mine and shown as a chip — Changed by Pak Andi · use their version — the only surface a conflict ever has. | | Preview | POST /studio/preview { route, values } renders the page with those documents overlaid, then idiomorph morphs it in. No repository call. | | Publish | Publishers squash the draft branch onto main in one commit (developer fixes on main are folded in, chosen fields can keep their live value), then the draft branch is reset with a Scribe-Base: marker. Both ref updates are compare-and-swap. | | Status | /studio/status combines CI checks for main with the public site's scribe-build meta: building, live, or failed. | | Media | An upload rides inside the next save, named by its content hash, so the photo and the field that points at it land in one commit. The editor host serves draft-line photos from the branch; the public build ships them from public/media. | | Portability | npm run lint:portability forbids every node:* import but async_hooks under src/ and routes/. |

The editor

Nothing is added to your markup. When a signed-in editor loads any page, a bar appears along the bottom edge and every section the CMS knows about gets a hover outline with its name; click one and an inspector docks on the right with that document's fields, grouped into sections. ⌘K opens a browser of everything else, grouped by page, with Write a new … under each list collection. Review is a sheet that rises from the bar: every pending field with who changed it and when; publishers can keep the live version of any field and publish the rest, editors see the same list under Waiting to be published.

Mark a section so the editor can find it on the page:

<section data-scribe="home/latest-news">…</section>

Widgets, labels, locations

Fields render through a registry (registerWidget), two widgets ship — bilingual and markdown (Tiptap, optional peers) — and every collection accepts documentLabels, fieldLabels and locate so what staff see is named in their language and a click goes somewhere. See the docs in the repository for the full reference.

What changed in 0.3.5

Fixes from the first content pass a real editor made through the browser (see docs/superpowers/specs/2026-08-30-scribe-report-fixes-design.md in the repository):

  • The draft line is what a signed-in editor sees on every page. The middleware overlays pending documents on any GET that carries a session; the bar's Draft | Published switch writes the <cookiePrefix>view cookie and reloads. Anonymous visitors are untouched.
  • Review opens in a blink. /studio/review and the page overlay are memoised under the two branch heads (cachedByHeads), so a review costs two head lookups instead of a tree walk.
  • Rows diff as rows. Deleting the fifth of eight list rows reads as 3 rows removed, not as edits to rows 5–8. Publishers keep or drop whole rows; editors put back or withdraw their own.
  • Editors can withdraw one field: DELETE /studio/draft?path=…&field=…[&row=added|removed].
  • The review sheet says who you are and what you can do, groups documents by page, marks changes older than a day, shows a skeleton while loading, locks while publishing, and ends on a confirmation naming the commit and the branch (with a note under astro dev that the local copy does not change).
  • Honest save states: Not saved yet while a change is typed but not sent, ⌘S to send it now, and the browser asks before you leave mid-save.
  • /studio is a page: documents by page with pending counts, the last commit on the publish branch, the build state, your role and the sign-in methods.
  • Locales. scribe({ locales: { default: 'id', prefixes: { en: '/en' } } }) keeps the language you are reading while the editor navigates and adds an ID | EN switch to the inspector.
  • Input hints for data-driven forms. A collection with formSource: 'data' may set fieldInputs: { 'story.img': 'media' } so a plain string leaf renders as a photo picker.
  • Accessible names on rows, palette options and the bar's buttons; the decorative drag glyph is gone (nothing reorders yet).
  • The status endpoint returns branches and repoWebUrl; publish returns branch.

GitHub sign-in needs SCRIBE_GITHUB_CLIENT_ID / _SECRET; without them only the Google button renders.

Development

apps/example runs the whole editor against an in-memory repository seeded from its content directory: npm run example:dev, sign-in bypassed. npm run verify runs the portability lint, typecheck, tests and build. The provider contract runs against GitHub when SCRIBE_TEST_GITHUB_TOKEN, SCRIBE_TEST_REPO and SCRIBE_TEST_BRANCH are set.

Licence

MIT. Portions adapted from Nuxt Studio (MIT); see NOTICE.