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

markdown-repository

v1.1.3

Published

Use markdown files like a database. Firestore-style query builder for .md and .mdx files with frontmatter.

Readme

Markdown Repository

Use markdown files like a database. Read directories of .md or .mdx files, query them by frontmatter fields, sort, filter, and limit — with an API modeled after Firestore.

Installation

npm install markdown-repository

Quick start

Given a directory of blog posts:

posts/
  firebase-cost-optimization.mdx
  why-mvps-fail.mdx
  react-server-components.mdx

Each with YAML frontmatter:

---
title: Firebase Cost Optimization
date: "2025-08-22"
tags: [firebase, devops]
hidden: false
---

Post content here.

Create a repository and query it:

import { MarkdownRepository, query, where, orderBy, limit, get } from "markdown-repository";

const posts = new MarkdownRepository("posts", {
  extensions: [".mdx"],
});

// Get all posts
const all = get(posts);

// Get one post by slug (filename without extension)
const post = get(posts, "firebase-cost-optimization");

// Filter by tag
const firebasePosts = get(
  query(posts, where("tags", "array-contains", "firebase"))
);

// Sort by date, newest first, take 5
const recent = get(
  query(posts, orderBy("date", "desc"), limit(5))
);

Repository options

const repo = new MarkdownRepository(directory, options);

| Option | Type | Default | Description | |---|---|---|---| | extensions | string[] | [".md"] | File extensions to include | | recursive | boolean | false | Traverse subdirectories | | validator | (item) => item is T | accepts all | Type guard for content validation |

MDX files

const posts = new MarkdownRepository("content/posts", {
  extensions: [".mdx"],
});

Recursive directories

For content organized in nested folders:

content/
  discovery/
    worth-building.mdx
    user-research.mdx
  delivery/
    ship-early.mdx
    incremental-releases.mdx
  discovery.mdx
  delivery.mdx
const principles = new MarkdownRepository("content", {
  extensions: [".mdx"],
  recursive: true,
});

// Slugs reflect the directory structure
get(principles, "discovery");                // → discovery.mdx
get(principles, "discovery/worth-building"); // → discovery/worth-building.mdx

// Get all — returns flat array with nested slugs
const all = get(principles);
// [
//   { slug: "delivery", title: "Delivery", ... },
//   { slug: "delivery/ship-early", title: "Ship Early", ... },
//   { slug: "discovery", title: "Discovery", ... },
//   { slug: "discovery/user-research", title: "User Research", ... },
//   { slug: "discovery/worth-building", title: "Worth Building", ... },
// ]

Content validation with TypeScript

Define a type guard to get type-safe access to frontmatter fields:

interface BlogPost extends MarkdownItem {
  title: string;
  date: string;
  tags: string[];
}

function isBlogPost(item: MarkdownItem): item is BlogPost {
  return (
    typeof item.title === "string" &&
    typeof item.date === "string" &&
    Array.isArray(item.tags)
  );
}

const posts = new MarkdownRepository<BlogPost>("posts", {
  extensions: [".mdx"],
  validator: isBlogPost,
});

// TypeScript knows post.title, post.date, post.tags exist
const post = get(posts, "firebase-cost-optimization");
console.log(post.title);

Files that fail validation are silently excluded from getAll() results. getBySlug() throws MarkdownRepositoryInvalidTypeError.

Query API

The query API follows Firestore's functional pattern. Constraints are standalone functions that compose via query() and execute via get().

query(repository, ...constraints)

Combines a repository with constraints. Returns a query object — no data is read until get() is called.

const q = query(
  posts,
  where("hidden", "!=", true),
  orderBy("date", "desc"),
  limit(10)
);

const results = get(q);

get(source, slug?)

Executes a read. Three overloads:

get(posts, "my-slug")  // → single item (T)
get(posts)             // → all items (T[])
get(q)                 // → query results (T[])

where(field, operator, value)

Filters items by a frontmatter field. Supports all Firestore comparison operators.

Equality

where("hidden", "==", true)
where("hidden", "!=", true)

Comparison

where("order", ">", 1)
where("order", ">=", 1)
where("order", "<", 10)
where("order", "<=", 10)

Array membership

// Field is an array, check if it contains a value
where("tags", "array-contains", "firebase")

// Field is an array, check if it contains any of these values
where("tags", "array-contains-any", ["firebase", "devops"])

Value membership

// Field value is one of these
where("status", "in", ["draft", "review"])

// Field value is none of these
where("status", "not-in", ["archived", "deleted"])

orderBy(field, direction?)

Sorts results. Direction is "asc" (default) or "desc". Multiple orderBy constraints are applied in order — first is primary sort, second breaks ties.

orderBy("date", "desc")
orderBy("order", "asc")
orderBy("title") // defaults to "asc"

limit(count)

Caps the number of returned items. Applied after filtering and sorting.

limit(5)

Dynamic query composition

Constraints are plain objects. Build them conditionally, store them in arrays, spread them into query().

function getPosts({ tag, sortBy = "date", max } = {}) {
  const constraints = [];

  constraints.push(where("hidden", "!=", true));

  if (tag) {
    constraints.push(where("tags", "array-contains", tag));
  }

  constraints.push(orderBy(sortBy, "desc"));

  if (max) {
    constraints.push(limit(max));
  }

  return get(query(posts, ...constraints));
}

// All posts, newest first
getPosts();

// Firebase posts, 5 max
getPosts({ tag: "firebase", max: 5 });

Real-world examples

Blog with scheduled publishing

Posts have a date field and optional hidden flag. A post is published when its date is today or earlier and it's not hidden.

import { MarkdownRepository, query, where, get } from "markdown-repository";
import { parse, startOfDay, isBefore, isEqual } from "date-fns";

const posts = new MarkdownRepository("mod/jurij/posts", {
  extensions: [".mdx"],
});

function parseDate(str) {
  return str.includes(" ")
    ? parse(str, "yyyy-MM-dd HH:mm", new Date())
    : parse(str, "yyyy-MM-dd", new Date());
}

function isPublished(post) {
  if (post.hidden || !post.date) return false;
  const postDate = startOfDay(parseDate(post.date));
  const today = startOfDay(new Date());
  return isBefore(postDate, today) || isEqual(postDate, today);
}

// All published posts, newest first
function getAllPosts() {
  return get(posts)
    .filter(isPublished)
    .sort((a, b) => (parseDate(a.date) < parseDate(b.date) ? 1 : -1));
}

// Posts with a specific tag
function getPostsByTag(tag) {
  return get(
    query(posts, where("tags", "array-contains", tag))
  ).filter(isPublished)
    .sort((a, b) => (parseDate(a.date) < parseDate(b.date) ? 1 : -1));
}

// Tag cloud with counts
function getAllTags() {
  const tagMap = {};
  for (const post of getAllPosts()) {
    for (const tag of post.tags || []) {
      tagMap[tag] = (tagMap[tag] || 0) + 1;
    }
  }
  return Object.entries(tagMap)
    .sort((a, b) => b[1] - a[1])
    .map(([tag, count]) => ({ tag, count }));
}

Nested content with categories

A principles handbook organized by category, with ordered pages inside each category.

content/
  discovery.mdx            # category parent
  discovery/
    worth-building.mdx     # order: 1
    user-research.mdx      # order: 2
  delivery.mdx
  delivery/
    ship-early.mdx
    incremental-releases.mdx
import { MarkdownRepository, query, where, orderBy, get } from "markdown-repository";

const principles = new MarkdownRepository("mod/principles/content", {
  extensions: [".mdx"],
  recursive: true,
});

// All principles as a flat list
function getAllPrinciples() {
  return get(principles);
}

// Single principle by path — accepts string or array
function getPrincipleBySlug(slug) {
  const slugString = Array.isArray(slug) ? slug.join("/") : slug;
  return get(principles, slugString);
}

// All slugs as path arrays (for Next.js generateStaticParams)
function getAllSlugs() {
  return get(principles).map((p) => p.slug.split("/"));
}

// Children of a category, sorted by order
function getCategoryChildren(category) {
  return get(
    query(
      principles,
      where("slug", "!=", category),
      orderBy("order", "asc")
    )
  ).filter((p) => p.slug.startsWith(category + "/"));
}

Next.js App Router integration

// app/blog/[slug]/page.js
import { posts } from "@/lib/posts";
import { get } from "markdown-repository";

export async function generateStaticParams() {
  return get(posts).map((post) => ({ slug: post.slug }));
}

export default async function PostPage({ params }) {
  const { slug } = await params;
  const post = get(posts, slug);

  return <article>{/* render post */}</article>;
}
// app/blog/[slug]/page.js (catch-all for nested slugs)
import { principles } from "@/lib/principles";
import { get } from "markdown-repository";

export async function generateStaticParams() {
  return get(principles).map((p) => ({
    slug: p.slug.split("/"),
  }));
}

export default async function PrinciplePage({ params }) {
  const { slug } = await params;
  const post = get(principles, slug.join("/"));

  return <article>{/* render principle */}</article>;
}

MarkdownItem shape

Every item returned by get() has this base shape:

interface MarkdownItem {
  slug: string;    // derived from filename (without extension)
  content: string; // markdown body (everything after frontmatter)
  excerpt: string; // auto-extracted excerpt
  [key: string]: any; // all frontmatter fields spread here
}

The slug for flat directories is the filename: my-post.mdx becomes "my-post". For recursive directories, it includes the path: discovery/worth-building.mdx becomes "discovery/worth-building".

Error handling

import {
  MarkdownRepositoryNotFoundError,
  MarkdownRepositoryInvalidTypeError,
} from "markdown-repository";

try {
  const post = get(posts, "nonexistent-slug");
} catch (error) {
  if (error instanceof MarkdownRepositoryNotFoundError) {
    // File doesn't exist
  }
  if (error instanceof MarkdownRepositoryInvalidTypeError) {
    // File exists but fails validator
  }
}

About

Built and maintained by Jurij Tokarski from Varstatt. MIT licensed.