Scheduled blog posts for Strapi
Scheduled blog posts for Strapi: deliver Crontent drafts into Strapi over signed webhooks and the REST API for editorial review.
How-to: Integrate Crontent with Strapi. Official docs: Strapi. Shared API contract: Publishing API and SDK.
Receive the signed Crontent webhook, fetch the post, map fields into a Strapi article via the REST API, and keep status unpublished until someone approves it.
What you need
CRONTENT_API_KEYandCRONTENT_WEBHOOK_SECRETfrom your Crontent project- Strapi v4+ with an
article(orpost) collection type - A Strapi API token with create permissions on that collection
- A webhook receiver with access to both APIs
Register the webhook:
await crontent.webhooks.register({
projectId: process.env.CRONTENT_PROJECT_ID!,
url: "https://your-app.com/api/crontent/webhook",
events: ["post.ready"],
});Map Crontent fields to Strapi
| Crontent field | Strapi field | Notes |
|---|---|---|
title | title | String, display H1 |
seo_title | seoTitle | Meta title; fall back to title |
slug | slug | UID or string field |
excerpt | description | Text or richtext |
body_mdx / body_html | content | Richtext or markdown field |
tags | tags | Relation or JSON |
hero_image_url | cover | Media or URL string |
canonical_url | canonicalUrl | Optional string |
sources[] | sources | JSON component or repeatable |
ready_at | crontentReadyAt | Datetime for sync |
Crontent researches and drafts from your brief; you set the angle. Keep publishedAt null so Strapi treats entries as drafts until review.
Receive and verify webhooks
Crontent retries failed deliveries at 1m, 5m, 30m, 2h, and 12h. Deduplicate using X-Crontent-Delivery.
import { Crontent } from "@crontent/sdk";
const crontent = new Crontent({ apiKey: process.env.CRONTENT_API_KEY! });
export async function POST(req: Request) {
const rawBody = await req.text();
const deliveryId = req.headers.get("x-crontent-delivery")!;
if (await isDuplicate(deliveryId)) return new Response("OK");
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 createStrapiDraft(post);
await recordDelivery(deliveryId);
return new Response("OK");
}Create a draft in Strapi
POST to the collection endpoint without setting publishedAt. In Strapi v4, omitting publish fields creates an unpublished entry.
async function createStrapiDraft(post: Post) {
const res = await fetch(`${process.env.STRAPI_URL}/api/articles`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
title: post.title,
seoTitle: post.seo_title ?? post.title,
slug: post.slug,
description: post.excerpt,
content: post.body_html ?? post.body_mdx,
tags: post.tags,
sources: post.sources,
crontentPostId: post.id,
// publishedAt intentionally omitted (draft)
},
}),
});
if (!res.ok) throw new Error(await res.text());
}Backfill and polling
Poll crontent.posts.list({ projectId, since, limit, cursor }) and skip posts whose crontentPostId already exists in Strapi. Useful after downtime or webhook misconfiguration.