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

v0.9.0

Published

Readme

cms-renderer

cms-renderer is a library for rendering CMS-powered routes inside a Next.js app.

It provides:

  • A catch-all route component that fetches CMS routes and blocks from the CMS API and renders them with your React component registry
  • A proxy helper for mounting the CMS admin and API behind your app
  • Component-fetching utilities for headless document access
  • Docs-oriented markdown rendering and styles
  • Type exports for block registries and route params

It is meant to be embedded in an existing Next.js app. It is not a standalone website.

Install

npm install cms-renderer

This package is intended for React + Next.js applications that can reach a running CMS deployment.

Exported Entry Points

Main entry points:

  • cms-renderer/lib/renderer for catch-all route rendering
  • cms-renderer/lib/types for block and route types
  • cms-renderer/lib/proxy for proxying CMS admin and API traffic
  • cms-renderer/lib/schema for headless component-based reads
  • cms-renderer/lib/custom-schemas for fetching custom component metadata and generating Zod code
  • cms-renderer/lib/docs-markdown and cms-renderer/styles/docs-markdown.css for docs markdown rendering
  • cms-renderer/lib/refresher for client-side refresh on CMS updates
  • cms-renderer/lib/ai-preview for headless AI preview rendering

Additional utility exports:

  • cms-renderer/lib/block-renderer
  • cms-renderer/lib/block-toolbar
  • cms-renderer/lib/client-editable-block
  • cms-renderer/lib/cms-api
  • cms-renderer/lib/markdown-utils
  • cms-renderer/lib/result
  • cms-renderer/lib/trpc
  • cms-renderer/lib/image/lazy-load

Core Usage

1. Render CMS Routes in a Catch-All Next.js Page

Use the default export from cms-renderer/lib/renderer inside a catch-all route such as app/[...slug]/page.tsx.

import ParametricRoutePage from 'cms-renderer/lib/renderer';
import type { BlockComponentRegistry } from 'cms-renderer/lib/types';
import HeaderBlock from '@/components/HeaderBlock';

const registry: Partial<BlockComponentRegistry> = {
  header: HeaderBlock,
};

interface PageProps {
  params: Promise<{ slug: string[] }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function Page({ params, searchParams }: PageProps) {
  const { slug } = await params;

  return (
    <ParametricRoutePage
      registry={registry}
      cmsUrl={process.env.NEXT_PUBLIC_CMS_URL!}
      apiKey={process.env.CMS_API_KEY}
      websiteId={process.env.NEXT_PUBLIC_WEBSITE_ID}
      params={Promise.resolve({ slug })}
      searchParams={searchParams}
    />
  );
}

What the route component does:

  • Resolves the incoming path from the catch-all slug
  • Fetches the route definition from the CMS API
  • Fetches each referenced block
  • Maps each block to your registry by schema_name
  • Renders built-in and custom block types
  • Supports edit_mode and ai_preview query params

2. Register Block Components

Block components receive content and optional routeParams.

import type { BlockComponentProps } from 'cms-renderer/lib/types';

type HeroProps = BlockComponentProps<{
  headline: string;
  subheadline?: string;
}>;

export default function HeroBlock({ content }: HeroProps) {
  return (
    <section>
      <h1>{content.headline}</h1>
      {content.subheadline ? <p>{content.subheadline}</p> : null}
    </section>
  );
}

Useful types are exported from cms-renderer/lib/types:

  • BlockData
  • BlockComponentProps<T>
  • BlockComponentRegistry
  • ResolvedRouteParams

3. Configure Environment

Required for route rendering:

NEXT_PUBLIC_CMS_URL=https://cms.example.com
NEXT_PUBLIC_WEBSITE_ID=00000000-0000-0000-0000-000000000000

Optional:

CMS_API_KEY=your-api-key
ADMIN_UPSTREAM_ORIGIN=https://cms.example.com
NEXT_PUBLIC_CMS_API_URL=https://cms.example.com

ParametricRoutePage requires:

  • cmsUrl, passed as a prop
  • a website ID, passed as a prop or available through environment variables

websiteId can be passed explicitly to ParametricRoutePage, or it can be read from:

  • NEXT_PUBLIC_WEBSITE_ID
  • WEBSITE_ID
  • CMS_WEBSITE_ID

Use CMS_API_KEY when your CMS requires authenticated access.

The default schema / component exports from cms-renderer/lib/schema read NEXT_PUBLIC_CMS_API_URL on first use (lazy), not when the module loads — so importing only configureSchema never touches that env. If you call configureSchema(...) directly, pass cmsUrl explicitly instead.

If no website ID is available, ParametricRoutePage throws at runtime.

CMS Proxy

Use createCmsProxy when your app needs to proxy /admin, /api, /auth, and related CMS assets to an upstream CMS deployment. Use it from proxy.ts or middleware.ts, depending on how your Next.js app is structured.

import { createCmsProxy, cmsProxyMatcher } from 'cms-renderer/lib/proxy';
import type { NextRequest } from 'next/server';

const cmsProxy = createCmsProxy({
  upstream: process.env.ADMIN_UPSTREAM_ORIGIN,
});

export async function middleware(request: NextRequest) {
  return cmsProxy(request);
}

export const config = {
  matcher: cmsProxyMatcher,
};

The proxy helper:

  • Forwards /admin, /api, and /auth
  • Can proxy additional custom path prefixes
  • Rewrites redirects back to the current host when appropriate
  • Rewrites cookies for the current deployment host
  • Proxies admin-originated static asset requests

Headless Component Access

Use configureSchema or the default component export when you want published documents for a custom component outside the block renderer.

import { configureSchema } from 'cms-renderer/lib/schema';

const cms = configureSchema({
  cmsUrl: process.env.NEXT_PUBLIC_CMS_URL!,
  apiKey: process.env.CMS_API_KEY,
  websiteId: process.env.NEXT_PUBLIC_WEBSITE_ID,
});

const posts = await cms.name('post').fetchAll();
const post = await cms.name('post').fetchSingleById('document-id');
const frenchPosts = await cms.name('post').translation('fr').fetchAll();

Available methods:

  • fetchAll()
  • fetchSingle()
  • fetchSingleById(id)
  • translation(language)

If you use the default component export, it reads NEXT_PUBLIC_CMS_API_URL from the environment on first access. If you call configureSchema(...) directly, pass the CMS origin/base URL as cmsUrl.

Offloading reads to the dataset service

Pass datasetEndpoint to serve published document reads from the standalone dataset service (the Rust port of /api/schemas running on the translation_manager EC2 host) instead of the CMS app:

const cms = configureSchema({
  datasetEndpoint: process.env.DATASET_ENDPOINT, // e.g. https://datasets.tryprofound.com
  apiKey: process.env.CMS_API_KEY,
  websiteId: process.env.NEXT_PUBLIC_WEBSITE_ID,
});

When set, fetch* calls hit ${datasetEndpoint}/dataset/{schema} (same query params and x-api-key header as the CMS route), and cmsUrl becomes optional. datasetEndpoint is part of the shared CmsConfig, so a single config object can be spread into both configureSchema and ParametricRoutePage.

ParametricRoutePage honors datasetEndpoint too: route resolution stays on tRPC (cmsUrl), but the published content of each resolved route-param document is read from the dataset service instead of the published_content bundled in the tRPC payload — moving that read to the dedicated pool. If the dataset read fails, it falls back to the bundled content, so the dataset service is never on the critical rendering path. Preview/draft routes always use cmsUrl, since the dataset service only serves published documents.

Custom Component Code Generation

Use cms-renderer/lib/custom-schemas to fetch custom component metadata and generate typed Zod component code for your app. The helper names in this module still use Schema for compatibility.

import {
  fetchAllCustomSchemaFields,
  saveZodSchemaCode,
} from 'cms-renderer/lib/custom-schemas';

const components = await fetchAllCustomSchemaFields({
  cmsUrl: process.env.NEXT_PUBLIC_CMS_URL!,
  apiKey: process.env.CMS_API_KEY,
  websiteId: process.env.NEXT_PUBLIC_WEBSITE_ID!,
});

await saveZodSchemaCode(components, 'generated/cms-schemas.ts');

Also exported:

  • fetchCustomSchemaFields(options, schemaName)
  • buildZodSchemas(schemas)

Headless AI Preview

renderParametricRoute supports an ai_preview=<n> query param that pulls AI-generated block variants from the CMS API. When you want to render preview content headlessly — you already have the block content in hand (from a generation webhook, an agent, your own preview endpoint) and don't want a CMS round-trip, query params, or edit-mode overlays — use aiPreview from cms-renderer/lib/ai-preview.

It dispatches through the same component registry as the catch-all route, so previews match production exactly.

import { aiPreview } from 'cms-renderer/lib/ai-preview';
import type { BlockComponentRegistry } from 'cms-renderer/lib/types';
import HeroBlock from '@/components/HeroBlock';

const registry: Partial<BlockComponentRegistry> = {
  'hero-block': HeroBlock,
};

// `blocks` is content you already have — e.g. the JSON an agent generated.
export default function AiPreviewPage({ blocks }) {
  return aiPreview(blocks, { registry });
}

aiPreview(content, options):

  • content — a single block or an array of blocks, each shaped as { id?, type, content, layout? }. type selects the component from the registry; content and optional visual layout metadata are passed straight to it. id defaults to the block's index.
  • options.registry — your BlockComponentRegistry (required).
  • options.routeParams — optional resolved route params, exposed to components as routeParams.
  • options.path — optional current path, enabling path-namespaced registry keys like "/{lang}/blog ArticleBlock".

Edit overlays are always disabled and nothing is fetched, so the output is plain, public-shaped markup. Unknown block types are skipped (with a development-only warning).

Docs Markdown

cms-renderer includes a docs-oriented markdown boundary for starter apps and docs sites.

import { DocsMarkdown } from 'cms-renderer/lib/docs-markdown';
import 'cms-renderer/styles/docs-markdown.css';

export default async function Page() {
  return <DocsMarkdown content={'# Hello\n\nThis came from the CMS.'} />;
}

DocsMarkdown supports:

  • WASM-backed markdown parsing
  • Syntax-highlighted code blocks
  • Optional custom image rendering via renderImage
  • Default styling through styles/docs-markdown.css

Live Refresh

Use Refresher in client components to refresh the current route when CMS content changes. You can also provide onInvalidate to run app-specific invalidation before router.refresh().

'use client';

import { Refresher } from 'cms-renderer/lib/refresher';

export function CmsLiveRefresh() {
  return (
    <Refresher
      cmsUrl={process.env.NEXT_PUBLIC_CMS_URL!}
      websiteId={process.env.NEXT_PUBLIC_WEBSITE_ID!}
      apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY}
    />
  );
}

Edit Mode

When the page is rendered with edit_mode=true or edit_mode=1, the renderer enables editable wrappers and block toolbar behavior for CMS iframe editing.

In normal public traffic, you do not need to do anything special for edit mode. This mode is intended for CMS-integrated editing flows, not for public pages.

Contributor Notes

Useful package scripts:

bun run build
bun run check-types
bun run lint
bun run test
bun run test:coverage

dist/ is generated by tsup during build and publish.