Summary

Use Notion as a CMS for your Netlify site, with a template and publishing script you can copy.

Notion makes your content pipeline agent-operable: agents research, outline, and assemble metadata where you write.

A Node script renders each post to styled HTML, auto-deployed through Netlify.

What "Notion as a CMS" means, and why I built my own

Using Notion as a CMS means you write in Notion and render it to your own site. You don't let a tool host your Notion pages for you.

That difference splits the field into two camps:

  1. Hosted wrappers like Super, Bullet, and Feather. They point a domain at your Notion pages and style them. Easy, but you pay monthly and you don't own the output.
  2. A build step that pulls your content through the Notion API and renders it to files you control.

I'm in the second camp, for three reasons. I like writing in Notion. I like that my site auto-deploys from GitHub through Netlify. I don't want to pay a monthly fee or hand over the HTML my site serves.

So I wrote a script. It reads a Notion database and renders each post to a static HTML page. No framework, no server, no build pipeline. One Node file, about 500 lines.

If you need newsletters, memberships, or publishing the instant you hit save, this isn't it. Use Ghost. If you want to write in Notion and own a fast, free, static site, keep reading.

How it's wired: Notion, one script, Netlify

Three pieces:

  1. A Notion database, where each row is a post.
  2. A Node script, publish.mjs, that reads the database and writes static HTML.
  3. Netlify, which redeploys the site every time I push to GitHub.

The data moves one way: Notion → script → static files → GitHub → Netlify.

The Notion template

The CMS is a Notion database. Each row is a post. The columns are what a page needs:

  • Title, Slug, Status, Author, and Published date.
  • SEO title, Meta description, Canonical URL, and a No-index checkbox.
  • Cover and Social images, for link previews.

You write the post in the row's body like any Notion page. Headings, lists, callouts, quotes, code blocks, images, tables, footnotes. The script renders all of them.

One convention worth knowing: a heading named "Summary" at the top of the body becomes the TLDR box on the published page. Everything under it, until the next heading, is the summary.

I published the database as a template. Duplicate it and you get the same columns and Status options.

The publish script

publish.mjs is one file with a single dependency: @notionhq/client.

It does the unglamorous work:

  • Queries the database and pulls every block of each post, recursively, so nested lists and toggles come through intact.
  • Converts Notion blocks to clean HTML. No react-notion-x, no heavy renderer. Just the markup I want.
  • Downloads every image into the post's own assets folder and serves it from your site, so Notion's image URLs (which expire after about an hour) never break a post.
  • Builds a table of contents, a sources list from footnotes, a read-time estimate, and the SEO and link-preview tags from the row's columns.
  • Regenerates sitemap.xml.

Your site's details live in one site.config.json: domain, author, where posts go. Your Notion token is an environment variable, never committed, so the whole folder is safe in a public repo.

Then you run one command: npm run publish. It writes the HTML locally. Nothing is live yet.

Netlify deploys from GitHub

This part I didn't build. Netlify already does it.

Point Netlify at your GitHub repo once. After that, every push triggers a deploy. There's no build command to set, because the script already ran on my machine and committed plain HTML. Netlify just serves the files.

So publishing is: run the script, read the diff, commit, push. The push is the deploy.

Publishing a post from start to finish

Start to finish, writing and shipping a post looks like this.

Write it in Notion and mark it Ready

I write the post in its Notion row. When it's done, I set Status to Ready.

Status is the publish gate. The script only renders posts marked Ready or Published. A post left in Draft is invisible. Nothing reaches the site by accident.

Run the publish script

I run npm run publish locally.

It renders the Ready and Published posts, updates the homepage and sitemap, and prints what it wrote. Then I open the local preview and read the post the way a visitor will.

Push, and Netlify ships it

If it looks right, I commit and push.

Netlify picks up the push and deploys. A minute later the post is live at its slug. If something is wrong, it's a normal git diff I can fix before pushing. The review step is part of the workflow, not bolted on.

Letting an agent run the pipeline

Once your CMS is a database and a script, an agent can operate it.

My posts don't start as a blank Notion page. I run Claude Code skills that do the early work:

  • One researches the topic and writes a brief into the row.
  • One turns the summary into an outline and drafts the SEO title and meta description.
  • One runs the publish script and checks the rendered pages.

I still write the post. But the research, the structure, the metadata, and the publishing are agent-driven. The database is the shared interface between the agent and me. This post went through exactly that path.

A Notion-backed CMS is already an agent-operable content system. The script just makes the last step concrete.

Clone the template and the code

Everything is public. To run the same setup:

  1. Duplicate the Notion template.
  2. Clone the repo: pkemp-ai/notion-netlify-blog.
  3. Create a Notion integration, share your database with it, and set the token as an environment variable.
  4. Edit site.config.json with your domain and database IDs.
  5. Write a post, set Status to Ready, run npm run publish, and push.

Connect the repo to Netlify once, and that's the whole loop.

Want to see the output first? This post, and the rest of the site, is the live example.