crontent
Integration·Next.js·beginner

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 fieldNext.js usage
titlePage H1, metadata.title fallback
seo_titlemetadata.title, openGraph.title (fallback: title)
slugRoute segment or CMS slug
excerptmetadata.description, listing cards
body_mdxMDX page body (preferred if you use @next/mdx)
body_htmlHTML fallback or CMS rich text
tagsFrontmatter tags or CMS taxonomy
hero_image_urlopenGraph.images, hero component
canonical_urlmetadata.alternatives.canonical
sources[]Footnotes, "Sources" section

Webhook Route Handler

Use the App Router and read the raw body before JSON parsing. See Route Handlers.

ts
// 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: &quot;https://your-site.com/api/crontent/webhook&quot;, events: [&quot;post.ready&quot;] }).

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

text
CRONTENT_API_KEY=...
CRONTENT_WEBHOOK_SECRET=...

Further reading