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

@iyulab/canopy-page

v0.13.0

Published

Authoring pipeline for documentation sites: one settings file, integrity checks, and a build.

Readme

canopy-page

One settings file, one command, one documentation site.

canopy-page turns a folder of markdown into a published documentation site. It owns the authoring pipeline around the rendering: the settings a site is configured by, the checks that keep broken references from shipping, and the build that ties them together. The rendering itself is canopy's job, and canopy-page drives it.

Live docs: https://iyulab.github.io/canopy-page — built with canopy-page itself, from the examples/site in this repository, republished on every push to main.


Why

Documentation sites in a product repository tend to be rebuilt from scratch each time: a site generator, a hand-maintained sidebar, a script that checks images, another that lists release notes newest-first. The parts that differ between products are small. The parts that repeat are the ones this package holds.

  • One contract. A settings.json beside the markdown, so a site is reproducible from its source tree rather than from a build script holding half the configuration.
  • Checks that run before publishing. A dead link costs nothing to find now and is expensive to find after deployment, where it presents as a reader hitting a 404.
  • One site, built in one pass. Links and backlinks resolve across the whole of it, so a guide and the release notes it refers to stay connected.

What ships in every site

No settings.json field turns these on — they are just there, on the next build. Reading a page never depends on any of the scripted ones: block scripts, or print the page, and only they go away.

  • Search, matching a query against every page's title, headings, and body — Ctrl+K / Cmd+K jumps to it from anywhere, badged on the search box itself so the shortcut is discoverable without reading the docs
  • The current page and section, marked in the sidebar and the on-page outline, updating as you scroll
  • A dark/light toggle that remembers a reader's choice; without one, pages follow the system setting
  • Content images open full-size in a lightbox when clicked, closing on a background click, Esc, or its close button
  • Prev/next cards linking to a page's neighbors in the sidebar's own order, and backlinks listing every page that points to it
  • Sidebar groups collapse, open exactly along the path to the page you're on and closed everywhere else
  • A breadcrumb trail in the topbar, when there's a title, a logo, home, or search for it to sit beside — and an icon on home specifically when it points off the site, so a link sitting right next to that trail doesn't look like it stays on it
  • A full-screen menu on narrow screens that starts closed instead of opening on every page, remembering a reader's own choice to leave it open for the rest of that visit
  • A code block wider than the screen shows a shadow at whichever edge still has more to scroll to, and nothing once you've scrolled there — a cue for a scrollbar that some OS/browser combinations hide until hovered
  • Sitemap and robots.txt, once siteUrl is set

See it live at https://iyulab.github.io/canopy-page, or read What a reader gets for how each one behaves.

Install

npm install --save-dev @iyulab/canopy-page

Node 22 or newer.

Getting started

npx canopy-page init docs/site     # write a settings file (and a home page, if needed)
npx canopy-page check docs/site    # report anything broken, without building
npx canopy-page build docs/site -o dist/help
npx canopy-page watch docs/site       # rebuild on change, serve it locally

init never replaces a settings file that is already there, and writes no page into a folder that already holds markdown — an existing set of documents is being adopted, not started.

build runs the same checks check does, on the same view of the site, and stops if any of them fail. Both leave with a non-zero exit code when they do, which is all a pipeline needs.

Commands

| Command | What it does | |---|---| | canopy-page init [site-dir] | Write a settings file naming the site after its folder | | canopy-page check [site-dir] | Check settings and references; build nothing | | canopy-page build [site-dir] [-o out] | Check, then publish to out (default ./site) | | canopy-page watch [site-dir] [-o out] [--port n] | Build, then rebuild on change and serve it locally (default port 8080) |

[site-dir] is the folder holding settings.json, and defaults to the current one.

settings.json

Every field is an override, so {} is a valid settings file: a folder of markdown builds with its navigation derived from the folder tree. Settings exist for what a tree cannot say by itself — the order of a release log, a label that is not a directory name, a draft folder that stays unpublished.

{
  "$schema": "https://iyulab.github.io/canopy-page/settings.schema.json",
  "title": "Product Help",
  "description": "How to use it",
  "lang": "en-GB",
  "icon": "assets/favicon.png",
  "exclude": ["_drafts", "*.tmp"],
  "rehypePlugins": ["rehype-declart"],
  "sections": [
    { "path": "guide", "label": "Guide", "items": [
      { "label": "Orders", "items": ["guide/orders/list", "guide/orders/detail"] },
      "guide/settings/*"
    ]},
    { "path": "release-notes", "label": "Release notes", "order": "desc" }
  ]
}

| Field | Meaning | |---|---| | $schema | Optional. Points an editor (VS Code, JetBrains) at settings.schema.json for completion and inline validation. Read and ignored by canopy-page itself | | title | Site name. Defaults to the folder's name | | description | Fills <meta name="description">, which is what link previews show | | lang | BCP 47 tag for <html lang>. Worth setting for any non-English site: assistive technology reads pronunciation from it | | strings | Overrides for the reader chrome's own text — search, toggleTheme, siteNav, pageNav, onThisPage, indexTitle (the auto-generated contents page's title/heading), backlinks (a page's "linked references" heading), breadcrumb (the topbar's ancestor-trail nav's label), searchFailed (the client search's failure message). lang only changes what <html lang> declares; this text is canopy's own UI or canopy-page's own search script, not vault content, so it stays English otherwise. No built-in translation table — the same reasoning home.label already follows: link text has to be written in the site's own language. Keys left out keep their English default | | icon | Favicon, relative to the settings file. Must be a published file | | tokens | CSS of design-token overrides, relative to the settings file. Appended after canopy's own tokens, so a file naming one value keeps the rest. It is configuration rather than content, so — unlike icon and logo — it is excluded from the published site automatically. Absent: canopy's default palette | | logo | Image shown beside the site title, relative to the settings file. Must be a published file — the opposite direction from tokens, because this one is content. Rendered with an empty alt, deliberately: the site title beside it already names the site, so there is no separate text to give it. Absent: the sidebar header shows the title text alone | | home | A link back to the site this documentation sits beside: { url, label }. Both are required together — naming half of it is not a valid setting. url is absolute when the target is a different origin, relative when it is a sibling of the published site (each page resolves it against its own depth, the same as every other internal link); there is no default label, because link text has to be written in the site's own language. Absent: no link back to a surrounding site is rendered | | siteUrl | Absolute URL naming where the built site will stand. Every link canopy writes is relative, which is what lets a site be served from any sub-path — and exactly why a sitemap, whose entries must be absolute, needs this separately. Only when it is set does build write sitemap.xml and a robots.txt pointing at it. Absent: neither file is written | | exclude | Paths to leave unpublished: a directory (_drafts or _drafts/**), an extension at any depth (*.tmp), or one exact path. Patterns are relative to the settings file, and a shape outside that list — images/*.md — is refused rather than left to match nothing | | rehypePlugins | Package names of rehype plugins to run on every page, after canopy's own sanitize step and before syntax highlighting — canopy's fixed extension point for markdown that needs more than CommonMark and GFM, a diagram fence rendered to SVG being the case this exists for. Each entry is an installed package name ("rehype-declart"), never a filesystem path — a relative-looking entry is refused, since the directory it would resolve against is wherever the build happens to run from, not this file | | sections | Ordered regions of the site — see below |

tokens is two blocks in practice, not one — a bare :root and a prefers-color-scheme: dark override:

/* brand.css */
:root {
  --accent: #0a7c5a;
  --accent-hover: #096a4d;
}

@media (prefers-color-scheme: dark) {
  :root {
    --accent: #4ecfa2;
    --accent-hover: #6fdcb5;
  }
}

canopy's own tokens end with a prefers-color-scheme: dark block, and a media query adds no specificity over a bare selector — so a bare :root appended after that block wins in both schemes. A one-block file naming only a light-mode colour would ship that colour onto a dark sidebar too.

Unlike settings.json (below), tokens gets no such validation — it's a plain CSS file appended to canopy's own, and CSS silently ignores a custom property nothing reads. A file carried over from a previous documentation tool's own token names (or a .dark/.light class toggle instead of canopy's [data-theme] attribute) parses fine, builds fine, and changes nothing: the site keeps canopy's default colours with no error anywhere. If a tokens override doesn't show up on the built site, that silent mismatch — not a build failure — is the first thing to check.

The settings file itself is never published, and neither is anything exclude names. A file named settings.json deeper in the site is content, and ships.

Validation is strict: an unknown key is rejected rather than ignored, because a mistyped one that is quietly dropped looks like a tool disobeying its configuration. Every message names the position it is about, down to sections[0].items[1].

Sections

A settings file describes one site, built in one pass. sections name ordered regions within it — a guide, a release log — rather than separate builds. Two genuinely independent sites are two settings files.

| Field | Meaning | |---|---| | path | The directory this section covers | | label | Heading shown for it. Defaults to the directory name | | order | asc or desc for the pages inside. desc is what a release log wants | | items | Explicit contents, in display order. Cannot be combined with order — a list is an order |

An entry in items is a page path, or a group:

{ "label": "Orders", "items": ["guide/orders/list", "guide/orders/detail"] }

Paths may be written with or without their extension. Two glob shapes are understood: dir/* is the pages directly in a directory, dir/** is every page beneath it. A glob means the pages there that are not placed already, which is what makes ["guide/install", "guide/*"] read the way it looks — this page first, then the rest.

Pages no section mentions are placed anyway, inside their own section where they have one, and reported. A page that exists but cannot be reached is a worse outcome than one shown in an order nobody chose, and listing three pages of a folder and forgetting the fourth describes an oversight rather than a decision to hide it.

Where no ordering is asked for at all, no navigation spec is produced and canopy derives the navigation itself. Ordering derived here follows file names rather than page titles.

A page is shown under the name canopy gives it: its frontmatter title, else the heading it opens with, else its filename. That usually means a section needs no label at all — label is for the cases the documents cannot answer, and it still overrides them when written. A section whose directory holds an index page is named by that page for the same reason.

A section's heading already links its own index page, so naming that page in items asks for what is there rather than for a second copy of it, and is not counted as placing it twice.

What check reports

Errors — these stop a build:

  • A settings reference that matches no page, or a page placed more than once
  • A link that points at nothing published, naming the page and the line
  • An image that is not a published file
  • A wikilink that matches no page. It renders as plain text rather than as a broken link, so the message says so — otherwise nobody knows what they are looking for

A link whose destination stops at a space is reported as that, rather than as the truncated path it becomes. An unbracketed destination ends at the first space — [x](../a b/c.md) links ../a and leaves the rest as text — so the target the message would otherwise name is one nobody wrote.

Warnings — reported, and the build continues:

  • Pages no section covers
  • A root-absolute reference (/assets/logo.png) with nothing published at that path. Where such a path resolves depends on what the site is served from, which is not a checker's to know — but a site served from its own root is the ordinary case, and a public/-style folder that other generators map onto the root does not exist here, so these silently 404. A warning rather than an error, because mounting the site elsewhere would make it right. When siteUrl already declares a sub-path mount, a root-absolute reference warns even if it resolves today, since that is the one case the checker can actually judge
  • An exclude pattern that matched nothing, which usually means a path written from the wrong place. Extension patterns are left alone: *.tmp in a site with no scratch files is a rule about what may never ship, not a claim that something is there
  • A section with no label and no index page, whose sidebar heading falls back to its own directory name — a filesystem detail, not a name anyone chose. Add a label, or an index page for the section to name itself
  • A filename whose published URL needs percent-encoding — a stray space or other ASCII character outside a URL's unreserved set, most often. The page still publishes and works; this is a nudge to check whether the encoding was intended, not a defect. Blind to non-ASCII on purpose, so a Korean, Japanese, or any other non-English filename is never flagged for being itself

Checking reads the settings and each page. It never renders, so it is fast enough to sit at the front of a pipeline, at the scale a product manual reaches. References inside fenced or inline code are ignored: a fenced example of a broken link is documentation, not a broken link. What canopy states it leaves alone is left alone here too — absolute URLs, protocol-relative URLs, bare fragments, and paths above the site root. A target ending in / names a directory, and is answered by the index page that directory is entered by.

build writes two files check never sees: with siteUrl set, a sitemap.xml listing every published page and a robots.txt pointing at it. Neither is checked, because neither exists until the build has already succeeded.

What belongs where

canopy-page owns the authoring pipeline; canopy owns the rendering.

| | canopy-page | canopy | |---|---|---| | Configuration | settings.json and its validation | — | | Structure | sections, order, labels, globs | navigation tree, link resolution | | Integrity | reference checks, exit codes | — | | Output | — | HTML, assets, backlinks, outlines, site shell |

canopy is driven through its command line rather than its library API, on purpose: it is the same door every other consumer uses, and a door only stays wide enough if the people who could have gone around it do not. Where the command line cannot express something, that is worth raising with canopy rather than working around here.

Contributing

See CONTRIBUTING.md, including what a release has to update besides the version number.

License

MIT