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

@jdubray/mdsite

v0.1.0

Published

Turn a folder of Markdown into a browsable static site that works over file://. No dependencies, no CDN, no build step. Code fences and ASCII diagrams survive byte-for-byte.

Readme

mdsite

Turn a folder of Markdown into a browsable static site that works over file://. No dependencies, no CDN, no build step, no server. Code fences and ASCII diagrams survive byte-for-byte.

npx @jdubray/mdsite ./docs --out ./site
open ./site/index.html

That's it. Markdown becomes HTML; PDFs and everything else are copied and linked, so they open in the browser's own viewer.


Why this exists

Most documentation tools assume a server, a bundler, or a network. That is fine until you want to hand somebody a folder — on a USB stick, inside an air-gapped network, as a build artifact, in a ZIP attached to an email — and have it just open.

file:// blocks XMLHttpRequest. So a single-page viewer that loads Markdown at runtime cannot work there, and every tool built that way fails silently in exactly the situation where you most needed it to work.

mdsite pre-renders every .md to its own .html, next to where it was. Relative links between documents are rewritten to point at the generated pages. The result is a folder of ordinary HTML files with no runtime anything.

What you get

  • A sidebar tree of the whole folder, on every page, with a filter box.
  • Per-page table of contents and hover anchors on headings.
  • Light and dark, from prefers-color-scheme.
  • Wide things scroll in their own container — tables, long code lines, ASCII diagrams — so the page body never scrolls sideways.
  • PDFs, images, HTML, Office files copied through and linked.
  • One inlined stylesheet. Nothing is fetched at runtime, ever.

Install

npm i -D @jdubray/mdsite     # or use npx, there is nothing to install

Node ≥ 20. Zero runtime dependencies — the dependencies block is empty and stays that way.

CLI

mdsite <src> [--out <dir>] [options]

  --out <dir>          output directory (default: ./mdsite-out). REPLACED on build.
  --title <name>       site name in the sidebar (default: the source folder name)
  --subtitle <text>    small text under the title (default: "documentation")
  --exclude <re>       skip paths matching this regular expression
  --flag-dir <dir>     directory whose pages carry a warning banner
  --flag-note <text>   the banner text
  --css <file>         replace the stylesheet entirely

--flag-dir exists for the common case of a folder you want to browse but not distribute:

mdsite ./docs --out ./site \
  --flag-dir internal --flag-note "Internal draft — not for distribution."

--out is deleted before the build. Pointing it inside --src would destroy your documents, so mdsite refuses that outright rather than doing it.

API

import { renderMarkdown, buildSite } from '@jdubray/mdsite';

// just the HTML
const { html, toc } = renderMarkdown('# Hello\n\nSome `code`.');

// the whole tree
buildSite({
  src: './docs',
  out: './site',
  title: 'My project',
  include: (rel) => !rel.startsWith('drafts/'),
  indexIntro: '> Start with **the manual**.',
});

| export | what | |---|---| | renderMarkdown(src, opts) | → { html, toc }. opts.linkFor rewrites hrefs and image srcs | | renderInline(text, linkFor) | inline spans only; input must already be escaped | | safeUrl(url, opts) | the URL scheme allowlist — returns the URL, or null | | buildSite(opts) | the generator | | collect(root, opts) | walk a directory, returning what would be rendered or copied | | escapeHtml · slugify · splitRow | the small pieces, exported because they are useful | | DEFAULT_CSS · FILTER_JS | the theme, if you want to extend rather than replace it |

Security

The renderer is designed to be pointed at Markdown its operator did not write — a docs folder from a pull request, a wiki export, user-submitted content.

  • Input is escaped before any formatting is applied, so markup in the source cannot become markup in the output. There is no raw-HTML passthrough, and that is deliberate.
  • Every emitted URL passes a scheme allowlist. [x](javascript:alert(1)) does not produce a live link. Refused are javascript:, vbscript:, data: in an href, and any unrecognised scheme — unknown means refused, not assumed harmless. Allowed are all relative forms plus http(s), mailto, tel, ftp, file.
  • Detection ignores control characters, so \tjavascript: and javascript: cannot slip past a naive prefix check.
  • The allowlist runs after your linkFor, not before, because linkFor is yours and can return anything.
  • data:image/* is kept for images — inline diagrams are a real pattern and a data URL in an <img> cannot execute — but never for a link, where it can navigate to attacker-authored HTML.
  • A refused link renders its label without an anchor, with a title saying why. It is not rewritten to #: a link that silently points somewhere else is worse than a visible refusal.

Escaping alone would not have covered the URL cases. The quotes are safe; the scheme is not.

What it does not do

Stated plainly, because a docs tool that overpromises wastes your afternoon:

  • It is not CommonMark-complete. It covers ATX headings, fenced code, GFM pipe tables, blockquotes, nested lists, rules, images, links, and the usual inline spans. Setext headings, reference links, footnotes, definition lists and inline HTML are not supported.
  • No syntax highlighting. Fences get a lang-* class and are left alone. Add a highlighter in your own CSS/JS if you want one.
  • No search index, beyond the sidebar filter, which is filename-only.
  • No incremental build. It is fast because it does almost nothing; just run it again.
  • No plugin system. Swap the CSS, pass linkFor, or import renderMarkdown and build your own pages.

If you need any of those, use Eleventy, MkDocs, Docusaurus or VitePress. This is for the case where you want a folder you can open.

Tests

npm test

33 tests, node:test, no runner. They cover the URL allowlist, escaping, verbatim fences, table edge cases (including escaped pipes), duplicate heading ids, link rewriting, and the refusal to write into the source directory.

License

MIT