NNuartz

Creating Plugins

Updated 2026-03-193 min read

How to write custom remark and rehype plugins for the nuartz markdown pipeline.

Warning

This guide assumes working knowledge of TypeScript and the unified ecosystem.

nuartz uses standard remark and rehype plugins. There is no custom plugin abstraction — you write plugins exactly as the unified ecosystem expects.

Plugin types

  • Remark plugins transform the Markdown AST (mdast). They run before the markdown is converted to HTML.
  • Rehype plugins transform the HTML AST (hast). They run after the markdown-to-HTML conversion.

Where plugins live

Custom plugins are in packages/nuartz/src/plugins/:

PluginTypePurpose
callout.tsremarkObsidian-style callout blocks (> [!note])
wikilink.tsremark[[wikilink]] syntax support
tag.tsremarkInline #tag extraction
highlight.tsremark==highlight== syntax
comment.tsremark%%obsidian comments%% removal
arrows.tsremarkArrow symbol replacements (->, =>, etc.)

Writing a remark plugin

A remark plugin is a function that returns a transformer operating on the mdast tree. Here's a minimal example:

import { visit } from "unist-util-visit"
import type { Root, Text } from "mdast"
import type { Plugin } from "unified"
 
export const remarkMyPlugin: Plugin<[], Root> = () => {
  return (tree) => {
    visit(tree, "text", (node: Text) => {
      // Transform text nodes
      node.value = node.value.replace(/--/g, "\u2014") // em-dash
    })
  }
}

Key tools for writing plugins:

Writing a rehype plugin

Rehype plugins work the same way but operate on hast (HTML AST) nodes:

import { visit } from "unist-util-visit"
import type { Root, Element } from "hast"
import type { Plugin } from "unified"
 
export const rehypeMyPlugin: Plugin<[], Root> = () => {
  return (tree) => {
    visit(tree, "element", (node: Element) => {
      if (node.tagName === "img") {
        node.properties.loading = "lazy"
      }
    })
  }
}

Adding data to vfile

Plugins can attach metadata to the vfile for downstream consumers. For example, remarkTag extracts #tags and stores them in file.data.tags:

export const remarkExample: Plugin<[], Root> = () => {
  return (tree, file) => {
    const items: string[] = []
    visit(tree, "text", (node: Text) => {
      // collect data from the tree...
      items.push(node.value)
    })
    file.data.myCustomData = items
  }
}

Registering your plugin

After creating your plugin, add it to the unified pipeline in packages/nuartz/src/markdown.ts:

import { remarkMyPlugin } from "./plugins/my-plugin.js"
 
// Add it to the chain:
const file = await unified()
  .use(remarkParse)
  // ... existing plugins ...
  .use(remarkMyPlugin)      // <-- add here
  .use(remarkRehype, { allowDangerousHtml: true })
  // ... rehype plugins ...
  .process(body)

Remark plugins must be added before remarkRehype. Rehype plugins must be added after it.

Testing

Each plugin has a corresponding test file (e.g., callout.test.ts). Follow the same pattern:

import { describe, it, expect } from "vitest"
import { unified } from "unified"
import remarkParse from "remark-parse"
import remarkStringify from "remark-stringify"
import { remarkMyPlugin } from "./my-plugin"
 
describe("remarkMyPlugin", () => {
  it("transforms dashes to em-dashes", async () => {
    const result = await unified()
      .use(remarkParse)
      .use(remarkMyPlugin)
      .use(remarkStringify)
      .process("hello -- world")
 
    expect(String(result)).toContain("\u2014")
  })
})

Run tests with:

cd packages/nuartz
bun test

Using existing unified plugins

You don't have to write everything from scratch. The unified ecosystem has hundreds of plugins. Browse remark plugins and rehype plugins for what's available.

To add an existing plugin:

cd packages/nuartz
bun add remark-emoji  # example

Then register it in the pipeline as shown above.

Linked from (6)