Stedefast

Plugins

Writing a Plugin

3 min read

A Stedefast plugin wraps one or more remark/rehype packages in a simple object. This guide covers four levels of plugin complexity.

Part 1: Wrapping an existing package

The simplest plugin wraps a package that already exists in the unified ecosystem:

import remarkSmartypants from "remark-smartypants";
import type { StedefastPlugin } from "@stedefast/core";

export function SmartypantsPlugin(): StedefastPlugin {
  return {
    name: "smartypants",
    remarkPlugins: [
      [remarkSmartypants, { dashes: "oldschool" }],
    ],
  };
}
// stedefast.config.ts
plugins: [SmartypantsPlugin()],

That's it. Any remark or rehype plugin from the unified ecosystem works the same way.

Part 2: Writing a remark plugin

Remark plugins operate on the Markdown AST before it is converted to HTML. Use unist-util-visit to walk the tree and transform nodes.

Here's a plugin that wraps every external link in a <span class="external-link">:

import { visit } from "unist-util-visit";
import type { Root } from "mdast";
import type { Plugin } from "unified";
import type { StedefastPlugin } from "@stedefast/core";

const remarkExternalLinks: Plugin<[], Root> = () => (tree) => {
  visit(tree, "link", (node) => {
    const href = node.url;
    if (href.startsWith("http://") || href.startsWith("https://")) {
      // Add data property — rehype will convert this to an attribute
      node.data = node.data ?? {};
      node.data.hProperties = {
        ...((node.data.hProperties as Record<string, unknown>) ?? {}),
        target: "_blank",
        rel: "noopener noreferrer",
        class: "external-link",
      };
    }
  });
};

export function ExternalLinksPlugin(): StedefastPlugin {
  return {
    name: "external-links",
    remarkPlugins: [remarkExternalLinks],
  };
}

Part 3: Writing a rehype plugin

Rehype plugins operate on the HTML AST after conversion from Markdown. Use hast-util-visit to walk <element> nodes.

Here's a plugin that adds loading="lazy" to every <img>:

import { visit } from "unist-util-visit";
import type { Root, Element } from "hast";
import type { Plugin } from "unified";
import type { StedefastPlugin } from "@stedefast/core";

const rehypeLazyImages: Plugin<[], Root> = () => (tree) => {
  visit(tree, "element", (node: Element) => {
    if (node.tagName === "img") {
      node.properties = {
        ...node.properties,
        loading: "lazy",
        decoding: "async",
      };
    }
  });
};

export function LazyImagesPlugin(): StedefastPlugin {
  return {
    name: "lazy-images",
    rehypePlugins: [rehypeLazyImages],
  };
}

Part 4: Injecting head assets

Use headAssets to inject scripts or stylesheets into every page's <head>:

export function PrismPlugin(): StedefastPlugin {
  return {
    name: "prism",
    headAssets: {
      scripts: ["https://cdn.jsdelivr.net/npm/prismjs/prism.min.js"],
      styles: ["https://cdn.jsdelivr.net/npm/prismjs/themes/prism-dark.min.css"],
    },
    // rehypePlugins: [...] add class="language-*" if needed
  };
}

Assets are deduplicated — if two plugins inject the same URL, it only appears once in the rendered <head>.

Testing a plugin

Test plugins using parseMarkdown() directly from /content:

import { describe, it, expect } from "vitest";
import { parseMarkdown } from "/content";
import { LazyImagesPlugin } from "./lazy-images.js";

describe("LazyImagesPlugin", () => {
  it("adds loading=lazy to images", async () => {
    const plugin = LazyImagesPlugin();
    const { html } = await parseMarkdown(
      "![alt text](image.png)",
      { rehypePlugins: plugin.rehypePlugins ?? [] },
    );
    expect(html).toContain('loading="lazy"');
  });
});

Publishing a plugin

Publish plugins to npm with a stedefast-plugin- prefix for discoverability:

# Package name convention
stedefast-plugin-prism
stedefast-plugin-reading-time
stedefast-plugin-copy-code

The package should:

  • Export a factory function returning StedefastPlugin
  • Set peerDependencies: { "/core": "*" }
  • Use "type": "module" and build with tsup
  • Include usage docs in README.md