Integration·Hashnode·intermediate
Automated blog posts for Hashnode
Automated blog posts for Hashnode: verify Crontent webhooks and publish draft posts through the Hashnode GraphQL API.
How-to: Integrate Crontent with Hashnode. Official docs: Hashnode. Shared API contract: Publishing API and SDK.
Verify the webhook, fetch the Crontent post, map titles and Markdown body, and create a Hashnode draft through the GraphQL API for review.
Field mapping
| Crontent field | Hashnode field |
|---|---|
title | title |
seo_title | subtitle or custom SEO meta if supported |
slug | slug (Hashnode may override) |
excerpt | brief |
body_mdx | contentMarkdown |
body_html | Convert to Markdown or use HTML mode |
tags | tags (tag slugs on your publication) |
hero_image_url | coverImageURL |
canonical_url | originalArticleURL (canonical link) |
sources[] | Append as Markdown footnotes in body |
Webhook to Hashnode draft
ts
import { Crontent } from "@crontent/sdk";
const crontent = new Crontent({ apiKey: process.env.CRONTENT_API_KEY! });
async function createHashnodeDraft(post: Awaited<ReturnType<typeof crontent.posts.get>>) {
const sourcesBlock = post.sources.length
? "\n\n## Sources\n" + post.sources.map((s) => `- [${s.title ?? s.url}](${s.url})`).join("\n")
: "";
const res = await fetch("https://gql.hashnode.com", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: process.env.HASHNODE_PAT!,
},
body: JSON.stringify({
query: `mutation CreateDraft($input: CreateStoryInput!) {
createStory(input: $input) { post { id slug title } }
}`,
variables: {
input: {
publicationId: process.env.HASHNODE_PUBLICATION_ID,
title: post.title,
contentMarkdown: post.body_mdx + sourcesBlock,
brief: post.excerpt,
tags: post.tags.map((t) => ({ slug: t, name: t })),
coverImageURL: post.hero_image_url,
originalArticleURL: post.canonical_url,
publishStatus: "DRAFT",
},
},
}),
});
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 createHashnodeDraft(post);
return new Response("ok");
}Headless mode
Hashnode supports headless publishing on paid plans. Map Crontent posts to your own domain while using Hashnode as the CMS backend.
Backfill
Paginate posts.list and create drafts for posts not already in Hashnode. Match on slug or store Hashnode post IDs in your database.
Environment variables
text
CRONTENT_API_KEY=...
CRONTENT_WEBHOOK_SECRET=...
HASHNODE_PAT=...
HASHNODE_PUBLICATION_ID=...