AI content for Next.js blogs
AI content for Next.js blogs: handle Crontent post.ready webhooks in an App Router route and store MDX or HTML drafts.
How-to: Integrate Crontent with Next.js. Official docs: Next.js. Shared API contract: Publishing API and SDK.
Add an App Router webhook route, verify Crontent's signature, fetch the post, and write an MDX or HTML draft into your content layer before anything goes live.
Field mapping
| Crontent field | Next.js usage |
|---|---|
title | Page H1, metadata.title fallback |
seo_title | metadata.title, openGraph.title (fallback: title) |
slug | Route segment or CMS slug |
excerpt | metadata.description, listing cards |
body_mdx | MDX page body (preferred if you use @next/mdx) |
body_html | HTML fallback or CMS rich text |
tags | Frontmatter tags or CMS taxonomy |
hero_image_url | openGraph.images, hero component |
canonical_url | metadata.alternatives.canonical |
sources[] | Footnotes, "Sources" section |
Webhook Route Handler
Use the App Router and read the raw body before JSON parsing. See Route Handlers.
// app/api/crontent/webhook/route.ts
import { Crontent } from "@crontent/sdk";
import { writeFile, mkdir } from "node:fs/promises";
import { join } from "node:path";
export const runtime = "nodejs";
const crontent = new Crontent({ apiKey: process.env.CRONTENT_API_KEY! });
const seen = new Set<string>(); // use Redis/DB in production
export async function POST(req: Request) {
const rawBody = await req.text();
const delivery = req.headers.get("x-crontent-delivery") ?? "";
if (seen.has(delivery)) return new Response("ok", { status: 200 });
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("invalid signature", { status: 401 });
const { post_id } = JSON.parse(rawBody) as { post_id: string };
const post = await crontent.posts.get(post_id);
const dir = join(process.cwd(), "content/posts");
await mkdir(dir, { recursive: true });
const frontmatter = [
"---",
`title: ${JSON.stringify(post.title ?? "Untitled")}`,
`description: ${JSON.stringify(post.excerpt ?? "")}`,
`slug: ${JSON.stringify(post.slug ?? post.id)}`,
`draft: true`,
`tags: ${JSON.stringify(post.tags)}`,
`heroImage: ${JSON.stringify(post.hero_image_url ?? "")}`,
"---",
"",
].join("\n");
await writeFile(join(dir, `${post.slug ?? post.id}.mdx`), frontmatter + post.body_mdx);
seen.add(delivery);
return new Response("ok", { status: 200 });
}Register the endpoint in the Crontent dashboard or via crontent.webhooks.register({ projectId, url: "https://your-site.com/api/crontent/webhook", events: ["post.ready"] }).
Store as draft
Keep draft: true in frontmatter until a human publishes. If you use a headless CMS (Sanity, Contentful), POST an unpublished document instead of writing files. Map seo_title to the CMS SEO field and body_mdx to your MDX or portable-text pipeline.
Backfill existing posts
On deploy, paginate crontent.posts.list({ projectId, since, limit: 50, cursor }) and import any post not already in your store. Skip posts whose slug already exists.
Environment variables
CRONTENT_API_KEY=...
CRONTENT_WEBHOOK_SECRET=...