Automated blog posts for Contentful
Automated blog posts for Contentful: receive Crontent post.ready webhooks and create draft entries via the Contentful Management API.
How-to: Integrate Crontent with Contentful. Official docs: Contentful. Shared API contract: Publishing API and SDK.
Verify the Crontent webhook, fetch the full post, map dual titles and sources, then create a Contentful Entry via the Management API and leave it unpublished for editorial review.
What you need
- Crontent API key and webhook secret (
CRONTENT_API_KEY,CRONTENT_WEBHOOK_SECRET) - A Contentful space with a content type (e.g.
blogPost) matching your front-end fields - Content Management API token with create access
- A server-side webhook endpoint (Contentful requires HTTPS)
Register your endpoint in Crontent:
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 Contentful
| Crontent field | Contentful field | Notes |
|---|---|---|
title | title | Short text, display heading |
seo_title | seoTitle | Meta and og:title; fall back to title |
slug | slug | Unique per locale |
excerpt | excerpt | Short text or long text |
body_mdx / body_html | body | Rich text or long text field |
tags | tags | Short text list or taxonomy |
hero_image_url | heroImage | Asset link or external URL field |
canonical_url | canonicalUrl | Optional |
sources[] | sources | JSON object array |
ready_at | crontentReadyAt | Date field for sync tracking |
Crontent handles research and first drafts from your brief. You control angle and tone in the brief; entries stay drafts until a human publishes in Contentful.
Receive and verify webhooks
Headers: X-Crontent-Signature, X-Crontent-Timestamp, X-Crontent-Delivery. Retries: 1m, 5m, 30m, 2h, 12h. Use the delivery ID as an idempotency key.
import { Crontent } from "@crontent/sdk";
import contentfulManagement from "contentful-management";
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 seen(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 createContentfulDraft(post);
await markSeen(deliveryId);
return new Response("OK");
}Create a draft in Contentful
Use the Management API. Entries created without publishing remain drafts.
const client = contentfulManagement.createClient({
accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN!,
});
const space = await client.getSpace(process.env.CONTENTFUL_SPACE_ID!);
const env = await space.getEnvironment("master");
async function createContentfulDraft(post: Post) {
const entry = await env.createEntry("blogPost", {
fields: {
title: { "en-US": post.title },
seoTitle: { "en-US": post.seo_title ?? post.title },
slug: { "en-US": post.slug },
excerpt: { "en-US": post.excerpt },
body: { "en-US": post.body_html ?? post.body_mdx },
tags: { "en-US": post.tags },
crontentPostId: { "en-US": post.id },
},
});
// Do not call entry.publish(); leave as draft for review
return entry;
}Backfill and polling
Use crontent.posts.list({ projectId, since, cursor }) to catch missed deliveries. Query Contentful by crontentPostId before creating to prevent duplicates.