> ## Documentation Index
> Fetch the complete documentation index at: https://docs.neuraldraft.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Astro

> Content collections backed by Neural Draft, with on-build fetching and webhook-driven rebuilds.

This snippet wires Neural Draft into Astro 5 via a content collection loader.
The result: type-safe blog content fetched at build time, plus a webhook
handler that triggers Vercel/Cloudflare/Netlify rebuilds when something
changes.

## Install

```bash theme={null}
npm install @neuraldraft/sdk
```

```bash filename=".env" theme={null}
NEURALDRAFT_API_KEY=ndsk_live_...
NEURALDRAFT_WEBHOOK_SECRET=whsec_...
```

## Shared client

```ts filename="src/lib/neural-draft.ts" theme={null}
import { NeuralDraftClient } from "@neuraldraft/sdk";
import { createHmac, timingSafeEqual } from "node:crypto";

const apiKey = import.meta.env.NEURALDRAFT_API_KEY ?? process.env.NEURALDRAFT_API_KEY;
if (!apiKey) throw new Error("Missing NEURALDRAFT_API_KEY");

export const nd = new NeuralDraftClient({ apiKey });

export function verifyNeuralDraftSignature(
  body: string,
  header: string,
  secret = import.meta.env.NEURALDRAFT_WEBHOOK_SECRET ?? "",
  toleranceSeconds = 300
): boolean {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=") as [string, string])
  );
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!Number.isFinite(t) || !v1 || !secret) return false;

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - t) > toleranceSeconds) return false;

  const signed = `${t}.${body}`;
  const expected = createHmac("sha256", secret).update(signed).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(v1, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

## A content collection loader

Astro's content layer treats Neural Draft like a CMS — pull posts at build time,
get type-safe content with Zod validation.

```ts filename="src/content.config.ts" theme={null}
import { defineCollection, z } from "astro:content";
import { nd } from "./lib/neural-draft";

const blog = defineCollection({
  loader: async () => {
    const all: any[] = [];
    let page = 1;
    while (true) {
      const res = await nd.blogPosts.list({
        status: "published",
        lang: "en",
        page_size: 100,
        page,
      });
      all.push(...res.data);
      if (res.data.length < 100) break;
      page++;
    }
    return all.map((p) => ({
      id: p.slug,
      slug: p.slug,
      title: p.title,
      excerpt: p.excerpt,
      content: p.content,
      featured_image: p.featured_image,
      published_at: p.published_at,
      category: p.category?.name ?? null,
      tags: (p.tags ?? []).map((t: any) => t.name),
    }));
  },
  schema: z.object({
    slug: z.string(),
    title: z.string(),
    excerpt: z.string().nullable(),
    content: z.string(),
    featured_image: z.string().url().nullable(),
    published_at: z.string().nullable(),
    category: z.string().nullable(),
    tags: z.array(z.string()),
  }),
});

export const collections = { blog };
```

## A blog index page

```astro filename="src/pages/blog/index.astro" theme={null}
---
import { getCollection } from "astro:content";

const posts = (await getCollection("blog")).sort((a, b) =>
  (b.data.published_at ?? "").localeCompare(a.data.published_at ?? "")
);
---

<main class="prose mx-auto py-12">
  <h1>Blog</h1>
  <ul>
    {posts.map((post) => (
      <li>
        <a href={`/blog/${post.data.slug}`}>
          <h2>{post.data.title}</h2>
          <p>{post.data.excerpt}</p>
        </a>
      </li>
    ))}
  </ul>
</main>
```

```astro filename="src/pages/blog/[slug].astro" theme={null}
---
import { getCollection } from "astro:content";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  return posts.map((post) => ({ params: { slug: post.data.slug }, props: { post } }));
}

const { post } = Astro.props;
---

<article class="prose mx-auto py-12">
  <h1>{post.data.title}</h1>
  {post.data.featured_image && (
    <img src={post.data.featured_image} alt={post.data.title} />
  )}
  <div set:html={post.data.content} />
</article>
```

## A webhook handler that triggers a rebuild

Astro itself doesn't trigger rebuilds, but every modern host (Vercel,
Netlify, Cloudflare) gives you a "deploy hook" URL. Forward Neural Draft
webhooks to that URL.

```ts filename="src/pages/api/neural-draft.ts" theme={null}
import type { APIRoute } from "astro";
import { verifyNeuralDraftSignature } from "../../lib/neural-draft";

const REBUILD_HOOK = process.env.DEPLOY_HOOK_URL!;

export const POST: APIRoute = async ({ request }) => {
  const body = await request.text();
  const sig = request.headers.get("x-neural-draft-signature") ?? "";
  if (!verifyNeuralDraftSignature(body, sig)) {
    return new Response("invalid signature", { status: 401 });
  }

  const event = JSON.parse(body) as { type: string };
  if (
    event.type === "blog_post.published" ||
    event.type === "blog_post.translated" ||
    event.type === "content.changed"
  ) {
    await fetch(REBUILD_HOOK, { method: "POST" });
  }

  return new Response(JSON.stringify({ received: true }), {
    headers: { "content-type": "application/json" },
  });
};
```

## Hand this to your AI

> *"Add a `/changelog` page that lists the last ten published blog posts
> tagged 'changelog'. Use the existing `nd` client from `src/lib/neural-draft.ts`
> and call `nd.blogPosts.list({ status: 'published', tag: 'changelog',
> page_size: 10, sort: '-published_at' })`. Render every text node with
> a `data-translate` key (`changelog.title`, `changelog.description`)
> so the customer can edit copy in the Neural Draft admin."*

With [`@neuraldraft/mcp`](/mcp/setup) installed, the AI will also call
`register_component` to make the new page editable.

## Notes

* This file uses Astro's `content.config.ts` — Astro 5 syntax. On 4.x, use the
  `defineCollection({ loader })` style from the experimental content layer.
* For multi-language sites, add `import.meta.env.PUBLIC_LANG` and clone the
  collection per locale.
* The deploy hook is per-platform: Vercel, Netlify and Cloudflare Pages all
  expose one in project settings. Free, no rate limit.
