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

@kristofferlundb/fencer

v0.1.2

Published

A remark plugin that transforms fenced YAML component blocks into framework-agnostic rendered components with optional Zod validation

Downloads

25

Readme

Fencer

A remark plugin that transforms fenced component code blocks (written in YAML) into custom rendered output. Works in Node.js and the browser.

Overview

This plugin lets you embed structured component data inside Markdown using YAML syntax, then render it however you like — as HTML strings, React elements, or any other format your framework supports.

# My Article

Here is a fact box:

```component
title: Did you know?
type: factBox
text: Honey never spoils. Archaeologists have found 3000-year-old honey that was still edible.
```

And here is the rest of the article.

The plugin will:

  1. Parse the YAML inside ```component blocks into a JSON object
  2. Validate the object against an optional Zod schema
  3. Render the object using your custom renderer function
  4. Replace the code block in the AST with the rendered output

Installation

npm install @kristofferlundb/fencer

Peer dependencies

  • zod (optional) — only needed if you want schema validation
npm install zod

Quick Start

import { remark } from "remark";
import remarkHtml from "remark-html";
import fencer from "@kristofferlundb/fencer";

const markdown = `
# Hello

\`\`\`component
title: A fun fact
type: factBox
text: The shortest war in history lasted 38 minutes.
\`\`\`

Regular paragraph here.
`;

const result = await remark()
  .use(fencer, {
    renderer: (data) => {
      return `<div class="${data.type}"><h3>${data.title}</h3><p>${data.text}</p></div>`;
    },
  })
  .use(remarkHtml, { sanitize: false })
  .process(markdown);

console.log(String(result));

Output:

<h1>Hello</h1>
<div class="factBox"><h3>A fun fact</h3><p>The shortest war in history lasted 38 minutes.</p></div>
<p>Regular paragraph here.</p>

Rendering by Component Type

The real power of Fencer is using data.type (or any field you choose) to render different components differently. Here are practical patterns for handling multiple component types.

Basic switch on type

The simplest approach — use a switch statement in your renderer to produce different HTML for each type:

```component
title: Did you know?
type: factBox
text: Honey never spoils.
```

```component
type: callout
variant: warning
title: Heads up
text: This API is deprecated and will be removed in v3.
```

```component
type: quote
text: The best way to predict the future is to invent it.
author: Alan Kay
year: 1971
```
import { remark } from "remark";
import remarkHtml from "remark-html";
import fencer from "@kristofferlundb/fencer";

const result = await remark()
  .use(fencer, {
    renderer: (data) => {
      switch (data.type) {
        case "factBox":
          return `<div class="fact-box">
            <h3>💡 ${data.title}</h3>
            <p>${data.text}</p>
          </div>`;

        case "callout":
          return `<aside class="callout callout--${data.variant}">
            ${data.title ? `<strong>${data.title}</strong>` : ""}
            <p>${data.text}</p>
          </aside>`;

        case "quote":
          return `<blockquote class="quote">
            <p>"${data.text}"</p>
            <footer>— ${data.author}${data.year ? ` (${data.year})` : ""}</footer>
          </blockquote>`;

        default:
          return `<div class="unknown-component"><pre>${JSON.stringify(data, null, 2)}</pre></div>`;
      }
    },
  })
  .use(remarkHtml, { sanitize: false })
  .process(markdown);

Type-safe rendering with a Zod discriminated union

For production use, define per-type schemas and combine them into a discriminated union. This gives you full type safety inside each case branch:

import { z } from "zod";
import { remark } from "remark";
import remarkHtml from "remark-html";
import fencer from "@kristofferlundb/fencer";

// Define a schema for each component type
const FactBoxSchema = z.object({
  type: z.literal("factBox"),
  title: z.string(),
  text: z.string(),
  source: z.string().optional(),
});

const CalloutSchema = z.object({
  type: z.literal("callout"),
  variant: z.enum(["info", "warning", "error", "success"]),
  title: z.string().optional(),
  text: z.string(),
});

const QuoteSchema = z.object({
  type: z.literal("quote"),
  text: z.string(),
  author: z.string(),
  year: z.number().optional(),
});

// Combine into a discriminated union on the "type" field
const ComponentSchema = z.discriminatedUnion("type", [
  FactBoxSchema,
  CalloutSchema,
  QuoteSchema,
]);

type ComponentData = z.infer<typeof ComponentSchema>;

// The renderer now has full type narrowing inside each case
function renderComponent(data: ComponentData): string {
  switch (data.type) {
    case "factBox":
      // TS knows: data.title, data.text, data.source?
      return `<div class="fact-box">
        <h3>📘 ${data.title}</h3>
        <p>${data.text}</p>
        ${data.source ? `<small>Source: ${data.source}</small>` : ""}
      </div>`;

    case "callout":
      // TS knows: data.variant, data.title?, data.text
      const icons = { info: "ℹ️", warning: "⚠️", error: "🚨", success: "✅" };
      return `<aside class="callout callout--${data.variant}">
        ${data.title ? `<strong>${icons[data.variant]} ${data.title}</strong>` : ""}
        <p>${data.text}</p>
      </aside>`;

    case "quote":
      // TS knows: data.text, data.author, data.year?
      return `<blockquote class="quote">
        <p>"${data.text}"</p>
        <footer>— ${data.author}${data.year ? ` (${data.year})` : ""}</footer>
      </blockquote>`;
  }
}

const result = await remark()
  .use(fencer, {
    schema: ComponentSchema,
    renderer: renderComponent,
  })
  .use(remarkHtml, { sanitize: false })
  .process(markdown);

With this setup, if someone writes an invalid component block in Markdown (e.g. a callout with variant: "purple"), Zod will catch it at processing time before it reaches your renderer.

Renderer lookup map

If you prefer to avoid a switch, you can use an object map to look up renderers by type:

const renderers = {
  factBox: (data) =>
    `<div class="fact-box"><h3>${data.title}</h3><p>${data.text}</p></div>`,

  callout: (data) =>
    `<aside class="callout callout--${data.variant}"><p>${data.text}</p></aside>`,

  quote: (data) =>
    `<blockquote><p>"${data.text}"</p><footer>— ${data.author}</footer></blockquote>`,
};

remark().use(fencer, {
  renderer: (data) => {
    const render = renderers[data.type];
    if (!render) {
      return `<div class="error">Unknown component type: ${data.type}</div>`;
    }
    return render(data);
  },
});

Nested data and arrays

Component types aren't limited to flat key-value pairs. Use nested objects and arrays for richer structures:

```component
type: card
title: Project Update
meta:
  author: Jane Doe
  date: 2025-01-15
  tags:
    - release
    - frontend
items:
  - Redesigned the dashboard
  - Fixed 12 accessibility issues
  - Improved load time by 40%
```
remark().use(fencer, {
  renderer: (data) => {
    switch (data.type) {
      case "card":
        const tags = (data.meta?.tags || [])
          .map((t) => `<span class="tag">${t}</span>`)
          .join(" ");
        const items = (data.items || [])
          .map((item) => `<li>${item}</li>`)
          .join("\n");
        return `<article class="card">
          <h3>${data.title}</h3>
          <div class="meta">By ${data.meta?.author} on ${data.meta?.date} ${tags}</div>
          <ul>${items}</ul>
        </article>`;

      default:
        return `<div>${JSON.stringify(data)}</div>`;
    }
  },
});

API

fencer(options)

options.renderer (required)

A function that receives the parsed component data and returns either:

  • A string of HTML — injected as an html node in the AST
  • A node object with a type property — inserted directly into the mdast tree
// String renderer
renderer: (data) => `<div class="${data.type}">${data.title}</div>`,

// Node renderer
renderer: (data) => ({
  type: "html",
  value: `<my-component title="${data.title}" />`,
})

options.schema (optional)

A Zod schema to validate parsed YAML against. When provided, the data argument in your renderer will be fully typed. This works with simple schemas and discriminated unions alike (see Rendering by Component Type for a full union example).

import { z } from "zod";

const ComponentSchema = z.object({
  title: z.string(),
  type: z.enum(["factBox", "callout", "quote"]),
  text: z.string(),
});

remark().use(fencer, {
  schema: ComponentSchema,
  renderer: (data) => {
    // data is typed as { title: string; type: "factBox" | "callout" | "quote"; text: string }
    return `<div class="${data.type}"><h3>${data.title}</h3><p>${data.text}</p></div>`;
  },
});

options.onValidationError (optional)

Controls what happens when Zod validation fails. Default: "throw".

| Value | Behavior | | --------------- | ----------------------------------------------------------------- | | "throw" | Throws an error with details about which fields failed | | "warn" | Logs a warning to the console and passes the raw data to renderer | | "passthrough" | Silently ignores the error and passes the raw data to renderer |

remark().use(fencer, {
  schema: MySchema,
  onValidationError: "warn",
  renderer: (data) => `<div>${data.title}</div>`,
});

options.lang (optional)

The fenced code block language identifier to match. Default: "component".

// Match ```widget blocks instead of ```component
remark().use(fencer, {
  lang: "widget",
  renderer: (data) => `<widget-element>${data.title}</widget-element>`,
});

Framework Integration

Vanilla HTML

import { remark } from "remark";
import remarkHtml from "remark-html";
import fencer from "@kristofferlundb/fencer";

const html = await remark()
  .use(fencer, {
    renderer: (data) =>
      `<div class="component component--${data.type}">
        <h3>${data.title}</h3>
        <p>${data.text}</p>
      </div>`,
  })
  .use(remarkHtml, { sanitize: false })
  .process(markdown);

React / Next.js (via MDX or custom processing)

Since the renderer returns HTML strings that get embedded in the AST, you can use this with any React-based Markdown pipeline. A common pattern is to output custom element tags that map to React components:

remark().use(fencer, {
  renderer: (data) => {
    // Emit a custom element that your React component library can pick up
    const props = Object.entries(data)
      .map(([k, v]) => `${k}="${v}"`)
      .join(" ");
    return `<CustomComponent ${props} />`;
  },
});

Or for rehype-based pipelines, return a custom mdast node:

remark().use(fencer, {
  renderer: (data) => ({
    type: "html",
    value: `<custom-component data-props='${JSON.stringify(data)}'></custom-component>`,
  }),
});

Astro

// astro.config.mjs
import fencer from "@kristofferlundb/fencer";

export default defineConfig({
  markdown: {
    remarkPlugins: [
      [
        fencer,
        {
          renderer: (data) =>
            `<div class="${data.type}"><h3>${data.title}</h3><p>${data.text}</p></div>`,
        },
      ],
    ],
  },
});

YAML Syntax

The content inside ```component blocks is parsed as standard YAML. You can use all YAML features:

```component
title: My Component
type: factBox
text: Simple string value
```

Nested objects:

```component
title: Advanced
type: card
meta:
  author: Jane Doe
  date: 2025-01-15
```

Arrays:

```component
title: Feature List
type: list
items:
  - First item
  - Second item
  - Third item
```

Multiline strings:

```component
title: Long Text
type: article
text: >
  This is a long piece of text
  that spans multiple lines but
  will be joined into one.
```

Exported Utilities

In addition to the main plugin, the package exports the internal utilities for standalone use:

import { parseYaml, validateData } from "@kristofferlundb/fencer";

// Parse YAML string to object
const data = parseYaml("title: Hello\ntype: box");
// => { title: "Hello", type: "box" }

// Validate data against a Zod schema
import { z } from "zod";
const schema = z.object({ title: z.string(), type: z.string() });
const validated = validateData(data, schema);

TypeScript

Full TypeScript support is included out of the box. The package ships with declaration files for both ESM and CJS.

import fencer from "@kristofferlundb/fencer";
import type {
  PluginOptions,
  Renderer,
  RendererResult,
  ComponentNode,
  ValidationErrorMode,
} from "@kristofferlundb/fencer";

When you provide a Zod schema, the renderer's data parameter is automatically inferred:

import { z } from "zod";

const schema = z.object({
  title: z.string(),
  type: z.enum(["factBox", "callout"]),
  text: z.string(),
});

remark().use(fencer, {
  schema,
  renderer: (data) => {
    // TypeScript knows: data.title is string, data.type is "factBox" | "callout", etc.
    return `<div>${data.title}</div>`;
  },
});

License

MIT