LOADING
438 words
2 minutes
Building a Blog with Notion as Your CMS

If you’ve ever wished you could just write in Notion and have your blog update automatically, you’re not alone. Notion’s rich editor, database features, and collaborative tools make it an ideal CMS — if you can bridge the gap to your static site.

In this guide, we’ll walk through setting up Notion as a headless CMS for an Astro blog.

Why Notion as a CMS?#

Most headless CMS platforms (Contentful, Sanity, Strapi) are powerful but come with a learning curve and often a price tag. Notion offers something different:

  • Familiar interface — If you already use Notion for notes, you know the editor
  • Rich content — Tables, callouts, toggles, embedded media
  • Database views — Filter by status, category, or publish date
  • Free tier — More than enough for a personal blog
  • Collaboration — Invite editors without teaching them a new tool

Architecture Overview#

Here’s how the pieces fit together:

┌─────────┐ Webhook ┌──────────────┐ Dispatch ┌─────────┐
│ Notion │ ──────────────► │ CF Worker │ ───────────────► │ GitHub │
│ DB │ │ (validate) │ │ Actions │
└─────────┘ └──────────────┘ └────┬────┘
sync + build
┌────▼────┐
│ CF │
│ Pages │
└─────────┘

When you change a page’s status to “Publishing” in Notion, a webhook fires. A Cloudflare Worker validates the request and triggers a GitHub Actions workflow that syncs content and deploys.

Setting Up the Notion Database#

Create a new database in Notion with these properties:

PropertyTypePurpose
TitleTitlePost title
StatusSelectPublishing workflow
CategorySelectBlog category
TagsMulti-selectPost tags
Publish DateDateWhen to publish
DescriptionTextSEO description
Hero ImageFiles & MediaCover image
SlugTextURL slug

The Status property drives the entire workflow:

Draft → Ready → Publishing → Synced
Sync Failed (on error)

Connecting Notion to Your Blog#

First, create a Notion integration and grab your API token:

sync-notion.mjs
import { Client } from '@notionhq/client';
const notion = new Client({ auth: process.env.NOTION_TOKEN });
const response = await notion.databases.query({
database_id: process.env.NOTION_DATABASE_ID,
filter: {
property: 'Status',
select: { equals: 'Publishing' },
},
});

For each page, we convert Notion blocks to Markdown:

async function pageToMarkdown(pageId: string): Promise<string> {
const blocks = await notion.blocks.children.list({
block_id: pageId,
});
return blocks.results
.map(block => blockToMarkdown(block))
.join('\n\n');
}

The Webhook Handler#

The Cloudflare Worker validates incoming webhooks using HMAC-SHA256:

async function verifySignature(
body: string,
signature: string,
secret: string,
): Promise<boolean> {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify'],
);
const sig = hexToBytes(signature);
return crypto.subtle.verify('HMAC', key, sig, encoder.encode(body));
}

Security note: Always validate webhook signatures. Without verification, anyone could trigger rebuilds on your site.

What’s Next#

In the next post of this series, we’ll cover the developer tools that make this workflow even smoother — from CLI scaffolding to automated deployments.


TL;DR: Notion + Cloudflare Workers + GitHub Actions = a free, fast, and familiar blogging workflow. Write where you think, publish where you want.

Building a Blog with Notion as Your CMS
/twilight/posts/building-a-blog-with-notion-cms/
Author
Twilight
Published at
2026-03-10
License
CC BY-NC-SA 4.0

Some information may be outdated