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

@ohnejs/client

v0.3.0

Published

Reads pages from an ohne CMS and wires its live preview, for any frontend.

Readme

@ohnejs/client

Reads the pages of an ohne CMS from any website: Nuxt, Next, React, Svelte, Astro, or plain HTML. It has no dependencies and runs wherever fetch does.

Install

pnpm add @ohnejs/client

Reading a page

import { createOhne } from '@ohnejs/client';

const ohne = createOhne({ api: 'https://api.example.com' });
const page = await ohne.resolve('/blog/hello');

page.kind is page, redirect, or notFound. A page carries the record, its seo, and its alternates in other locales. ohne.renderHead(page) turns those into <head> tags.

Pass the path with its query, as location.pathname + location.search, so a redirect that keeps the query carries it to its target.

Reading site data

Not everything is a page. A menu, a footer, or a list of posts lives in records you read by collection name:

const settings = await ohne.read('Settings');
const post = await ohne.read('Posts', uuid);
const posts = await ohne.list('Posts', { order: ['-publishedAt'], page: 2, perPage: 10 });

read with only a name reads a singleton, a collection that holds one record. With a UUID it reads that record, or undefined when a visitor may not see it. list answers one page: { records, total, page, perPage, lastPage }, 20 records by default and at most 100.

You read as a visitor does, so only what the collection's read access shows comes back. Uploads carry absolute URLs, and a record with a page carries its path.

list filters with where, in the shape of the URL where, and select narrows the fields:

const featured = await ohne.list('Posts', {
  where: { featured: true, views: { atLeast: 100 } },
  select: ['title', 'image'],
  locale: 'de',
});

UUID and path come whatever select says.

Forms

A contact form, a newsletter sign-up, or a comment stores a record as a visitor:

const result = await ohne.create('Messages', { name, email, message });

if (!result.ok) showErrors(result.message, result.errors);

It works for a collection whose create is public. A refusal the visitor can fix, like an invalid email or a failed spam check, resolves with the API's message and a message per field in errors. Any other failure throws. A spam check's token travels as a header: ohne.create('Messages', input, { headers: { 'recaptcha-token': token } }).

Types

Your CMS describes its pages in an ohne.ts file. Fetch it from the API, or let ohne dev keep it current with the cms.types setting:

curl -fsS https://api.example.com/cms/types.ts -o ohne.ts

Pass its CMS type to createOhne, and checking page.collection types page.record:

import { createOhne } from '@ohnejs/client';
import type { CMS } from './ohne.ts';

const ohne = createOhne<CMS>({ api: 'https://api.example.com' });
const page = await ohne.resolve('/about');

if (page.kind === 'page' && page.collection === 'Pages') {
  console.log(page.record.title);
}

CMS describes everything your CMS resolves; Page, Block, Related and Upload name its parts. It types read and list too: a collection name, select, order, and where are checked against Records, and the answer narrows to the fields you select. A types file from a cms before 0.4.0 names no Records, so read and list refuse every name until you fetch it again.

Creatable types create the same way: a misspelled or missing field fails to compile.

Live preview

In the dashboard's editor your page gets a preview token. Pass it on, and connect in the browser:

import { connect, createOhne } from '@ohnejs/client';

const ohne = createOhne({ api: 'https://api.example.com' });
const token = ohne.token(location.href);
const page = await ohne.resolve(location.pathname + location.search, { token });

if (token) connect({ api: 'https://api.example.com', onData: render });

connect does nothing outside the editor's frame. Without onData or onRefresh, it fetches the page again and swaps its <body>.

Rich text

A richText field holds a tree of blocks and runs, never HTML. richTextToHTML renders it and escapes every text and address on the way, so nothing in the value can inject markup:

import { richTextToHTML } from '@ohnejs/client';
import type { Block } from './ohne.ts';

const hero = (fields: Block<'Hero'>['fields']) => richTextToHTML(fields.body);

A link to another page carries its href when the reader can open that page, and renders as plain text otherwise. To keep such links inside a client-side router, let interceptLinks catch the clicks:

import { interceptLinks } from '@ohnejs/client';

const dispose = interceptLinks(document, (path) => router.push(path));

It leaves alone links to other origins, links that open a new tab, downloads, and clicks with a modifier key held. Call dispose on unmount.

Webhooks

verifyWebhook(request, secret) checks a webhook the cms sent and answers its event, or null.

Docs

Your website explains reading pages and the live preview. Frameworks shows the whole loop for Nuxt, Next, React, and plain HTML.