Building a Blog with Notion as Your CMS

发布于:2026-03-10 · #notion

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:

plaintext
UTF-8|11 Lines|
┌─────────┐     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:

plaintext
UTF-8|3 Lines|
Draft → Ready → Publishing → Synced

              Sync Failed (on error)

Connecting Notion to Your Blog

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

TypeScript
UTF-8|12 Lines|
// 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:

TypeScript
UTF-8|9 Lines|
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:

TypeScript
UTF-8|17 Lines|
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.