crontent
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 fieldHashnode field
titletitle
seo_titlesubtitle or custom SEO meta if supported
slugslug (Hashnode may override)
excerptbrief
body_mdxcontentMarkdown
body_htmlConvert to Markdown or use HTML mode
tagstags (tag slugs on your publication)
hero_image_urlcoverImageURL
canonical_urloriginalArticleURL (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=...

Further reading