crontent
Integration·Ghost·beginner

Scheduled blog posts for Ghost

Scheduled blog posts for Ghost: receive Crontent deliveries and create draft Ghost posts with the Admin API.

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

Verify Crontent's signature, fetch the post, and create a Ghost Admin API post with status: draft so editors publish when ready.

What you need

  • Crontent CRONTENT_API_KEY and CRONTENT_WEBHOOK_SECRET
  • Ghost site (Ghost(Pro) or self-hosted) with Admin API credentials
  • Admin API key (Integration type) from Ghost settings
  • Webhook receiver with @crontent/sdk and @tryghost/admin-api (optional)

Register the webhook:

ts
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 Ghost

Crontent fieldGhost fieldNotes
titletitlePost title and H1
seo_titlemeta_titleCustom meta; fall back to title
slugslugURL slug
excerptcustom_excerptCard and meta description
body_mdx / body_htmlhtmlLexical/HTML body
tagstagsArray of { name } objects
hero_image_urlfeature_imageFeature image URL
canonical_urlcanonical_urlSEO canonical
sources[]codeinjection_foot or customAppend source list in HTML
ready_atInternal trackingStore in a note or external DB

Crontent handles research and first drafts; you steer angle in the brief. Ghost drafts stay out of the public site until an editor clicks Publish.

Receive and verify webhooks

Headers: X-Crontent-Signature, X-Crontent-Timestamp, X-Crontent-Delivery. Retries at 1m, 5m, 30m, 2h, 12h. Deduplicate on delivery ID.

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

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 createGhostDraft(post);
  await markSeen(deliveryId);
  return new Response("OK");
}

Create a draft in Ghost

Use the Admin API. Set status: "draft" explicitly.

ts
import GhostAdminAPI from "@tryghost/admin-api";

const api = new GhostAdminAPI({
  url: process.env.GHOST_URL!,
  key: process.env.GHOST_ADMIN_API_KEY!,
  version: "v5.0",
});

async function createGhostDraft(post: Post) {
  const sourcesHtml = post.sources
    .map((s) => `<li><a href="${s.url}">${s.title ?? s.url}</a></li>`)
    .join("");

  await api.posts.add({
    title: post.title,
    slug: post.slug ?? undefined,
    html: `${post.body_html ?? post.body_mdx}<h3>Sources</h3><ul>${sourcesHtml}</ul>`,
    custom_excerpt: post.excerpt ?? undefined,
    meta_title: post.seo_title ?? post.title ?? undefined,
    feature_image: post.hero_image_url ?? undefined,
    canonical_url: post.canonical_url ?? undefined,
    tags: post.tags.map((name) => ({ name })),
    status: "draft",
  });
}

Backfill and polling

Use crontent.posts.list({ projectId, since, cursor }) and compare slugs or stored post IDs against Ghost. Run as a scheduled job if webhooks were missed.

Official documentation