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

@bhouston/markdown-content-node

v1.0.7

Published

Filesystem and image metadata helpers for @bhouston/markdown-content

Readme

@bhouston/markdown-content-node

Node.js filesystem helpers for loading structured markdown content powered by @bhouston/markdown-content. Provides blog post loading with caching, path-safe markdown file reading, and image metadata extraction via sharp.

Install

npm install @bhouston/markdown-content-node @bhouston/markdown-content sharp

Blog Directory Convention

Each blog post lives in its own directory named after its id. The directory must contain a content.md file with blog front matter. Sibling image files are automatically picked up by loadBlogPost.

posts/
  my-first-post/
    content.md
    hero.png
  another-post/
    content.md

Directories whose names start with _ are ignored during traversal.

Blog Loading API

loadBlogPost(rootDirectory, directory, id, options?)

Reads and parses a single blog post from <directory>/content.md. Validates the id format ([a-zA-Z0-9-_]+), parses with builtInBlogParserSchemas by default, attaches id, directory (relative to rootDirectory), readTime, and sibling image metadata.

The loader automatically passes the resolved content.md pathname to @bhouston/markdown-content as sourceName, so parser errors include both the filename and original markdown text in MarkdownContentError.

import { loadBlogPost } from '@bhouston/markdown-content-node';

const post = await loadBlogPost('/content/posts', '/content/posts/my-post', 'my-post');
// post.title, post.date, post.author, post.tags[]
// post.id, post.directory, post.readTime
// post.images: MarkdownImageMetadata[]

LoadBlogPostOptions

| Field | Type | Description | | --------------- | ---------------------------------------- | ------------------------------------------------------------ | | parser | { parseMarkdownDocument(raw, source?): TPage }? | Provide a fully-constructed parser instance. | | parserSchemas | CreateMarkdownParserOptions? | Alternatively, provide schemas to build a parser on the fly. |

If neither is provided the default builtInBlogParserSchemas parser is used.

getBlogPosts(postsDirectory, options?)

Recursively loads all blog posts under postsDirectory, sorted by date descending. Returns the posts array directly; use getBlogPostsWithDiagnostics to also receive warnings.

import { getBlogPosts } from '@bhouston/markdown-content-node';

const posts = await getBlogPosts('/content/posts');

getBlogPostsWithDiagnostics(postsDirectory, options?)

Same as getBlogPosts but returns { posts, warnings } so callers can inspect load failures when errorMode: 'skip'.

import { getBlogPostsWithDiagnostics } from '@bhouston/markdown-content-node';

const { posts, warnings } = await getBlogPostsWithDiagnostics('/content/posts', {
  errorMode: 'skip',
  onWarning: (w) => console.warn(w.code, w.directory),
});

GetBlogPostsOptions

| Field | Type | Default | Description | | --------------------- | ---------------------------------- | ---------------------- | ----------------------------------------------------------- | | useProductionCache | boolean | true | Enable in-memory cache when NODE_ENV === 'production'. | | nodeEnv | string? | process.env.NODE_ENV | Override the environment check. | | errorMode | 'strict' \| 'skip' | 'strict' | 'skip' collects failures as warnings instead of throwing. | | onWarning | (w: GetBlogPostsWarning) => void | — | Called for each non-fatal warning. | | loadBlogPostOptions | LoadBlogPostOptions? | — | Options forwarded to loadBlogPost. |

The production cache is keyed by postsDirectory and is only activated when no custom parser or parserSchemas are provided. Call clearBlogPostsCache() to invalidate it manually.

getBlogPostById(postsDirectory, id, options?)

DFS search for a directory named id that contains content.md, then loads it. Returns null if not found.

import { getBlogPostById } from '@bhouston/markdown-content-node';

const post = await getBlogPostById('/content/posts', 'my-post');
if (!post) {
  // not found
}

clearBlogPostsCache()

Clears the shared in-memory production cache.

Markdown File Reading

readMarkdownFile(contentRootDirectory, fullPath)

Path-traversal-safe file reader. Strips leading slashes and blocks .. segments. Resolves <path>.md and <path>/index.md automatically. Returns { content, contentFileName } or null if the file is not found or the path is unsafe.

import { readMarkdownFile } from '@bhouston/markdown-content-node';

const result = await readMarkdownFile('/content', '/docs/getting-started');
if (result) {
  const { content, contentFileName } = result;
}

resolveMarkdownFile(baseFileName)

Tries <baseFileName>.md then <baseFileName>/index.md. Returns the resolved path or null.

resolveMarkdownRequestPath(basePath, relativePath)

Normalizes a URL path for routing. Strips leading slashes and index suffixes. Returns { fullPath, shouldRedirectToFullPath } — use shouldRedirectToFullPath to issue a canonical redirect when the request path contained a trailing index.

Image Metadata

readImageMetadata(directory, options?)

Reads top-level raster image files from directory using sharp. Returns MarkdownImageMetadata[] sorted by filename. Skips files with missing dimensions. Supported extensions: .jpg, .jpeg, .png, .webp, .avif, .gif, .tif, .tiff.

import { readImageMetadata } from '@bhouston/markdown-content-node';

const images = await readImageMetadata('/content/posts/my-post');
// [{ path: 'hero.png', width: 1200, height: 630 }, ...]

ReadImageMetadataOptions

| Field | Type | Description | | ----------- | ------------------------------------------ | ----------------------------------------------------- | | onWarning | (w: MarkdownContentNodeWarning) => void? | Called for directory-read or image-metadata failures. |

Error Types

MarkdownContentNodeError

Thrown for hard failures. Has a code property.

| Code | When thrown | | ---------------------------- | ------------------------------------------------------- | | INVALID_POST_ID | Blog post id is empty or contains invalid characters | | BLOG_POST_READ_FAILED | content.md could not be read from disk | | BLOG_POST_PARSE_FAILED | Parsing the markdown document failed | | BLOG_DIRECTORY_READ_FAILED | A blog directory could not be listed (strict mode only) |

When parsing fails, MarkdownContentNodeError.cause will usually be the underlying MarkdownContentError from @bhouston/markdown-content, including sourceName and sourceText.

import { MarkdownContentError } from '@bhouston/markdown-content';
import { MarkdownContentNodeError, loadBlogPost } from '@bhouston/markdown-content-node';

try {
  await loadBlogPost('/content/posts', '/content/posts/my-post', 'my-post');
} catch (error) {
  if (error instanceof MarkdownContentNodeError && error.cause instanceof MarkdownContentError) {
    console.error(error.code); // 'BLOG_POST_PARSE_FAILED'
    console.error(error.cause.sourceName); // '/content/posts/my-post/content.md'
    console.error(error.cause.sourceText); // original markdown text
  }
}

MarkdownContentNodeWarning

Non-fatal warnings emitted via onWarning callbacks.

| Code | Description | | ----------------------------- | ----------------------------------------------------- | | BLOG_POST_LOAD_FAILED | A single blog post failed to load in 'skip' mode | | BLOG_DIRECTORY_READ_FAILED | A blog directory could not be listed in 'skip' mode | | IMAGE_DIRECTORY_READ_FAILED | The image directory could not be read | | IMAGE_METADATA_READ_FAILED | A single image's metadata could not be extracted |

Exports

| Module | Key exports | | ------------------- | ------------------------------------------------------------------------------------------------------------- | | getBlogPosts | getBlogPosts, getBlogPostsWithDiagnostics, getBlogPostById, clearBlogPostsCache, option/warning types | | loadBlogPost | loadBlogPost, LoadBlogPostOptions | | markdownFile | readMarkdownFile, resolveMarkdownFile, resolveMarkdownRequestPath, MarkdownFile | | readImageMetadata | readImageMetadata, ReadImageMetadataOptions | | errors | MarkdownContentNodeError, MarkdownContentNodeErrorCode, MarkdownContentNodeWarning |

Requirements

  • Node.js 20+
  • @bhouston/markdown-content (peer dependency)
  • sharp (peer dependency, >=0.33.0 <1)

License

MIT