Astro Integration Guide

Astro pulls content from UnfoldCMS at build time (SSG) or on demand (SSR). The integration is fetch() + your existing Astro patterns — no Astro-specific package required.

Setup

# .env
PUBLIC_CMS_URL=https://your-cms.com
UNFOLDCMS_WEBHOOK_SECRET=your-webhook-secret

Static Blog Index

---
// src/pages/blog/index.astro
const res = await fetch(`${import.meta.env.PUBLIC_CMS_URL}/api/v1/posts?per_page=50`);
const { data: posts } = await res.json();
---

<html>
  <body>
    <h1>Blog</h1>
    <ul>
      {posts.map((post: any) => (
        <li>
          <a href={`/blog/${post.slug}`}>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
            <time>{new Date(post.posted_at).toLocaleDateString()}</time>
          </a>
        </li>
      ))}
    </ul>
  </body>
</html>

Static Single Post (SSG)

---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
  const res = await fetch(`${import.meta.env.PUBLIC_CMS_URL}/api/v1/posts?per_page=100`);
  const { data: posts } = await res.json();
  return posts.map((post: any) => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;

// Fetch full body (list endpoint returns excerpt only)
const fullRes = await fetch(`${import.meta.env.PUBLIC_CMS_URL}/api/v1/posts/${post.slug}`);
const { data: fullPost } = await fullRes.json();
---

<html>
  <head>
    <title>{fullPost.seo_title || fullPost.title}</title>
    <meta name="description" content={fullPost.meta_desc || fullPost.excerpt} />
    {fullPost.featured_image && (
      <meta property="og:image" content={fullPost.featured_image.large} />
    )}
  </head>
  <body>
    <article>
      <h1>{fullPost.title}</h1>
      <p>by {fullPost.author.name} · {fullPost.reading_time} min read</p>
      <div set:html={fullPost.body} />
    </article>
  </body>
</html>

On-Demand Rendering (SSR Mode)

For content that should always be fresh (e.g. category pages):

---
// astro.config.mjs sets output: 'server' or 'hybrid'
// src/pages/category/[slug].astro
export const prerender = false; // override SSG for this page

const { slug } = Astro.params;
const res = await fetch(`${import.meta.env.PUBLIC_CMS_URL}/api/v1/categories/${slug}/posts`);
if (!res.ok) return Astro.redirect('/404');
const { data: posts, pagination } = await res.json();
---

<h1>Category: {slug}</h1>
<p>{pagination.total} posts</p>
{posts.map((p: any) => <a href={`/blog/${p.slug}`}>{p.title}</a>)}

Site Settings in the Layout

---
// src/layouts/Base.astro
const res = await fetch(`${import.meta.env.PUBLIC_CMS_URL}/api/v1/settings/public`);
const { data: settings } = await res.json();
---

<html>
  <head>
    <title>{settings['general.site_name']}</title>
    <link rel="icon" href={settings['general.favicon_url']} />
  </head>
  <body>
    <header>
      <img src={settings['general.logo_url']} alt={settings['general.site_name']} />
      <nav>
        <a href={settings['social.twitter']}>Twitter</a>
        <a href={settings['social.github']}>GitHub</a>
      </nav>
    </header>
    <slot />
  </body>
</html>

Webhook-Triggered Rebuilds

Astro static sites need to rebuild when content changes. Wire UnfoldCMS webhooks to your deploy provider's build hook:

Vercel

# Register a webhook pointing at your Vercel deploy hook
curl -X POST https://your-cms.com/api/v1/admin/webhooks \
  -H "Authorization: Bearer ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Vercel Astro Rebuild",
    "url": "https://api.vercel.com/v1/integrations/deploy/prj_xxx/yyy",
    "events": ["post.published", "post.updated", "post.deleted"]
  }'

Vercel deploy hooks don't validate signatures (they accept any POST), so the UnfoldCMS HMAC is harmless extra security. The hook URL itself is the secret.

Netlify

Same pattern — point the webhook at your Netlify build hook URL.

Self-hosted (verifying signatures)

If you control the build server, verify signatures before triggering:

// api/webhook.ts (Vercel Edge / Netlify Functions / etc.)
import crypto from 'node:crypto';

export async function POST(req: Request) {
  const raw = await req.text();
  const sig = req.headers.get('signature') || '';
  const expected = crypto
    .createHmac('sha256', process.env.UNFOLDCMS_WEBHOOK_SECRET!)
    .update(raw)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return new Response('Invalid signature', { status: 401 });
  }

  // Trigger your build pipeline
  await fetch(process.env.BUILD_HOOK_URL!, { method: 'POST' });

  return new Response('OK', { status: 200 });
}

Performance Notes

  • Astro fetches happen at build time for SSG pages. A site with 1,000 posts hits the API 1,001 times during build (once for the index, once per post). For sites that big, consider increasing the API rate limit or building with concurrency.
  • Use the per_page=100 max to minimize total request count.
  • Cache aggressively: Astro's Astro.glob and import.meta.glob are fine for build-time data, but for SSR routes, add a CDN layer (Vercel/Netlify edge cache) in front of the CMS API.

RSS Feed from the API

// src/pages/rss.xml.ts
import rss from '@astrojs/rss';

export async function GET() {
  const res = await fetch(`${import.meta.env.PUBLIC_CMS_URL}/api/v1/posts?per_page=50`);
  const { data: posts } = await res.json();

  return rss({
    title: 'My Blog',
    description: 'Powered by UnfoldCMS',
    site: 'https://your-site.com',
    items: posts.map((post: any) => ({
      title: post.title,
      pubDate: new Date(post.posted_at),
      description: post.excerpt,
      link: `/blog/${post.slug}`,
    })),
  });
}