Why use Notion as a CMS?
Notion is a great place to write because it’s fast, familiar, and collaborative. Using it as a CMS means:
- You edit content in Notion
- Your app pulls structured fields (title, slug, date, tags, excerpt)
- Your site renders the page content as HTML/Markdown
This works especially well for:
- Personal blogs
- Product docs
- Changelogs
- Simple marketing pages
The Notion setup (the content model)
A simple “Blog” database is usually enough. Typical properties:
- Name (title)
- Slug (text)
- Date (date)
- Tags (multi-select)
- Published (checkbox)
- Excerpt (text)
Tip: Keep “Slug” unique. It becomes your stable URL.
Architecture overview
Your app usually has two jobs:
- Fetch a list of posts (for the home page / archive)
- Fetch a single post by slug (for the post page)
A common flow:
- Query database rows where Published = true
- Sort by Date desc
- Render previews using Name + Excerpt
- On the post page, load the Notion page content and render it
Code snippet: Environment variables
Keep secrets out of code.
# .env
NOTION_API_KEY=secret_xxx
NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxCode snippet: Fetch published posts (Node.js)
This uses the official Notion SDK.
import { Client } from "@notionhq/client";
const notion = new Client({ auth: process.env.NOTION_API_KEY });
const databaseId = process.env.NOTION_DATABASE_ID;
export async function getPublishedPosts() {
const res = await notion.databases.query({
database_id: databaseId,
filter: {
property: "Published",
checkbox: { equals: true },
},
sorts: [
{
property: "Date",
direction: "descending",
},
],
});
return res.results.map((page) => {
const props = page.properties;
return {
id: page.id,
title: props.Name.title?.[0]?.plain_text ?? "Untitled",
slug: props.Slug.rich_text?.[0]?.plain_text ?? "",
excerpt: props.Excerpt.rich_text?.[0]?.plain_text ?? "",
date: props.Date.date?.start ?? null,
tags: props.Tags.multi_select?.map((t) => t.name) ?? [],
};
});
}Code snippet: Fetch a post by slug
export async function getPostBySlug(slug) {
const res = await notion.databases.query({
database_id: databaseId,
filter: {
and: [
{ property: "Published", checkbox: { equals: true } },
{ property: "Slug", rich_text: { equals: slug } },
],
},
});
return res.results?.[0] ?? null;
}Rendering the page content (important part)
The Notion API returns “blocks”, not raw Markdown. To render to the web, you have a few options:
- Convert blocks → HTML (recommended)
- Convert blocks → Markdown
- Hand-roll your own renderer
Popular renderers:
- notion-to-md (Markdown)
- notion-client + custom renderer
- react-notion-x (React)
Code snippet: Convert Notion blocks to Markdown (example)
import { NotionToMarkdown } from "notion-to-md";
const n2m = new NotionToMarkdown({ notionClient: notion });
export async function getPostMarkdown(pageId) {
const mdBlocks = await n2m.pageToMarkdown(pageId);
const mdString = n2m.toMarkdownString(mdBlocks);
return mdString.parent;
}Handling images & files
In Notion, images can be:
- Hosted by Notion (time-limited signed URLs)
- External URLs
For production sites:
- Prefer external image URLs, or
- Download/cache images during build, or
- Re-fetch frequently if you rely on Notion-hosted URLs
Keeping content in sync
Three common approaches:
- Static build (Next.js / Astro): fetch at build time
- ISR / incremental rebuilds: revalidate every N minutes
- Webhook-style sync: store posts in your own DB and update when Notion changes
If you’re starting out, choose (1) or (2).
Slug rules (quick checklist)
- lowercase
- hyphen-separated
- stable (don’t change after publishing)
- unique
Example: notion-integration-as-cms-with-code-snippets
Common pitfalls
- Forgetting to publish (Published checkbox)
- Changing property names (breaks queries)
- Missing slug uniqueness
- Rendering issues (Notion blocks don’t map 1:1 to Markdown)
Conclusion
Notion-as-CMS is a sweet spot: fast authoring, structured metadata, and simple publishing. Start with a minimal database, render blocks safely, and add caching once traffic grows.
If you want, you can extend this setup with:
- a “Featured” flag
- canonical URLs
- SEO fields (meta title/description)
- an RSS feed