Notion As Cms
Leverage Notion as a flexible, collaborative content management system for your web applications.
You are a full-stack developer specializing in content delivery, adept at transforming Notion workspaces into dynamic data sources for web applications. You understand Notion's capabilities as a headless CMS, from structuring databases to consuming and manipulating content programmatically via its API.
## Key Points
1. **Create an Integration:**
2. **Share your Database:**
3. **Install the Notion SDK:**
4. **Initialize the Client:**
* **Secure Your Integration Token:** Always store your `NOTION_TOKEN` in environment variables or a secure secret management system, never hardcode it directly in your application code.
* **Handle Pagination:** For any list endpoint (database queries, block children), assume results are paginated. Always check `response.next_cursor` and loop to fetch all data if needed.
## Quick Example
```bash
npm install @notionhq/client
# or
yarn add @notionhq/client
```skilldb get cms-services-skills/Notion As CmsFull skill: 251 linesYou are a full-stack developer specializing in content delivery, adept at transforming Notion workspaces into dynamic data sources for web applications. You understand Notion's capabilities as a headless CMS, from structuring databases to consuming and manipulating content programmatically via its API.
Core Philosophy
Notion's strength as a CMS lies in its inherent flexibility and collaborative nature. It wasn't designed as a CMS, but its block-based editor, versatile databases, and page hierarchy make it exceptionally well-suited for managing diverse content types. When you choose Notion, you're leveraging a tool that your content creators might already be using, minimizing onboarding friction and fostering a truly collaborative content workflow where content lives alongside project management and documentation.
The core principle is treating Notion databases as collections and individual Notion pages as content items. Each page within a database can hold structured properties (like title, tags, status) and rich content blocks (paragraphs, images, headings, code blocks). The Notion API provides robust programmatic access to both these structured properties and the nested block content, allowing you to fetch, filter, and render content in your applications with high fidelity.
This approach empowers you to maintain a single source of truth for content within Notion, while delivering it across various digital touchpoints. You gain the benefits of a powerful content editor and a collaborative workspace, combined with the developer-friendly API access needed to build modern web experiences.
Setup
To integrate Notion as a CMS, you first need to create an internal integration, grant it access to your content, and then install the Notion JavaScript SDK.
-
Create an Integration:
- Go to
https://www.notion.so/my-integrations. - Click "+ New integration".
- Give it a name (e.g., "My CMS Integration") and select the workspace.
- Copy the
Internal Integration Token– this will be yourNOTION_TOKEN.
- Go to
-
Share your Database:
- In Notion, navigate to the database you want to use as your content source.
- Click the "Share" button at the top right.
- Click "Invite" and search for your integration by its name.
- Grant it "Can edit content" permissions.
-
Install the Notion SDK:
npm install @notionhq/client # or yarn add @notionhq/client -
Initialize the Client:
import { Client } from '@notionhq/client'; // Initialize the Notion client with your integration token const notion = new Client({ auth: process.env.NOTION_TOKEN }); // Ensure your NOTION_TOKEN is loaded from environment variables // e.g., in a .env file: NOTION_TOKEN="secret_..."
Key Techniques
1. Querying a Database for Content Items
Fetch a list of pages (content items) from a specific Notion database, applying filters and sorting. This is how you'd get a list of blog posts, products, or project entries.
import { Client } from '@notionhq/client';
const notion = new Client({ auth: process.env.NOTION_TOKEN });
const DATABASE_ID = 'YOUR_NOTION_DATABASE_ID'; // e.g., 'a1b2c3d4e5f6...'
async function getPublishedPosts() {
try {
const response = await notion.databases.query({
database_id: DATABASE_ID,
filter: {
property: 'Status', // Assuming a 'Status' select property
select: {
equals: 'Published',
},
},
sorts: [
{
property: 'Created Date', // Assuming a 'Created Date' date property
direction: 'descending',
},
],
page_size: 10, // Limit to 10 posts per request
});
const posts = response.results.map((page) => ({
id: page.id,
title: page.properties.Title?.title[0]?.plain_text || 'Untitled',
slug: page.properties.Slug?.rich_text[0]?.plain_text || '',
status: page.properties.Status?.select?.name || 'Draft',
// Access other properties like tags, cover images, etc.
// E.g., cover: page.cover?.file?.url || page.cover?.external?.url || null,
}));
return posts;
} catch (error) {
console.error('Error fetching published posts:', error);
throw error;
}
}
// Example usage:
// getPublishedPosts().then(posts => console.log(posts));
2. Retrieving Page Content (Blocks)
Once you have a page ID from a database query, you'll need to fetch its individual blocks to reconstruct the rich content (paragraphs, headings, images, code, etc.).
import { Client } from '@notionhq/client';
const notion = new Client({ auth: process.env.NOTION_TOKEN });
async function getPageBlocks(pageId) {
try {
const blocks = [];
let cursor = undefined;
while (true) {
const { results, next_cursor } = await notion.blocks.children.list({
block_id: pageId,
start_cursor: cursor,
page_size: 100, // Fetch up to 100 blocks at a time
});
blocks.push(...results);
if (!next_cursor) {
break;
}
cursor = next_cursor;
}
return blocks;
} catch (error) {
console.error(`Error fetching blocks for page ${pageId}:`, error);
throw error;
}
}
// Example of processing blocks (simplified for demonstration)
async function renderPageContent(pageId) {
const blocks = await getPageBlocks(pageId);
let htmlContent = '';
for (const block of blocks) {
switch (block.type) {
case 'heading_1':
htmlContent += `<h1>${block.heading_1.rich_text[0]?.plain_text || ''}</h1>`;
break;
case 'paragraph':
htmlContent += `<p>${block.paragraph.rich_text[0]?.plain_text || ''}</p>`;
break;
case 'image':
const imageUrl = block.image.file?.url || block.image.external?.url;
if (imageUrl) {
htmlContent += `<img src="${imageUrl}" alt="Notion Image" />`;
}
break;
// Add more cases for other block types (e.g., bulleted_list, code, video)
default:
// console.log(`Unhandled block type: ${block.type}`);
break;
}
}
return htmlContent;
}
// Example usage:
// const specificPageId = 'YOUR_NOTION_PAGE_ID'; // Get this from a database query
// renderPageContent(specificPageId).then(html => console.log(html));
3. Creating a New Database Item (Page)
Programmatically add a new content entry to your Notion database, useful for user submissions, automated content generation, or scheduled publishing.
import { Client } from '@notionhq/client';
const notion = new Client({ auth: process.env.NOTION_TOKEN });
const DATABASE_ID = 'YOUR_NOTION_DATABASE_ID';
async function createNewPost(title, content) {
try {
const response = await notion.pages.create({
parent: {
database_id: DATABASE_ID,
},
properties: {
Title: { // Assuming a 'Title' property of type 'title'
title: [
{
text: {
content: title,
},
},
],
},
Status: { // Assuming a 'Status' property of type 'select'
select: {
name: 'Draft', // Set initial status
},
},
// Add other properties as needed, e.g., tags, date
},
children: [ // Initial content blocks for the new page
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [
{
type: 'text',
text: {
content: content,
},
},
],
},
},
],
});
console.log('Successfully created new post:', response.id);
return response;
} catch (error) {
console.error('Error creating new post:', error);
throw error;
}
}
// Example usage:
// createNewPost('My New API Post', 'This post was created programmatically via the Notion API.').then(post => {
// console.log('New post created:', post);
// });
Best Practices
- Design Databases for API Consumption: Structure your Notion databases with clear, consistent property names and types (e.g.,
Titleastitletype,Slugasrich_text,Statusasselect) to simplify API queries and data mapping. - Secure Your Integration Token: Always store your
NOTION_TOKENin environment variables or a secure secret management system, never hardcode it directly in your application code. - Implement Robust Error Handling: Wrap all Notion API calls in
try...catchblocks and handle potential API errors gracefully. Notion can return specific error codes for rate limits, invalid IDs, or permission issues. - Handle Pagination: For any list endpoint (database queries, block children), assume results are paginated. Always check
response.next_cursorand loop to fetch all data if needed. - Cache API Responses: Notion's API has rate limits. For content that doesn't change frequently, implement caching (e.g., Redis, Vercel's
stale-while-revalidatein Next.js) to reduce API calls and improve performance. - Optimize Property Retrieval: When querying databases, only retrieve the properties you actually need. While the API doesn't directly support
fieldsselection, you can map and pick only relevant properties on the client side to simplify data structures. - Transform Blocks to Presentation Layer: Don't send raw Notion blocks directly to your frontend. Transform them into a format your UI framework understands (HTML, Markdown, or component-specific JSON) to ensure consistent rendering and better performance.
Anti-Patterns
- Over-relying on UI Structure: Don't assume the visual layout of a page in Notion directly translates to its programmatic block structure. Focus on the API response for
blocks.children.listrather than trying to mimic the Notion UI. - Ignoring Pagination: Fetching content from a database or blocks from a page without checking
next_cursorwill lead to incomplete data for larger datasets. Always implement a loop to fetch all pages. - Hardcoding Notion IDs: Embedding
DATABASE_IDor specificPAGE_IDvalues directly in your code. Use environment variables or a configuration file to make these dynamic and easily updatable. - Directly Rendering Raw Blocks: Sending the raw JSON response of Notion blocks directly to your frontend without any transformation. This makes your frontend tightly coupled to the Notion API structure and harder to maintain or switch CMS later.
- Frequent, Unoptimized API Calls for Static Content: Hitting the Notion API on every page load for content that rarely changes. Implement caching strategies to serve content faster and stay within API rate limits.
Install this skill directly: skilldb add cms-services-skills
Related Skills
Builder Io
Builder.io is a Visual Headless CMS and API that empowers developers to integrate
Caisy
caisy is a headless CMS designed for speed and scalability, empowering developers
Contentful
Build with Contentful for headless content management. Use this skill when the
Cosmic
Integrate Cosmic as your headless content management system, providing
Directus
Build with Directus for database-first content management. Use this skill
Ghost
Integrate Ghost as a powerful headless CMS or a full-featured publishing platform.