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

cms-renderer

v2.0.5

Published

Median CMS API wrapper — render React typed websites using Median CMS

Readme

cms-renderer

Render a Median website. One class reads it, two components render it, and a Markdown renderer handles rich text.

import { Median, ParametricPage, ParametricPreview } from 'cms-renderer';
import { Markdown } from 'cms-renderer/markdown';
import 'cms-renderer/markdown.css';

Median (server or browser)

const median = new Median({
  apiKey: process.env.MEDIAN_API_KEY, // in a browser, only a read-only (content_read) key
  datasetEndpoint: process.env.DATASET_ENDPOINT!,
  websiteId: process.env.VITE_WEBSITE_ID!,
  registry, // block type → component; `"/docs/{slug} header"` overrides a type on matching paths
  revalidate: 60, // seconds a published page stays in memory (false: until invalidate(), 0: never)
  fetchOptions: {}, // extra fetch options for published reads
});

| Method | What it does | |---|---| | getPage(path, { preview }) | One request: the page's blocks (type, content, layout) and the documents its URL is bound to. Published pages come from /published_page and are cached (concurrent reads share one request); preview: true reads the draft from /draft_page, never cached. null when there is no page. | | resolveComponent(path, { preview }) | getPage, with each block matched to its registry key (block.component); unregistered blocks are dropped. Plain data: return it from a loader. | | listPages() | Every published URL, for static builds and sitemaps. | | documents(schema).fetchAll() / fetchById(id) / fetchByIds(ids) | A component's documents; .translation('fr') for translated content. | | schemas() / generateSchemas() | The website's components, and Zod schema source (with inferred types) for them. | | invalidate(path?) | Drop cached published pages, e.g. after a publish (see Publishes). |

Image fields arrive with their URLs resolved; render them with your own image component.

In a browser, listPages, getPage, documents and schemas work the same (the page service allows cross-origin reads). The key is visible to visitors there, so use a read-only one, and keep preview reads on the server unless drafts may be public.

ParametricPage / ParametricPreview

Render a resolved page. They never fetch and never see the key.

// TanStack Start
loader: () => getPage({ data: path }), // a server function calling median.resolveComponent(path)
component: () => <ParametricPage page={Route.useLoaderData()} registry={registry} />,

// A CMS preview route: the draft, plus the edit overlay the CMS uses to select and edit blocks
<ParametricPreview page={draft} registry={registry} cmsUrl="https://app.mediancms.com" />

Each component receives content, layout, routeParams, language and path.

Next.js: prerendered pages plus a draft preview

// app/[[...slug]]/page.tsx: every published page, prerendered at build
export const dynamicParams = false;
export async function generateStaticParams() {
  return (await median.listPages()).map((path) => ({ slug: path.split('/').filter(Boolean) }));
}
export default async function Page({ params }) {
  const { slug = [] } = await params;
  const page = await median.resolveComponent(`/${slug.join('/')}`);
  if (!page) notFound();
  return <ParametricPage page={page} registry={registry} />;
}

// app/cms-preview_/[[...slug]]/page.tsx: the live draft, on every request
export const dynamic = 'force-dynamic';
export default async function Preview({ params }) {
  const { slug = [] } = await params;
  const page = await median.resolveComponent(`/${slug.join('/')}`, { preview: true });
  if (!page) notFound();
  return <ParametricPreview page={page} registry={registry} cmsUrl="https://app.mediancms.com" />;
}

The CMS Template Builder and Canvas load /cms-preview_/<path>. Reference fields arrive filled with the referenced (published) document's fields. To pick up publishes without a rebuild, see Publishes.

layout: the block's Appearance settings

What an editor sets under Appearance in the CMS block editor arrives as the block's layout (BlockLayout), never in content. Every setting is optional: when it is absent, keep the component's own default.

| Field | Values | |---|---| | alignment | 'left', 'center', 'right' (read it with normalizeAlignment) | | backgroundColor | hex color, e.g. #f5f0e8 | | textColor | hex color | | backgroundImageFit | 'cover' (fill, may crop) or 'contain' (whole image) | | backgroundImagePosition | 'top', 'center', 'bottom' |

export default function Hero({ content, layout }: BlockComponentProps<HeroContent>) {
  return (
    <section style={{ backgroundColor: layout?.backgroundColor, color: layout?.textColor }}>
      <div style={{ backgroundSize: layout?.backgroundImageFit ?? 'cover' }} />
    </section>
  );
}

generateSchemas() emits the same shape as blockLayoutSchema / BlockLayout.

Publishes: keeping a site current

The page service streams every change to what published pages serve (publishes, rollbacks, published documents; never drafts) as Server-Sent Events from /content-changes. Each event's id is the website's published version, so a listener that reconnects with Last-Event-ID hears at once about anything it missed.

Listen from the server process, not the browser. A site that only reacts in an open tab stays stale whenever nobody has it open. Start one subscription when the server starts, keep it for the life of the process, and refresh from there:

| Method | What it does | |---|---| | contentChanges({ since, signal }) | The raw stream (fetch response). since: the last version you saw. | | relayContentChanges(request) | A route handler relaying the stream to browsers, keeping the key on the server. | | watchContentChanges(url, { since, onChange }) | Browser side of the relay: re-render open tabs. Optional; the server listener is what keeps the site current. |

// The loop every framework below runs once per server process.
let since: string | null = null;
for (;;) {
  try {
    const response = await median.contentChanges({ since });
    for await (const event of sse(response.body)) {   // parse `id:` / `event:` / `data:`
      since = event.id ?? since;
      if (event.event !== 'ready') refresh();          // `ready` only reports the version
    }
  } catch {}
  await sleep(backoff());                              // reconnect: 1s, doubling to 30s
}
  • Next.js (src/instrumentation.ts): register() starts the loop when NEXT_RUNTIME === 'nodejs'. Next only allows revalidatePath inside a request, so refresh() POSTs to the site's own route handler, which calls revalidatePath('/', 'layout'); protect that route with a token. Keep a page revalidate (e.g. 60 s) as a fallback.
  • TanStack Start / Nitro: a Nitro server plugin starts the loop and stops it on the close hook; refresh() calls median.invalidate() (and rebuilds any cached pages). The docs app does this in apps/docs/server/plugins/docs-content.ts.
  • Serverless hosts (e.g. Vercel functions) suspend an instance between requests, so a background listener only runs while one is alive. On those, the page revalidate fallback is what bounds staleness; a long-running server (Node or Nitro on a container) keeps the stream open continuously.

createCmsProxy (cms-renderer/proxy)

Serves the Median admin panel at the site's own /admin: forwards /admin and the requests it makes (its files, server functions and API routes) to the CMS, which recognises the website by x-forwarded-host. Takes a Request, returns the CMS's Response, or null for the site's own paths.

const proxy = createCmsProxy(); // { upstream } for a self-hosted CMS
// Next.js src/proxy.ts:      (await proxy(request)) ?? NextResponse.next()
// TanStack Start middleware: (await proxy(request)) ?? next()

Markdown (server)

<Markdown content={markdown} renderImage={...} renderCodeAction={...} renderCard={...} />

The parser is WebAssembly embedded in the package, so there is no file to serve. Call initMarkdown(url | Response) first to stream-compile it from elsewhere instead. After upgrading md4w, run bun run embed-md4w.