@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 sharpBlog 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.mdDirectories 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
