Back home
A small possum holding a balloon is sitting on the ground in a park; it's a depiction of Eleventy's mascot. To its left, a girl holding the Notion logo happily waves walking towards the possum. The girl is drawn in the same style as the one used on Notion's own website.

Notion meets Eleventy

The eleventy-from-notion plugin just got a major update. More features, more flexibility, and a better developer experience. Here's how!

Before we get started, let me describe what the eleventy-from-notion plugin does. Essentially, it lets Notion serve as your CMS; the plugin imports your pages and front matter as regular text files into your Eleventy site. In other words: you get a good writing experience, while enjoying Eleventy's flexibility and customizability.

First, some high-level details that you'll probably want to know before deciding to get your hands dirty.

Now, let's walk you through setting things up. I'll assume you have a personal Notion account ready.

Getting set up

First things first: let's create some data in Notion. The plugin reads your posts from a single data source. As such, we need a table. Each row in this table is a page that'll be imported as a text file into your Eleventy codebase. Let's also create some columns; these'll map to front matter, and is even rendered as such when viewing an individual page in Notion.

A rendition of an example table in Notion, with the default "Name" column, and additional "URL" and "Status" columns. There are three rows, the first has "Dogs" in the name column, the URL is "/dogs/" and its status is tagged "published". The two other rows have similar data in them, but for "Cat" and "Fish".

As a start, a permalink column of sorts is recommended, since the imported files are named after the Notion page IDs and that's probably not what you want your URLs to look like. It doesn't matter what you name this column; we'll map this to the permalink front matter key later. Then create a new page, fill out the columns and insert some example content for testing purposes. Of course, if you have pages already, you can skip this - the plugin won't be able to make changes to your pages, so it is completely safe to use existing pages while setting things up.

Once you've got some data in Notion, it's time to tell Notion about this new integration. The "internal integration" is what'll provide the plugin with access to the Notion API, allowing it to read your pages under the specified table (i.e. data source). We'll go through the whole process here, but if you'd like, you can also refer to Notion's documentation on how to create a Notion integration.

To create a new internal integration, head to My integrations and click the nice big "New integration" button.

A button as found in Notion containing a "+" with the text "New integration".

Next, give your new integration a name (e.g. "Eleventy123") and select the workspace that you want to import pages from. The integration's "Type" should be "internal". You can add a logo if you'd like, but this is optional.

Now, there's some configuration left to do. This plugin only needs read access, so in the "Content Capabilities" section, tick only "Read content". That's really all the permissions the plugin needs.

The permission section as displayed in Notion's integration settings. There are three sections; the first has a header "Content capabilities". There are three options, but only the first, "Read content", is checked. The other two options are "Update content" and "Insert content", both are unchecked. The second section is titled "Comment capabilities". It includes two options, for reading and inserting comments, neither of which are selected. The last section, "User capabilities", has three options, but these are radio buttons. The first option is selected: "No user information". The other two options allow reading user information without or with email addresses respectively.

Now, on the same page, you should be able to find your "Internal Integration Secret". Copy this, but keep it safe, as this is a password of sorts that can read your Notion content.

Fortunately, Notion integrations don't apply to your workspace as a whole; you need to add it to a page, giving it access to that page and all below it. To do this, head back to your page containing the data source (your table of pages) and click the three dots in the top-right corner to drop down a menu. Here, follow the "Connections" item and search for the integration by its name. Select it, confirm the connection, and that's it! We're all set from the Notion side of things.

Your Notion config

We've now reached the part where we get to write some code! As a first step, install the plugin using

# For Deno
deno add jsr:@vrugtehagel/eleventy-from-notion
# For NPM
npx jsr add @vrugtehagel/eleventy-from-notion
# For Bun
bunx jsr add @vrugtehagel/eleventy-from-notion

With the package installed, import it into your Eleventy config file, and add the plugin like so:

import EleventyFromNotion from '@vrugtehagel/eleventy-from-notion'

export default function(eleventyConfig){
	// …
	eleventyConfig.addPlugin(EleventyFromNotion, {
		config: './notion.config.js'
	});
	// …
}

The only option to pass is a path to your Notion config; it is a path relative to the current working directory, defaulting to ./notion.config.js. Thus, in most cases, you can put this notion.config.js file alongside your eleventy.config.js and omit the config option altogether.

The syntax in your Notion config closely resembles that of an Eleventy config. You export a default function, which receives a config parameter with methods to configure how your pages are imported. Let's first do the important stuff; connecting to your Notion pages.

import * as process from 'node:process'

export default function(notionConfig){
	notionConfig.setIntegrationSecret(process.env.YOUR_SECRET)
	notionConfig.setDataSourceId('...')
	notionConfig.setOutputDirectory('./path/to/output/')
}

Your integration secret is the API key you copied earlier. Don't paste this into your Notion config as a string literal; it's essentially a password, treat it like one! I recommend you set up an environment variable and pass it using process.env. as shown. The data source ID specifies which Notion table to pull pages from. You can find this by clicking the table's "settings" icon (to the left of the "New" button), hitting "Manage data sources", then locate your table. Now open its submenu, which should reveal a "Copy data source ID" button.

A depiction of Notion's somewhat convoluted UI for copying a data source's ID. The top bar represents the toolbar on the data source, and the settings icon is highlighted. Its dropdown, with header "View settings", contains the second-to-last option "Manage data sources", right before the option "Lock database". A new dropdown is depicted titled "Manage data sources", which includes a list of available data source. In this case, only one is shown named "Animals", which refers to the example table from before. On the right of the "Animals" item, three dots reveal yet another short dropdown with the options "Copy data source ID" and "Move to". The former is highlighted.

As for the output directory, your choice should be specified relative to your current working directory. Create the output folder manually if it doesn't already exist; the plugin will not create it for you to avoid accidents.

Now that the plugin can pull your data out of Notion, we need to specify how we'd like to convert the pages into a text format. The way the plugin works is that, for each type of block, rich text or data, it needs a parser and a formatter. If a type is found in any imported pages, but not registered, an error is thrown. Of course, this is tedious work, so by default, the plugin has default parsers for most types, and it exports some plugins packaging formatters for certain language. So, let's say we want to output Markdown files with YAML front matter. We can configure it using:

import * as process from 'node:process'
import {Markdown, Yaml} from '@vrugtehagel/eleventy-from-notion'

export default function(notionConfig){
	// ...
	notionConfig.use(Markdown)
	notionConfig.use(Yaml)
}

There is a chance that, even with the default parsers and the formatters from the plugins, there are things that are not supported. This is because, for some types, there is no sensible way of converting what's in Notion to the format specified. For example, the Markdown plugin does not support formatting toggle blocks, since Markdown as a language does not have a standardized way of expressing this. However, if your posts contain these kinds of unsupported blocks, you can specify your own! Here's an example for the toggle block mentioned before:

export default function(notionConfig){
	// ...
	notionConfig.setBlockFormatter('toggle', block => {
		return `\n::: ${block.content}\n${block.body}\n:::\n`
	})
}

The final step is specifying your front matter. Each column in Notion can be imported, and even page metadata can be pulled in. Let's dive right into an example:

export default function(notionConfig){
	// ...
	notionConfig.importProperty('Name', 'richTitle')
	notionConfig.importProperty('URL', 'permalink')
	notionConfig.importProperty('Status', 'status')
	notionConfig.importMeta('is_locked', 'isLocked')
}

The first parameter is the (case-sensitive) name of the column in Notion, or in the case of metadata, the property name as returned by the Notion SDK. The second parameter is the front matter key to map to.

As with block and rich text types, not all data types are supported by default. The majority of use cases are covered, but it is not always obvious how a data type should be converted to stringified front matter. The "text" column type, for example, accepts rich text. But sometimes, you'll want the plain text instead, for example, for use in a <meta name="description">. This type is converted to a { plain, rich } JavaScript object, so you have access to both.

There's more you can configure, like skipping pages or configuring the options to the underlying Notion client. For detailed documentation about the plugin's functionality, see jsr.io/@vrugtehagel/eleventy-from-notion.

Liftoff

With all the basics in place, your build should now be able to import your pages successfully. Congrats! Happy blogging to you.

If, on the other hand, you're running into strange issues or problems you don't seem to be able to solve, don't hesitate to file a GitHub issue. I'd love to help out and improve the plugin!