Scheduled blog posts for Astro
Scheduled blog posts for Astro: wire Crontent webhooks into a server endpoint and content collections or a headless CMS.
How-to: Integrate Crontent with Astro. Official docs: Astro. Shared API contract: Publishing API and SDK.
Expose a server endpoint, verify the Crontent webhook, fetch the post, append a draft to an Astro content collection (or CMS), then rebuild or render on demand after review.
Field mapping
| Crontent field | Astro usage |
|---|---|
title | Collection title, page <h1> |
seo_title | Layout <title>, Open Graph (fallback: title) |
slug | slug in collection schema |
excerpt | description frontmatter |
body_mdx | MD body in src/content/posts/ |
body_html | Use if you render HTML with set:html |
tags | tags array in schema |
hero_image_url | image field, <Image> src |
canonical_url | <link rel="canonical"> in layout |
sources[] | Custom sources field or footer component |
Webhook endpoint
Enable SSR with an adapter (Vercel, Netlify) and add an API route.
// src/pages/api/crontent-webhook.ts
import type { APIRoute } from "astro";
import { Crontent } from "@crontent/sdk";
import { writeFile, mkdir } from "node:fs/promises";
import { join } from "node:path";
const crontent = new Crontent({ apiKey: import.meta.env.CRONTENT_API_KEY });
export const POST: APIRoute = async ({ request }) => {
const rawBody = await request.text();
const valid = await crontent.webhooks.verify({
rawBody,
signature: request.headers.get("x-crontent-signature"),
timestamp: request.headers.get("x-crontent-timestamp"),
secret: import.meta.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);
const dir = join(process.cwd(), "src/content/posts");
await mkdir(dir, { recursive: true });
const yaml = [
"---",
`title: ${JSON.stringify(post.title)}`,
`description: ${JSON.stringify(post.excerpt ?? "")}`,
`pubDate: ${JSON.stringify(post.ready_at ?? post.created_at)}`,
`draft: true`,
`tags: ${JSON.stringify(post.tags)}`,
`heroImage: ${JSON.stringify(post.hero_image_url ?? "")}`,
"---",
"",
post.body_mdx,
].join("\n");
await writeFile(join(dir, `${post.slug}.md`), yaml);
return new Response("ok");
};Define the collection in src/content/config.ts with a draft boolean. Filter drafts out of production listings until you flip the flag.
Create drafts in a CMS
For Sanity or similar, replace the filesystem write with an unpublished document create. Astro fetches at build time via the CMS SDK.
Backfill
Run a one-off script calling crontent.posts.list({ projectId, since, limit, cursor }) and write missing slugs. Astro rebuilds pick up new collection files automatically.
Environment variables
CRONTENT_API_KEY=...
CRONTENT_WEBHOOK_SECRET=...