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

@coniferous/docs-react

v0.5.4

Published

React hooks, renderer, and opinionated Mantine layout for building doc sites on top of @coniferous/docs-core.

Readme

@coniferous/docs-react

React hooks, a renderer, and an opinionated Mantine layout for shipping doc sites. Built on top of @coniferous/docs-core.

@coniferous/docs-react            — hooks + <DocRenderer> + unstyled defaults
@coniferous/docs-react/layout     — <DocsLayout> + Mantine-styled element defaults

Import whichever piece you want — the layout subpath is only needed if you want the pre-styled 3-pane shell.

Install

npm install @coniferous/docs-react @coniferous/docs-core
# If you plan to use the layout:
npm install @mantine/core @tabler/icons-react

Quick start — zero styling

import { createStaticLoader } from "@coniferous/docs-core/loaders/static";
import {
  DocRenderer,
  DocsProvider,
  useDocContent,
} from "@coniferous/docs-react";

import getStarted from "./docs/get-started.md?raw";
import trackWork from "./docs/track-work.md?raw";

const loader = createStaticLoader({
  "get-started": getStarted,
  "track-work": trackWork,
});

const config = {
  sections: [
    { title: "Start", pages: ["get-started", "track-work"] },
  ],
};

function App({ slug }: { slug: string }) {
  return (
    <DocsProvider loader={loader} config={config} activeSlug={slug}>
      <Page slug={slug} />
    </DocsProvider>
  );
}

function Page({ slug }: { slug: string }) {
  const { parsed } = useDocContent(slug);
  if (!parsed) return <p>Not found</p>;
  return <DocRenderer ast={parsed.ast} />;
}

"It just works" — the opinionated layout

import { DocsProvider, DocRenderer } from "@coniferous/docs-react";
import {
  DocsLayout,
  DocsPrevNext,
  mantineComponents,
} from "@coniferous/docs-react/layout";
import { Link, useParams } from "react-router-dom";

export default function DocsPage() {
  const { slug = "get-started" } = useParams();
  return (
    <DocsProvider loader={loader} config={config} activeSlug={slug}>
      <Inner slug={slug} />
    </DocsProvider>
  );
}

function Inner({ slug }: { slug: string }) {
  const { parsed, frontmatter } = useDocContent(slug);
  if (!parsed || !frontmatter) return null;
  return (
    <DocsLayout
      ast={parsed.ast}
      pageTitle={frontmatter.title}
      activeSlug={slug}
      LinkComponent={Link}
      linkHrefProp="to"
    >
      <DocRenderer ast={parsed.ast} components={mantineComponents} />
      <DocsPrevNext slug={slug} />
    </DocsLayout>
  );
}

For a full reference integration — including Shiki syntax highlighting, HMR-aware Vite loader, and the Spruce overlay editor — see website/src/DocsPage.tsx.

Customization model

Two complementary knobs:

  1. Component map — per-element overrides (<DocRenderer components={{ h2, callout, a, ... }} />). Same pattern as react-markdown. Unset entries fall back to our defaults.
  2. Headless hooks — if you want full control of markup, skip <DocsLayout> and build your own with useDocContent, useToc, useDocsSearch, useDocsConfig, useDocsPrevNext, and useActiveHeading.

We deliberately don't expose slot overrides — they always leak constraints into the layout. Either restyle one element (component map) or build your own shell (hooks).

SEO + agent discoverability

Two drop-in components layer on top of @coniferous/docs-core's pure helpers.

<DocSeo> — per-page head tags

import { buildSeoMetadata } from "@coniferous/docs-core";
import { DocSeo } from "@coniferous/docs-react";

function Page({ slug }: { slug: string }) {
  const { parsed, raw } = useDocContent(slug);
  const page = useDocsConfig().getPage(slug);
  if (!parsed || !page) return null;

  const metadata = buildSeoMetadata(page, {
    siteConfig: SITE_CONFIG,      // your own { baseUrl, siteName, titleTemplate, … }
    body: raw ?? undefined,       // enables body-paragraph description fallback
  });

  return (
    <>
      <DocSeo metadata={metadata} />
      <DocRenderer ast={parsed.ast} />
    </>
  );
}

<DocSeo> is imperative — it renders nothing in the DOM tree. An effect mutates document.head to match the provided SeoMetadata, updating the existing <title> / <meta> / <link> / JSON-LD nodes in place. This is the right shape for a site that prerenders per-page head on disk and then hydrates: the prerendered tags stay authoritative, <DocSeo> just keeps them in sync on client-side route changes. Zero duplicates post-hydration.

If you need the imperative sync without the component wrapper (e.g. in a hook), import syncDocumentHead directly:

import { syncDocumentHead } from "@coniferous/docs-react";

useEffect(() => syncDocumentHead(metadata), [metadata]);

Framework-native integration

<DocSeo> targets client-side-navigation-aware React apps (React Router, TanStack Router, wouter, etc.). If you're on a framework that owns head rendering end-to-end, skip <DocSeo> and feed buildSeoMetadata's output into the framework's metadata API instead:

| Framework | Pattern | | --- | --- | | Vite + SSG | Use prerenderSite from @coniferous/docs-core/prerender for build + <DocSeo> for client-side nav. Reference: Spruce marketing site — website/scripts/prerender.mjs, website/src/DocsPage.tsx. | | Next.js (App Router) | Feed buildSeoMetadata into generateMetadata(). Skip <DocSeo> — Next's metadata API handles client-side nav natively. See the Next recipe in @coniferous/docs-core/README.md. | | Astro | Spread buildSeoMetadata into the <head> slot, or use renderSeoToHtml + set:html for the complete head. See the Astro recipe in @coniferous/docs-core/README.md. | | Remix / TanStack Start | Feed buildSeoMetadata into the route loader's meta export. |

<CopyMarkdownButton> — "Copy as markdown"

import { CopyMarkdownButton } from "@coniferous/docs-react";

// In a page
<CopyMarkdownButton />
// Or with an explicit slug / label
<CopyMarkdownButton slug="get-started" label="Copy source" />

The button pulls the current slug from <DocsProvider> (via activeSlug), reads the raw markdown with getRawMarkdown, strips the frontmatter by default, and writes it to the clipboard. It's a plain <button> with inline styles — no UI-library dependency — so it drops into any layout.

DocsLayout renders one by default in the content toolbar. Override via the toolbar prop:

<DocsLayout /* … */ toolbar={false}>                {/* hide */}
<DocsLayout /* … */ toolbar={<MyOwnToolbar />}>     {/* replace */}

Replace with a custom ReactNode when you want to add "Open in Claude" / "Open in ChatGPT" buttons or anything else alongside the copy action.

Syntax highlighting

<DocRenderer ast={parsed.ast} shiki />

Or customize the theme / languages:

<DocRenderer
  ast={parsed.ast}
  shiki={{ theme: "github-light", langs: ["rust", "typescript"] }}
/>

Shiki is lazy-loaded on first render, so the grammars don't pay the cost until you actually encounter a code block.

Blog UI — @coniferous/docs-react/blog

Mantine components for a blog built on the framework-agnostic helpers in @coniferous/docs-core (getBlogPosts, resolveBlogConfig, …). They're brand-neutral: chrome, the router link component, and the no-cover placeholder are all props, so you wrap them in your own header/footer.

  • <BlogCardGrid posts hrefFor LinkComponent coverPlaceholder /> — the index. A responsive grid of fixed-height cards: cover band (image or your placeholder), date + reading time, a 2-line-clamped title, a 3-line-clamped excerpt, and an author byline (photo when present, initials otherwise). <BlogCard> is exported too if you want a single card.
  • <BlogArticle post ast sourceFile dev shiki /> — a turnkey post body: <BlogPostHeader> (title, byline, date, cover hero) + a <DocRenderer> for the markdown, wrapped in a <LayoutProvider> so the Mantine component map works without the 3-pane docs chrome. sourceFile makes the Spruce overlay work on blog files for free. Pass shiki to enable (slim, fine-grained) syntax highlighting. <BlogPostHeader> is exported separately if you compose the body yourself.
  • useBlogPost(slug, opts) + <BlogArticleBySlug slug> — the turnkey path. useBlogPost resolves { post, page, ast, seo } from the loader in context (wraps useDocContent + getBlogPosts().find + resolveBlogConfig().pages.find + blogPostSeo), returning null for a 404. <BlogArticleBySlug> renders straight from a slug, hiding the ast null-juggling. siteConfig is an explicit option (the context doesn't carry it) — supply it to get seo.
import { createAuthorResolver, getBlogPosts } from "@coniferous/docs-core";
import { DocsProvider, DocSeo } from "@coniferous/docs-react";
import { BlogCardGrid, BlogArticleBySlug, useBlogPost } from "@coniferous/docs-react/blog";

const getAuthor = createAuthorResolver(AUTHORS, "team");

// Index page
function BlogIndex() {
  const posts = getBlogPosts(loader, { excludeDrafts: import.meta.env.PROD, getAuthor });
  return <BlogCardGrid posts={posts} LinkComponent={Link} linkHrefProp="to" />;
}

// Post page — turnkey: slug in, article out. `useBlogPost` reads the loader
// from context, so it must run INSIDE <DocsProvider> — hence the split.
function BlogPost({ slug }) {
  return (
    <DocsProvider loader={loader} config={{ sections: [] }} activeSlug={slug}>
      <PostBody slug={slug} />
    </DocsProvider>
  );
}

function PostBody({ slug }) {
  const result = useBlogPost(slug, { siteConfig, getAuthor });
  if (!result) return <NotFound />;
  return (
    <>
      <DocSeo metadata={result.seo!} />
      <BlogArticleBySlug slug={slug} options={{ siteConfig, getAuthor }} shiki />
    </>
  );
}

See BLOG-INTEGRATION.md for the full end-to-end wiring — content store / HMR, prerender (posts and index from one prerenderSite call via the index field), the render(url, ctx) contract, and the dedupe + ssr.noExternal build config.

Theming

The blog components read the same --spruce-docs-* CSS variables as the docs layout — surface (--spruce-docs-bg-subtle), border (--spruce-docs-border / --spruce-docs-border-hover), and foreground (--spruce-docs-fg, -fg-muted, -fg-faint). Import @coniferous/docs-react/styles.css and the cards/header automatically match your docs; override accent/accentSoft (and any other variable) once and both surfaces follow. Each value has an inline fallback, so a card never renders Mantine's default gray if the stylesheet is missing — it just falls back to a transparent surface. The no-cover band is yours to style via coverPlaceholder.

Prose typography is driven by three variables — --spruce-docs-prose-font-size (1rem), --spruce-docs-prose-line-height (1.7, unitless), and --spruce-docs-prose-paragraph-gap (1em, em-relative) — so redefining the font size reflows the whole vertical rhythm. <BlogArticle> adds a .spruce-docs-blog scope that bumps the size to 1.0625rem and sets a --spruce-docs-prose-measure (68ch) reading column; pass maxWidth to override the measure per article. Override any token at :root, the blog scope, or your own container — never !important.

Steps marker (:::steps number chip) uses a dedicated --spruce-docs-step-marker-bg / --spruce-docs-step-marker-fg pair (default: glyph on a solid --spruce-docs-accent disk, contrast-safe in both themes) so it stays legible regardless of how you tune --spruce-docs-accent-fg.

Build the static HTML + RSS + merged sitemap/llms with prerenderSite({ sets }) + buildRss from @coniferous/docs-core — see that package's "Blog support" section.

Overlay editing

useDocContent automatically runs stampSourceAttrs from @coniferous/docs-core on every parsed tree, so the source-map attributes (data-source-file, data-source-start, data-source-end) are baked into the mdast's data.hProperties. Any renderer that honors those — including <DocRenderer> — ships overlay-compatible markup with no extra wiring.

To set the source-file prefix, pass sourceFilePrefix to the provider:

<DocsProvider
  loader={loader}
  config={config}
  sourceFilePrefix="website/src/docs/"
  dev={import.meta.env.DEV}
>

The Spruce overlay editor (under /overlay in this monorepo) attaches to any product site that emits those attributes — clicking a paragraph in the rendered doc opens the source markdown in Spruce, and edits hot-reload back into the page.

License

MIT © Coniferous — see LICENSE.