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_KEYandCRONTENT_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/sdkand@tryghost/admin-api(optional)
Register the webhook:
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 field | Ghost field | Notes |
|---|---|---|
title | title | Post title and H1 |
seo_title | meta_title | Custom meta; fall back to title |
slug | slug | URL slug |
excerpt | custom_excerpt | Card and meta description |
body_mdx / body_html | html | Lexical/HTML body |
tags | tags | Array of { name } objects |
hero_image_url | feature_image | Feature image URL |
canonical_url | canonical_url | SEO canonical |
sources[] | codeinjection_foot or custom | Append source list in HTML |
ready_at | Internal tracking | Store 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.
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.
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.