Creating Plugins
Build custom Grafos plugins with remark/rehype transforms, graph modifications, and UI components.
This guide walks you through building a Grafos plugin from scratch. By the end, you will understand the plugin interface, how to add markdown transforms, modify graph data, and publish your plugin for others to use.
Plugin Anatomy
A Grafos plugin is a function that returns an object conforming to the GrafosPlugin interface. The function pattern lets plugins accept configuration options:
import type { GrafosPlugin } from 'grafos'
interface ReadingTimeOptions {
wordsPerMinute?: number
}
export function grafosReadingTime(
options: ReadingTimeOptions = {}
): GrafosPlugin {
const { wordsPerMinute = 200 } = options
return {
name: 'grafos-reading-time',
afterContent: ({ content }) => {
const words = content.split(/\s+/).length
const minutes = Math.ceil(words / wordsPerMinute)
return <p className="text-sm text-muted">{minutes} min read</p>
},
}
}
The name field is required and should be unique. It is used in error messages and debug logging.
Every other field is optional. You only implement the hooks your plugin needs. A plugin that only adds a remark transform does not need to implement afterContent, and vice versa.
The full GrafosPlugin interface:
interface GrafosPlugin {
name: string
remarkPlugins?: RemarkPlugin[]
rehypePlugins?: RehypePlugin[]
transformGraph?: (graph: GraphData) => GraphData
head?: () => React.ReactNode
beforeContent?: (props: PageProps) => React.ReactNode
afterContent?: (props: PageProps) => React.ReactNode
}
Remark & Rehype
The most common plugin use case is adding custom transforms to the markdown pipeline. Grafos uses the unified ecosystem (remark for markdown, rehype for HTML), so any existing unified plugin works.
A plugin that adds a remark plugin to auto-link headings:
import type { GrafosPlugin } from 'grafos'
import remarkAutolinkHeadings from 'remark-autolink-headings'
export function grafosAutolinkHeadings(): GrafosPlugin {
return {
name: 'grafos-autolink-headings',
remarkPlugins: [remarkAutolinkHeadings],
}
}
A plugin that adds a rehype plugin with options:
import type { GrafosPlugin } from 'grafos'
import rehypeExternalLinks from 'rehype-external-links'
export function grafosExternalLinks(): GrafosPlugin {
return {
name: 'grafos-external-links',
rehypePlugins: [
[rehypeExternalLinks, { target: '_blank', rel: ['noopener'] }],
],
}
}
Plugins from all Grafos plugins are concatenated in order and appended after the built-in transforms. The processing order is:
- Built-in remark plugins (wikilinks, math, callouts)
- Config-level
markdown.remarkPlugins - Plugin-level
remarkPlugins(in plugin array order) - Built-in rehype plugins (syntax highlighting, KaTeX)
- Config-level
markdown.rehypePlugins - Plugin-level
rehypePlugins(in plugin array order)
This means your plugin's remark transforms see the output of built-in transforms, and your rehype transforms see the output of built-in rehype transforms.
Graph Transform
The transformGraph hook lets you modify the graph data after it is generated from content files. This is useful for adding computed properties, filtering nodes, or injecting virtual nodes.
A plugin that adds a "tag cloud" node connected to all pages with a specific tag:
import type { GrafosPlugin, GraphData } from 'grafos'
export function grafosTagNodes(tags: string[]): GrafosPlugin {
return {
name: 'grafos-tag-nodes',
transformGraph: (graph: GraphData) => {
for (const tag of tags) {
const tagNodeId = `tag:${tag}`
graph.nodes.push({
id: tagNodeId,
label: `#${tag}`,
color: '#f59e0b',
})
for (const node of graph.nodes) {
if (node.tags?.includes(tag)) {
graph.edges.push({
source: node.id,
target: tagNodeId,
})
}
}
}
return graph
},
}
}
The transformGraph function receives the full GraphData object and must return a (possibly modified) GraphData object. You can mutate the input directly or return a new object.
Adding many virtual nodes and edges can impact graph rendering performance. The force simulation and instanced rendering are optimized for the typical case of one node per page. If your plugin adds hundreds of extra nodes, test the graph performance on your target content size.
Publishing
If your plugin is useful to others, consider publishing it to npm. Follow these conventions:
Naming. Use the grafos- prefix for community plugins (e.g., grafos-reading-time, grafos-mermaid). Official plugins use the @grafos/ scope.
Package structure. Export the plugin factory function as the default or named export:
// src/index.ts
export { grafosReadingTime } from './plugin'
export type { ReadingTimeOptions } from './plugin'
Peer dependencies. Declare grafos as a peer dependency so your plugin uses the same version as the user's project:
{
"name": "grafos-reading-time",
"peerDependencies": {
"grafos": "^1.0.0"
}
}
Documentation. Include a README with installation instructions, configuration options, and usage examples. Show how to add the plugin to grafos.config.ts.
Type safety. Export TypeScript types for your plugin's options. Users of defineConfig() will get autocompletion for your plugin's configuration.
You do not need to publish to npm to use a plugin. You can define plugins in a local file and import them directly in your config. Publishing is only necessary when you want to share with others.
For theme customization, see theme. For plugin development, see creating-plugins. For content organization, see content-directory.
See creating-plugins for a guide on building your own plugins.