crontent
Integration·Webflow·intermediate

Automated blog posts for Webflow

Automated blog posts for Webflow: create unpublished Webflow CMS items from Crontent post.ready webhooks via the CMS API.

How-to: Integrate Crontent with Webflow. Official docs: Webflow. Shared API contract: Publishing API and SDK.

Verify the Crontent webhook, fetch the post, and POST an unpublished Webflow CMS collection item so editors publish from the Designer.

Field mapping

Crontent fieldWebflow CMS field (example)
titlename
seo_titleseo-title custom field
slugslug
excerptpost-summary RichText or PlainText
body_mdxConvert to HTML for RichText post-body
body_htmlpost-body RichText
tagsMulti-reference or PlainText
hero_image_urlmain-image Image field (upload or URL)
canonical_urlcanonical-url PlainText
sources[]Append to body or separate RichText field

Webhook to Webflow draft

Create a CMS collection with fields matching the table above. Generate a site API token with CMS write scope.

ts
import { Crontent } from "@crontent/sdk";

const crontent = new Crontent({ apiKey: process.env.CRONTENT_API_KEY! });

async function createWebflowDraft(post: Awaited<ReturnType<typeof crontent.posts.get>>) {
  const sourcesHtml = post.sources.length
    ? `<h2>Sources</h2><ul>${post.sources.map((s) =>
        `<li><a href="${s.url}">${s.title ?? s.url}</a></li>`
      ).join("")}</ul>`
    : "";

  const res = await fetch(
    `https://api.webflow.com/v2/collections/${process.env.WEBFLOW_COLLECTION_ID}/items`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.WEBFLOW_API_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        isArchived: false,
        isDraft: true,
        fieldData: {
          name: post.title,
          slug: post.slug,
          "post-summary": post.excerpt,
          "post-body": (post.body_html ?? "") + sourcesHtml,
          "seo-title": post.seo_title ?? post.title,
          "canonical-url": post.canonical_url,
        },
      }),
    }
  );
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

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 createWebflowDraft(post);
  return new Response("ok");
}

Publish from Webflow after reviewing layout, images, and SEO settings.

Image handling

Webflow image fields often require an asset upload step. Upload hero_image_url via the Assets API before linking the field.

Backfill

Import via posts.list. Check existing slugs with a collection items list call to avoid duplicates.

Further reading