Integration·Drupal·advanced
Scheduled blog posts for Drupal
Scheduled blog posts for Drupal: create unpublished nodes from Crontent webhooks via JSON:API for editorial review.
How-to: Integrate Crontent with Drupal. Official docs: Drupal. Shared API contract: Publishing API and SDK.
Verify the Crontent webhook, fetch the post, and create a Drupal node via JSON:API with status: false so it stays unpublished until review.
Field mapping
| Crontent field | Drupal field |
|---|---|
title | title |
seo_title | Metatag title or custom field |
slug | Path alias (set after save via Path module) |
excerpt | field_summary or body.summary |
body_html | body.value with body.format: full_html |
body_mdx | Convert to HTML before save |
tags | field_tags entity references |
hero_image_url | field_image (create media entity first) |
canonical_url | Metatag canonical or custom field |
sources[] | field_sources or append to body |
Webhook to unpublished node
Create a dedicated API user with permission to create unpublished content. Use JSON:API authentication (OAuth or basic with HTTPS).
ts
import { Crontent } from "@crontent/sdk";
const crontent = new Crontent({ apiKey: process.env.CRONTENT_API_KEY! });
async function createDrupalDraft(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(`${process.env.DRUPAL_BASE_URL}/jsonapi/node/article`, {
method: "POST",
headers: {
"Content-Type": "application/vnd.api+json",
Accept: "application/vnd.api+json",
Authorization: `Basic ${Buffer.from(`${process.env.DRUPAL_USER}:${process.env.DRUPAL_PASS}`).toString("base64")}`,
},
body: JSON.stringify({
data: {
type: "node--article",
attributes: {
title: post.title,
status: false,
body: {
value: (post.body_html ?? "") + sourcesHtml,
format: "full_html",
summary: post.excerpt ?? "",
},
},
},
}),
});
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 createDrupalDraft(post);
return new Response("ok");
}Set the path alias to post.slug in a follow-up PATCH to /jsonapi/path_alias/path_alias or via Pathauto after review.
Backfill
Import historical posts with posts.list. Query existing nodes by title or alias to skip duplicates.
Environment variables
text
CRONTENT_API_KEY=...
CRONTENT_WEBHOOK_SECRET=...
DRUPAL_BASE_URL=https://example.com
DRUPAL_USER=api-writer
DRUPAL_PASS=...