Scheduled blog posts for Notion
Scheduled blog posts for Notion: create draft Notion database pages from Crontent post.ready webhooks using the Notion API.
How-to: Integrate Crontent with Notion. Official docs: Notion. Shared API contract: Publishing API and SDK.
Verify the Crontent webhook, fetch the post, and create a Notion database page with mapped properties before you mark it ready to share.
Field mapping
| Crontent field | Notion property |
|---|---|
title | Name (title) |
seo_title | SEO Title (rich_text) |
slug | Slug (rich_text or formula) |
excerpt | Excerpt (rich_text) |
body_mdx | Convert to Notion blocks |
body_html | Parse to blocks or store in page body |
tags | Tags (multi_select) |
hero_image_url | Cover external URL |
canonical_url | Canonical URL (url) |
sources[] | Bulleted list blocks at page bottom |
Webhook to Notion draft
Create an integration at Notion integrations and share your database with it.
import { Crontent } from "@crontent/sdk";
import { Client } from "@notionhq/client";
const crontent = new Crontent({ apiKey: process.env.CRONTENT_API_KEY! });
const notion = new Client({ auth: process.env.NOTION_TOKEN });
function paragraph(text: string) {
return { object: "block", type: "paragraph", paragraph: { rich_text: [{ type: "text", text: { content: text.slice(0, 2000) } }] } };
}
async function createNotionDraft(post: Awaited<ReturnType<typeof crontent.posts.get>>) {
const blocks = (post.body_mdx ?? "").split("\n\n").slice(0, 100).map(paragraph);
if (post.sources.length) {
blocks.push(paragraph("Sources"));
for (const s of post.sources) {
blocks.push(paragraph(`${s.title ?? s.url}: ${s.url}`));
}
}
return notion.pages.create({
parent: { database_id: process.env.NOTION_DATABASE_ID! },
cover: post.hero_image_url ? { type: "external", external: { url: post.hero_image_url } } : undefined,
properties: {
Name: { title: [{ text: { content: post.title ?? "Untitled" } }] },
Status: { select: { name: "Draft" } },
Slug: { rich_text: [{ text: { content: post.slug ?? "" } }] },
Excerpt: { rich_text: [{ text: { content: post.excerpt ?? "" } }] },
"SEO Title": { rich_text: [{ text: { content: post.seo_title ?? post.title ?? "" } }] },
"Canonical URL": post.canonical_url ? { url: post.canonical_url } : undefined,
Tags: { multi_select: post.tags.map((name) => ({ name })) },
},
children: blocks,
});
}
export async function handleWebhook(req: Request) {
const rawBody = await req.text();
const valid = await crontent.webhooks.verify({
rawBody,
signature: req.headers.get("x-crontent-signature"),
timestamp: req.headers.get("x-crontent-timestamp"),
secret: process.env.CRONTENT_WEBHOOK_SECRET!,
});
if (!valid) return new Response("unauthorized", { status: 401 });
const { post_id } = JSON.parse(rawBody);
const post = await crontent.posts.get(post_id);
await createNotionDraft(post);
return new Response("ok");
}For production MDX, use a Markdown-to-Notion converter library. The example splits paragraphs for brevity.
Publishing workflow
Editors review in Notion, fix formatting, set Status to Published. Notion Sites or Super/Potion frontends pick up published rows.
Backfill
Paginate posts.list and create draft pages for missing slugs. Notion API has rate limits (roughly 3 requests per second).
Environment variables
CRONTENT_API_KEY=...
CRONTENT_WEBHOOK_SECRET=...
NOTION_TOKEN=...
NOTION_DATABASE_ID=...