🧑🏼‍🏭 Don't mind us, we are still working on this site using NotionCMS! Like this project? Sponsor us on GitHub: ⚡️⚡️⚡️ Get Started ⚡️⚡️⚡️
⏳: ~6 min read.
🗺️: current location: /notion-cms/plugins/building-a-plugin

Building a NotionCMS plugin

NotionCMS plugins are a powerful way to extend what you can do with the Notion as headless CMS model. Let’s take a tour of the NotionCMS plugin API, how they slot into the overall architecture and most importantly, how you can build your own to take advantage of Notion by leveraging NotionCMS tooling any way you please.

If you haven’t read the getting started guide, you should start there!

Check if existing plugins will work

Here is a list of NotionCMS core plugins:

  • Head/SEO
  • Linker
  • Images

If none of those do the trick, NotionCMS provides a friendly plugin API so read on and craft your own!

How NotionCMS works

If you are familiar with the Notion API SDK you know that you can query a database but the results of that query doesn’t give you everything you need. You also have to query individual pages to gather the ‘blocks’ of content that get transformed into HTML.

NotionCMS peforms those queries in two stages and takes 2 passes at putting together the final tree structure. On the first pass we query the database and link together the parent pages to their children to build the skeleton of the tree. On the second pass we pull down the block content for each page and stash it in the correct location in the tree. This multi-pass approach means that there are a few places where you may want to hook into to do some data transformations, so we’ll go through each hook in detail next.

How NotionCMS Plugins work

Notion plugins run in almost the sequence they are defined in the NotionCMS configuration and allow you to pull in common data at certain places within the phases of putting the CMS tree together. Depending which hook you use, you will have access to different data. Plugins of the same hook will run one after another and feed the previous plugins output into the next running plugin.


Tip: NotionCMS plugins serialize/deserialize functions too so you can save those as data in the Page structure. This is helpful for things like building template functions etc (see ncms-plugin-templates for an example). Keep in mind the general limitations of serialized functions. For best results they should be pure functions.

If you are using Typescript with a capital T, here are the Plugin types you will see:

type PluginPassthrough = Blocks | CMS | Page | string

interface Plugin {
  name: string,
  hook: 'pre-tree' | 'pre-parse' | 'post-parse' | 'during-tree' | 'post-tree'
  exec: (context: PluginPassthrough) => PluginPassthrough

Plugin Hooks

There are 5 plugin hooks that you can use in NotionCMS and each is best used for accomplishing a particular goal.

  1. pre-tree
  2. during-tree
  3. pre-parse
  4. post-parse
  5. post-tree
  6. import
  7. export


The pre-tree hook runs after we’ve hit the Notion database and assembled the tree skeleton but before we’ve pulled the content in at all. Data you have available here are: routes, name, slug, authors, tags, otherProps, and possibly the coverImage if one is set.

This is probably the hook you’ll reach for the least often but if you wanted to perform tree-level changes before any heavy data insertion happens this might be where you do it. Filtering data from the tree here will mean that it won’t get pulled in during the next hooks so do your filtering operations here.

The input argument available for this hook is the CMS object.


The during-tree hook runs during the process of pulling in the block content from Notion. A plugin defined to run on this hook will run once per node in the tree and have access to that node’s data. This includes the content pulled from Notion and at this point already parsed as HTML (if you want to do pre- or post- parsing operations only, take a look at the post- and pre-parse hooks below).

The data you have available at your disposal here also includes an array of the node’s ancestors in _ancestors. This is useful if you want to check for a property existing only on an ancestral node in order to perform some action on the current node.

The input argument to this hook is a Page object.


The pre-parse hook runs just before the Notion blocks are transformed into HTML.

The input argument to this hook is a Blocks object. This refers to the blocks that come directly from the Notion API, so anything you might want to do to the raw Notion data, you can do here.


The pre-parse hook runs just after the Notion blocks are transformed into HTML. So if you don’t care about blocks or any of the other higher-level tree node data and want to perform just some operations on the HTML string, this is the hook for you!

The input argument to this hook is a string comprised of html.


The post-tree hook is the final hook and gives you access to the entire assembled tree structure, content in the form of HTML and all. This would be the best hook to use for any sort of cleanup, restructuring that requires information embedded inside the blocks content itself (otherwise use pre-tree).

The input argument available for this hook is the CMS object.


// tbd


// tbd

Order of Plugin Operations

As mentioned before, plugins run in the sequence they are defined, however, the hook they use in their definitions plays a big role in when they run so keep that in mind when ordering them. For instance, you might have an order like this:

const cmsWithPlugins = new NotionCMS({
    plugins: [
        pluginA(), // hook is 'during-tree'
        pluginB(), // hook is 'pre-tree'
        pluginC() // hook is 'during-tree'

Then the order the plugins will actually run is this: pluginB, pluginA, pluginC, since the pre-tree hook runs before the during-tree one.

Plugin Structure

Here is the general structure you will follow for plugins:

import {/*some types*/} from '@agency-kit/notion-cms'

export default function () {
  return {
    name: 'ncms-plugin-custom',
    hook: 'during-tree',
    exec: (context: PageContent): PluginPageContent => {
            // Do your sweet sweet transforms here

Where to go from here

We provide a recommended starter kit for writing NotionCMS plugins in TS. You can find that here: https://github.com/agency-kit/notion-cms-plugin-starter (still private as of writing)

We have a convention for naming NotionCMS plugins: ncms-plugin-x where x is the descriptive name of the plugin you are publishing. This makes it easier for others to find helpful plugins and grow the community!

we make notionware. 2023 \c\