← Blog

Building · Notion · Cloudflare · Automation · Web Dev

I Built a Blog That Publishes From Notion

August 14, 2026 · 8 min read

My blog has no editor.

No admin panel. Nothing that looks like a place to write. I don’t want to deal with that.

I mean, I write in Notion, always been that way. Huh, it occurred to me, do I really need to reinvent the wheel?

What if, I flip a dropdown from Draft to Published.

Then some time later, usually within the hour and sometimes within a minute, the post shows up on my actual website.

That would feel magical. But maybe it’s difficult?

It isn't.

I’ll go over the parts, piece by piece.

Where the Writing Actually Happens

The source of everything is a Notion database, which is really just a (database) table.

Each row is a post.

Alongside the actual writing, a handful of columns hold the metadata a website needs:

image.png

FieldPurpose
TitleThe post title
DescriptionShort summary
CategoryWhere the post belongs
TagsTopics associated with the post
Publish DateWhen it was published
StatusDraft or Published

And one column matters more than the rest:

Status

It has exactly two options:

Draft

Published

That single field is the whole publishing mechanism.

Nothing else in this system asks a more complicated question than:

Is this row's Status set to Published?

Whatever you build something like this on top of, whether it's a spreadsheet, a headless CMS, or a plain folder of files, you want exactly one field like this.

Unambiguous. Machine-readable. Impossible to misinterpret.

Not a naming convention you're trusting yourself to remember.

The Site Itself, and Why It Needs Help

The site is built with Next.js running on Cloudflare Static Pages , the site being exported as a fully static set of files.

That distinction matters more than it sounds.

A static site has no process running in the background.

Nothing can wake up, check Notion, and notice that something changed.

Every page that exists, exists because it was generated ahead of time.

So there needs to be something between:

"I published something in Notion."

and

"The website shows it."

Something outside the website has to do the work, either on a schedule or in response to an event, and then tell the site to rebuild itself from scratch.

The Script That Actually Reads Notion

That work is handled by a small script that runs right before the site builds.

It:

  1. Authenticates to Notion using an API key scoped to this integration.
  2. Finds rows where Status = Published.
  3. Reads the actual Notion page content.
  4. Converts the Notion blocks into the format the site expects.
  5. Writes each post out as its own file.
  6. Removes files for posts that are no longer published.

The important part is that it isn't just reading metadata.

It reads the actual Notion blocks:

  • Paragraphs
  • Headings
  • Lists
  • Code blocks
  • Links
  • Other supported content

Then it turns those blocks into something the site's existing templates already know how to render.

The part that's easy to get wrong

What happens when a post is removed?

Or when I change it from Published back to Draft?

The first version of my script only ever added new files.

Nothing ever told an old one to leave.

So I changed the sync logic to compare:

What Notion says should exist

against

What currently exists locally

Anything left over from a previous run that isn't in the current set gets deleted.

So now:

Publish in Notion

→ Post appears on the site.

Un-publish in Notion

→ Post disappears from the site.

That's the difference between an importer and a real mirror.

If you're syncing data between two systems, don't just ask how new things get added. Ask what happens when something disappears.

How Notion Becomes an Actual Blog Post

So how does a Notion page actually become the page you're reading right now?

Well there are two separate pipelines happening.

First Pipeline: Notion → Markdown

First, the build script talks to Notion and pulls the actual page content as Notion blocks.

A few packages handle that part:

PackageWhat it does
@notionhq/clientTalks to Notion's API and fetches the database and page content
notion-to-mdConverts Notion blocks into Markdown
gray-matterReads the metadata stored in Markdown frontmatter

So the flow looks roughly like this:

Notion page

→ Notion API

→ Notion blocks

notion-to-md

→ Markdown file

At this point, the Notion page has become a normal Markdown file sitting in the repository.

Second Pipeline: Markdown → The Page You Actually See

The second pipeline happens when the site builds.

That Markdown file gets passed through a few more tools that turn it into the actual HTML and styling used by the blog:

PackageWhat it does
react-markdownTurns Markdown into React components
remark-gfmAdds GitHub-flavored Markdown features like tables and strikethrough
rehype-slugGives headings stable IDs for anchor links
unist-util-visitPowers the small custom plugin that turns callout syntax into styled callout boxes
shikiSyntax-highlights code blocks at build time

The important part about Shiki is that the highlighting happens during the build.

The visitor doesn't need to download a syntax-highlighting library just to see a colored code block.

The HTML is already prepared when the page is generated.

So the complete content pipeline is basically:

Notion_API_Deployment-2026-08-14-151137.png

And that's actually one of the things I like most about this setup.

I'm not rendering Notion directly on every page request.

I'm not shipping a Notion SDK to the browser.

I'm not making visitors wait for an API call to Notion.

Notion is simply the writing interface and source of truth.

By the time someone visits the blog, all of that work has already happened.

They just get a static page. Since again we’re also on Cloudflare Static Pages.

Two Different Things Can Trigger a Rebuild

Once the files exist, something still has to actually rebuild and redeploy the site.

I use two separate triggers for that.

The Fast Path

A small always-on listener, built as a Cloudflare Worker, waits for Notion to send a signed notification whenever something in the database changes.

Notion_API_Deployment-2026-08-14-151440.png

That's it.

It's fast because it's event-driven.

Nothing happens until something actually changes.

The Slow Path

A scheduled job runs once an hour, regardless of whether anything changed, and pings the exact same rebuild trigger.

Below is my .github/workflow on my repo.

It needs that CLOUDFLARE_DEPLOY_HOOK_URL so Cloudflare Pages will rebuild and redeploy the site now. (I mean, when invoked)

image.png

Fast path: live within about a minute.

Slow path: live within an hour, even if the fast path fails.

I think that’s good enough, no?

The Whole Shebang

Notion_API_Deployment-2026-08-14-145021.png

In one sentence

I write in Notion → Notion tells the system something changed → the site rebuilds → the sync script pulls Published posts → the updated site goes live.

What's Actually Doing the Work?

Yeah it’s everything below that’s working hand-in-hand.

PieceWhat it does
Next.jsBuilds the static site
Notion APIProvides the source content
TypeScriptRuns the sync logic
Notion official clientTalks to the Notion API
Notion block converterTurns Notion content into site-ready content
Syntax highlighterHighlights code during the build
Cloudflare WorkerHandles the fast rebuild trigger
GitHub ActionProvides the hourly backup trigger
Cloudflare PagesBuilds and serves the final site

The Engineering Was in the Boring Parts

No fancy tools right.

It was in getting the cogs correct:

One source of truth

Notion decides whether a post exists on the site.

One unambiguous switch

Status = Published means publish.

Addition and removal

The sync doesn't just create files. It removes things that are no longer supposed to exist.

A fast path with a safety net

The webhook gets posts live quickly.

The hourly trigger makes sure a silent failure doesn't stay silent forever.

Static sites need an external trigger

Because there's no server sitting around watching for changes, something else has to tell the build system when to work.

And That's the Whole Trick

I don’t want to deal with CMS.

I don’t want to deal with an Admin Panel.

I don’t want to put a custom editor on my Blog.

I don’t want to go to a terminal instance just to publish a post.

There's just a Notion page, a status dropdown, and a handful of small pieces doing exactly what they're supposed to do.

I write.

I publish.

The website updates itself.

Notion is where I write.

blog.neilmarc.com is where it ends up.

Easy, peasy, lemon, squeezy ryt?

Written by Neil Marc

Software engineer · Product builder

neilmarc.com