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

@eqtylab/docs

v0.4.0

Published

Astro documentation framework built on the Equality design system

Readme

@eqtylab/docs

An Astro documentation framework built on the Equality design system. One package to build every EQTY docs site — component library, product docs, and API reference — with folder-driven navigation and versioned deploys.

Quick start

pnpm add @eqtylab/docs @eqtylab/equality astro react react-dom
// astro.config.mjs
import docs from '@eqtylab/docs';
import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://docs.example.com',
  base: process.env.DOCS_BASE ?? '/',
  integrations: [docs({ title: 'My Docs' })],
});
// src/content.config.ts  — required; Astro cannot inject collections
import { docsCollections } from '@eqtylab/docs/loaders';

export const collections = docsCollections();

Then write MDX under src/content/docs/. That's the whole setup: the integration injects the page routes, a Markdown twin per page, /llms.txt, and a 404. Search is built separately, at the end of astro build; see Search needs a build.

Content is MDX-only. Component overrides apply exclusively to MDX, so a .md page would render its tables, code fences and callouts differently from every other page. A .md file in the content tree fails the build rather than going silently missing — renaming it is usually the only change needed, since MDX is a superset of Markdown.

Navigation comes from folders

Ordering lives in one optional _group.yaml per directory — there is deliberately no sidebar.order frontmatter to hand-maintain.

# src/content/docs/getting-started/_group.yaml
label: Getting Started
order: [installation, usage, themes] # filesystem names, not titles
collapsed: false
  • Children listed in order come first, in that order.
  • Everything unlisted appends alphabetically by sidebar label (sort: filename or manual to change that).
  • A section's overview always leads it, whether that is the folder's index.mdx, a page named overview, or one titled "Overview" - alphabetical order would otherwise bury it mid-list.
  • index.mdx in a folder becomes that folder's own page, listed first in the group as Overview. A section header only expands - it is never a link - because not every section has an index, and a header that navigates only sometimes teaches the wrong habit. Rename that row with indexLabel on the group, or globally with sidebar.indexLabel. The content root's index.mdx is the site landing page and stays out of the sidebar.
  • A typo in order produces a build warning naming the file and the token rather than silently reordering, and the schema is .strict() so an unknown key fails the build.

Linking between pages

Write the link the way the file sits on disk and it resolves to that page's URL:

See [usage](./usage.mdx) and [the guides index](../guides/index.mdx#ordering).

./usage.mdx becomes /getting-started/usage/, with base, pathPrefix and the trailing slash applied exactly as the sidebar applies them. A .md target finds the .mdx file of the same name, so markdown synced in from a product repo needs no rewriting, and a folder's index.mdx resolves to the folder. A target that names no page in the content directory is a build warning naming the file and the href — it still rewrites, so the typo shows up in the log rather than only as a 404.

Only content files are rewritten, and only targets that land inside the content directory: a relative link out of the collection is someone else's URL to own. Root-relative links are base-prefixed instead — Astro does not do that for authored markdown — and extensionless relative links are left to the browser.

Three conventions worth knowing

Styling is inline Tailwind, and one @source line is what makes it work. The chrome is styled with utility classes written in the markup. That only compiles because runtime/styles/docs.css names this package's own files in an explicit @source: Tailwind v4's automatic source detection skips node_modules, which is exactly where this markup sits in a consumer's build. Drop that line and every utility here produces nothing, with no error anywhere. pnpm run check:source enforces that the glob still covers all shipped markup, and runs as part of build. Recipes used by more than one component are @utility definitions in runtime/styles/utilities.css; a co-located *.module.css is the escape hatch for rules that cannot live on an element, which today means only GlobalSearch.module.css.

Overruling an Equality component takes !. packages/ui ships unlayered CSS, Tailwind utilities live in @layer utilities, and an unlayered declaration beats a layered one whatever its specificity. So where this markup has to overrule a value an Equality class already sets on the same element — CodeBlock's max-height, CommandList's — the utility carries !. Properties Equality leaves alone need nothing. Getting this wrong fails silently: the class is in the HTML and the rule is in the bundle, it just loses.

Route-level MDX components override page-level ones. @astrojs/mdx builds the map as { Fragment, ...fileComponents, ...props.components }, so the route's <Content components={…} /> wins over a page's own export const components. The route map is therefore kept to element overrides (a, pre); everything richer is an explicit import from @eqtylab/docs/components, so per-page overrides stay possible.

Code blocks

Fenced code renders through Equality's CodeBlock, server-side, so docs match product surfaces with no client React. The highlighter paints through the CSS Custom Highlight API rather than by wrapping tokens in markup, so the server-rendered DOM is the final DOM, the palette resolves through the normal cascade, and a static render gets correct light and dark colours for free.

Two small scripts do what would otherwise need hydration: runtime/scripts/eq-copy.ts for the copy button, and runtime/scripts/eq-highlight.ts to start the highlight pass. Fence tags are passed through as authored — the highlighter owns language resolution, including its alias table (ts, jsx, sh, yml and the rest), and renders plain rather than guessing at a tag it does not know.

Set code: { highlighter: 'shiki' } to use Astro's Shiki instead: wider language coverage, line highlighting and meta support, at the cost of a different look from the product.

Everything renders through Equality

The framework contributes layout, navigation and prose typography. Everything that renders content is an Equality component, server-rendered with no client React, so docs and product surfaces cannot drift:

| Authored as | Renders as | | --------------- | --------------------------------------------------------------------------------------- | | Code fences | CodeBlock | | Markdown tables | TableContainer / TableHeader / TableBody / TableRow / TableHead / TableCell | | <Alert> | Alert, as an aside | | <Tabs> | Tabs, server-rendered | | Links | base-aware a override |

Authors write Equality's own components, with no imports. The route-level component map supplies named components as well as element overrides, so this works in any page with nothing at the top of the file — there is no docs-specific component vocabulary to learn:

---
title: My page
---

<Alert variant="warning" title="Heads up">
  Body content, with real block elements.
</Alert>

The only thing the framework adds is a default: as="aside". Alert's default div carries role="alert", an assertive live region — right for a message responding to something the user did, wrong for a standing note in a document. Importing Alert from @eqtylab/equality explicitly opts out and gives you the raw component, because a file-level import wins over the component map.

One detail worth knowing: tables need a column count. TableContainer is a CSS grid whose parts all use subgrid, so it needs an explicit track list or the table collapses to one column. Markdown has no syntax for that, so rehypeTableColumns derives the count from the first row at build time.

Tabs are authored the same way, with no imports:

<Tabs syncKey="platform">
  <TabItem label="Linux">Anything, including fences and tables.</TabItem>
  <TabItem label="macOS">
    Sets sharing a `syncKey` switch together, and the choice is remembered.
  </TabItem>
</Tabs>

This is also why content is MDX-only — these overrides are the mechanism, and Astro's plain-Markdown pipeline has no component substitution at all.

Bringing your own CSS and scripts

customCss and clientScripts attach a consumer's own assets to every docs page:

docs({
  title: 'My Docs',
  customCss: ['./src/styles/site.css'],
  clientScripts: ['./src/scripts/glossary.ts'],
});

Both resolve project-relative paths against the project root, so they mean the same thing in dev and in a build. clientScripts entries are bundled rather than served as-is, which is what a script that rewrites rendered text needs: it has to import scheduleHighlight from @eqtylab/equality and call it afterwards, or the code blocks it touched lose their highlighting. (CodeBlock paints through the CSS Custom Highlight API over Ranges into those very text nodes.) A plain <script src> in public/ cannot import anything.

Owning a page yourself

A file in your own src/pages wins. The integration sees the route is already claimed, logs that it is yielding, and injects nothing there — so a landing page is yours to write, with the same chrome every other page gets:

---
import Prose from '@eqtylab/docs/chrome/Prose.astro';
import DocsPage from '@eqtylab/docs/layouts/DocsPage.astro';
import { docsNav } from '@eqtylab/docs/lib/nav-data.ts';

// `nav` is required: the sidebar, the drawer and the 404's suggestions all read it.
const nav = await docsNav(Astro.url.pathname);
---

<DocsPage title="Home" nav={nav} toc={[]} showToc={false} splash>
  <Prose title="Home">Anything at all.</Prose>
</DocsPage>

@eqtylab/docs/lib/* ships the rest of what the injected route uses, with the collection already wired in: docsEntries and pathContext alongside docsNav in nav-data.ts, breadcrumbsFor / prevNextFor / buildTocTree re-exported from there, and mdxComponents in mdx-components.ts for rendering a collection entry through the same component map.

Building the Search Index

Full-text search uses the built HTML, via Pagefind. The integration runs it in astro:build:done and writes the index to dist/pagefind/.

RUn pnpm run build to make search work in pnpm run dev. Dev serves dist/pagefind/ off disk, so results describe the last build; rebuild while dev runs and the next search picks it up. Until that first build the palette says search needs one.

What gets indexed is set by the markup, not by config:

| Attribute | Where | Effect | | ---------------------- | -------------------------------- | ------------------------------------- | | data-pagefind-body | the <article> | Bounds the index to page content | | data-pagefind-ignore | header, sidebar, TOC, action row | Keeps chrome out of every result | | data-pagefind-meta | the <h1> and the <article> | Carries title and the group label |

The group value is the section's label from _group.yaml, so search headings and the sidebar cannot drift apart.

The control itself is a command palette in the header, opened by click, ⌘K or /. Fill the header's search slot to replace it, or set search.provider: 'none' to drop it entirely.

Versions

On by default. With release tags in the repository (v1.2.3, one per release), the build makes one frozen copy of the docs per older group and serves it under /v<group>/, adds a switcher to the header, a banner and noindex to every old page, and scopes search to the page's version. Every release tag that is not a copy of its own redirects to its group's copy. Without tags it is dormant and the build is unchanged.

docs({
  versions: {
    current: '4.0.0', // defaults to the highest matching tag
    tags: 'v*', // git tag glob; tags not ending in MAJOR.MINOR.PATCH are skipped
    granularity: 'minor', // 'major' (default) | 'minor' | 'patch'
  },
});
// or
docs({ versions: false });

What to know:

  • current is never read from your package.json. Set it from the product you document, or leave it to the highest tag.
  • A tag counts when it ends in MAJOR.MINOR.PATCH. Any prefix is allowed, so v1.2.3, 1.2.3, sdk-v1.2.3 and @scope/[email protected] all work. Prereleases and anything else are skipped with a warning naming the tag.
  • CI needs the tags: check out with fetch-depth: 0.
  • If a tag lands one deploy after its bump and current is inferred, the / label is one release behind for that deploy. The content is current; the label heals on the next tag.
  • An archived page is a document, not an app. Prose, headings, tables and code samples are kept; live component examples are not rendered, because an old page's imports bind it to today's library. The banner says so.
  • Old versions are read-only. A wrong page is fixed by a new tag.
  • Moving granularity between minor and patch does not break published URLs: every grouping coarser than the one you set also resolves, so /v3/ and /v3.9/ both work at either setting. Moving to major does break them. Only groupings at or coarser than the setting are emitted, so at major nothing emits v3.9 and every published /v3.9/ URL retires.

The DOCS_BASE family of environment variables still exists for consumers who deploy each version as a separate build under its own base; this feature does not use them.

Development

pnpm build    # guard + node bundle + runtime copy + declarations
pnpm test     # nav ordering, prev/next, breadcrumbs, TOC, prose scope, link resolution

src/*.ts is bundled for Node (it is loaded when Astro reads your config). src/runtime/** ships as source and is compiled by the consumer's Astro — .astro files cannot be bundled. The copy is verbatim, which is what keeps dev source-linking byte-equivalent to the published package.

The dev surface

packages/demo — the Equality docs site — is the first-class consumer, so the framework is developed against the same site that ships. Run pnpm dev from the repo root: this package's dev keeps dist/runtime in sync while the demo serves on port 4321.

The sync is what makes edits to src/runtime/** hot-reload. Astro resolves injected route entrypoints through the real exports map, so dist/runtime — not src/runtime — is what Vite actually serves. Changes to src/*.ts are a different matter: they are bundled by tsup and read by Astro at config time, so they need pnpm build:node and a dev-server restart.

To exercise a versioned sub-path build:

DOCS_BASE=/v0.0/ pnpm --filter demo build

Then confirm nothing 404s when packages/demo/dist is served at /v0.0/.

Fixtures

tests/fixtures/basic is a separate, deliberately minimal consumer used for assertions — it contains a misspelled _group.yaml key on purpose, which is why it is kept apart from the demo rather than doubling as the dev surface.