Skip to content
Blume
Esc
↑↓navigate↵open⌘Jpreview
Guides

Content sources

Turn a Notion database into a docs site

Keep writing in Notion and publish a searchable docs site from a database, with a Status property that decides what goes live.

By 8 min read

By the end of this guide, your team writes docs in a Notion database and readers get a docs site built from it: a sidebar, search, your own domain, and a Status property that decides which pages go live. Edits in Notion reach the site on the next build, which you'll trigger automatically.

Notion can also publish pages to the web by itself. Notion Sites gives a page a public notion.site address, with optional search engine indexing and custom domains as a paid add-on. If a public Notion page is all you need, that's the simpler path. This guide is for when you want a docs site instead: Notion pages side by side with Markdown pages, docs-style navigation and search, and a site you host and deploy yourself. When engineers own the Markdown half and another team owns Notion, Combine Markdown and Notion in one site adds the ownership rules and CI checks.

Set up the database

Blume reads one Notion database, and each row becomes a page. Create a database (or pick an existing one) with these properties:

PropertyTypeWhat Blume does with it
NameTitleThe page title, and its URL when Slug is empty
DescriptionTextThe page's description, shown under the title and to search engines
SlugTextThe page's path, like getting-started or guides/setup
OrderNumberIts position in the sidebar
StatusStatus or selectWhether it publishes

Only the title is required, and Blume finds the title property whatever it's called. The others are optional, and any other properties you keep for your team, like an owner or a due date, never reach the site.

Status is the publish gate. A page publishes when its Status equals the value you tell Blume means published, and every other value makes it a draft. Notion's default status options are Not started, In progress, and Done, so this guide treats Done as published. Add three or four pages, set most of them to Done, and leave one In progress so you can watch it stay off the site.

Connect Notion

Blume reads the database through the Notion API, with a token from an internal connection (called an integration in older Notion docs). In Notion's developer portal:

  1. Under Internal connections, create a new connection in the workspace that holds your database.
  2. On its Configuration tab, copy the installation access token. Blume only reads, so the connection needs no capability beyond reading content.
  3. On its Content access tab, give it access to your database. You can also do this from the database itself: open the ••• menu, choose Add connections, and pick your connection.

Share the database itself. If the database appears on another page as a linked view, sharing that page isn't enough.

Last, copy the database's ID. Open the database as a full page and copy its link: the ID is the 32-character string in it, before any ?v=.

Add the Notion source

In an empty folder, start a Blume project:

npx blume init

When it asks where your content lives, pick both filesystem and notion. That keeps a local docs/ folder for pages you'd rather write in Markdown, like the home page, and adds @notionhq/client, the Notion SDK the source needs, to your dependencies. In an existing Blume project, install the SDK yourself:

npm install @notionhq/client

Then point the source at your database. Replace the ID with yours, and pick a prefix: every Notion page is served under it.

import { defineConfig } from "blume";
import { filesystem, notion } from "blume/sources";

export default defineConfig({
  title: "Acme Docs",
  content: {
    sources: [
      filesystem({ root: "docs" }),
      notion({
        database: "8f2c1e0a4b7d4f3c9e6a5d2b1c0f9e8d",
        prefix: "handbook",
        publishedValue: "Done",
      }),
    ],
  },
});

publishedValue defaults to Published, so a database on Notion's default statuses publishes nothing until you set it to Done. If your properties have other names, map them with properties, for example properties: { slug: "URL", order: "Position" }. The keys are title, description, slug, order, and status.

The source reads its token from NOTION_TOKEN. Blume loads .env.local for you, so put the token there and keep the file out of Git:

echo "NOTION_TOKEN=paste-your-token-here" >> .env.local
echo ".env.local" >> .gitignore

Check the imported pages

Start the dev server:

npx blume dev

Your Notion pages appear under /handbook/, in a sidebar group of their own beside your Markdown pages, sorted by Order. A page with a Slug is served at that path under the prefix, and one without is served at its title, lowercased and hyphenated. Walk through a few pages and check:

  • Headings. The title is already the page's top heading, so write sections with Notion's Heading 2 and Heading 3. A Heading 1 becomes a second top heading.
  • Callouts, toggles, and columns. Callouts become Blume callouts, toggles become accordions, and columns stay columns. A callout's emoji and color don't carry over.
  • Code blocks. Code keeps its text exactly and its language, so it's highlighted like any code block on the site.
  • Images. Notion's image links are signed and expire, so Blume downloads each image at build time and serves it from your own site. The published pages don't depend on Notion at all.
  • Links. Links keep their target, so a link to another Notion page opens Notion, not your site. Link to the page's path on your site instead, like /handbook/setup.

Paragraphs, lists, to-dos, quotes, dividers, and videos carry over too. Tables, bookmarks, embeds, equations, synced blocks, and child pages don't: they're left out of the page without a warning, so check any page that uses them.

The dev server fetches the database once and then serves its snapshot, so restarting stays fast. After you edit a page in Notion, pull the latest content and the running server reloads:

npx blume sync

To pick up edits on their own while you work, set pollInterval (in seconds) on the source. It only affects the dev server.

Keep drafts off the site

The page you left In progress shows up in blume dev, so you can review drafts before they go out. A production build leaves it out. Build the site and serve the result:

npx blume build
npx blume preview

Open the draft's URL on the preview server: it's a 404, and it's missing from the sidebar, the search index, and the sitemap. Set its Status to Done, build again, and it's there. To share a build that includes drafts for review, run npx blume build --preview and deploy it only behind access protection, never to your public domain.

Deploy

A Notion-backed site builds to static files, so it runs on any static host. This guide uses Vercel: Blume detects the site URL there, and its deploy hooks drive the next step.

  1. Push the project to a GitHub repository.
  2. Import the repository in Vercel. Set the build command to npm run build and the output directory to dist if Vercel doesn't detect them, and use Node.js 22 or later.
  3. In the project's settings, add NOTION_TOKEN as an environment variable, for Production and Preview alike.
  4. Deploy.

The source reads the token while the site builds, not while it serves, so the token never reaches your readers. For another host, the same settings are on the deployment page.

Rebuild when Notion changes

Blume imports Notion at build time, so an edit reaches readers on the next build, not the moment you type it. Give Notion a way to start that build. In your Vercel project's settings, open Git, and under Deploy Hooks create a hook for your production branch. Vercel gives you a URL that starts a build whenever something sends it a POST request. Anyone with the URL can trigger a build, so treat it like a password.

On a paid Notion plan, let Notion call it. Add a database automation with the Property edited trigger on Status, and a Send webhook action with the deploy hook URL. Setting a page to Done then publishes it. A property trigger only watches the property you pick, so edits to the body of a page that's already published won't fire it on their own. Pair it with a schedule, or run the hook by hand.

On any plan, rebuild on a schedule instead. Save the hook URL as a repository secret named VERCEL_DEPLOY_HOOK, then add a workflow that calls it every hour:

name: Rebuild docs from Notion

on:
  schedule:
    - cron: "0 * * * *"
  workflow_dispatch:

jobs:
  rebuild:
    runs-on: ubuntu-latest
    steps:
      - run: curl -fsS -X POST "$DEPLOY_HOOK"
        env:
          DEPLOY_HOOK: ${{ secrets.VERCEL_DEPLOY_HOOK }}

workflow_dispatch adds a button to run it by hand from the repository's Actions tab. Change a page in Notion, run the workflow, and the change is live once the build finishes.

What stays private

Blume can only read what you shared with the connection, and only pages that pass the Status gate are built. Everything in a published page's body becomes public, but the rest of your workspace, the properties Blume doesn't map, and your drafts stay in Notion. So does the token, which lives in your environment variables rather than your repository.

Troubleshooting

No Notion pages show up

Every page is probably a draft. Check that publishedValue matches your published option exactly, capitals included. Without it, Blume looks for Published, which Notion's default statuses don't have. If your status property isn't called Status, name it with properties.status.

A page you published is missing

Its Status doesn't match publishedValue, or the build ran before you changed it. Rebuild, and in dev run npx blume sync, since the dev server serves its snapshot until you do.

Notion can't find the database

The connection doesn't have access to it. Share the database itself with the connection, not a page that shows a linked view of it, and check the ID is the database's and not a view's. Blume passes on Notion's own error in a BLUME_SOURCE_FETCH_FAILED diagnostic.

The build fails with an authorization error

NOTION_TOKEN isn't set where the build runs. Blume fetches Notion before it checks for missing variables, so on a fresh build machine the fetch fails first: BLUME_SOURCE_FETCH_FAILED, with Notion's message that the Authorization header must use the format Bearer <token>. You only see the BLUME_MISSING_SECRET warning when Blume has a snapshot to serve instead, as a dev server that fetched once already does. Locally, check .env.local is in the folder you run Blume from. On your host, add the variable to the environment that ran the build, then redeploy: a changed variable only applies to new builds.

The build says a package isn't installed

@notionhq/client isn't in your dependencies. On a fresh build the fetch fails with BLUME_SOURCE_FETCH_FAILED and a message naming the package and its install command. Install it with npm install @notionhq/client and build again.

The site shows old content with a warning

BLUME_SOURCE_OFFLINE means the fetch failed and Blume served the last snapshot it had instead. The warning includes the reason. A fresh build machine has no snapshot to fall back on, so there the same failure stops the build and your last deployment stays live.

Next step

Publish your Notion docs

Pick notion when init asks where your content lives, then add your database ID to the config.

npx blume init
Read the Notion source docs

A step here not working for you? Report a broken step.

Keep going.More guides.

Upgrade your docs with Blume.

Install today and ship a production-grade docs site in minutes. Free and open source, forever.

npx blume init