Astro · Static Sites · 2026

CMS for Astro

ElmapiCMS is a self-hosted headless CMS with a REST API and multi-project support. This guide shows how to connect it to Astro: install the SDK, fetch content in Astro pages, generate dynamic routes, and keep static builds fresh with webhooks.

Why

A good fit for Astro projects

Astro is excellent for content-heavy websites that prioritize performance and static output. ElmapiCMS adds an editor-friendly backend so content teams can publish without editing files in Git.

This combination works well for blogs, docs, marketing pages, and multi-site setups where one CMS instance powers multiple Astro frontends.

If you are comparing options first, read the best CMS for Astro comparison, then use this guide for implementation details.

Setup

Install SDK and configure environment variables

Install the official ElmapiCMS JavaScript SDK:

npm install @elmapicms/js-sdk

Add these values to your Astro environment file (for example .env or .env.production). Keep API keys server-side and never expose privileged tokens through client bundles.

ELMAPI_API_URL=https://your-cms.example.com/api
ELMAPI_PROJECT_ID=your-project-uuid
ELMAPI_API_KEY=your-token

You can find Project ID and API token management under Settings → API Access.

Client

Create a reusable Elmapi client

Centralize SDK setup in one utility file so every Astro page or server endpoint uses the same configuration.

// src/lib/elmapi.ts
import { createClient } from "@elmapicms/js-sdk";
 
const url = import.meta.env.ELMAPI_API_URL;
const key = import.meta.env.ELMAPI_API_KEY;
const projectId = import.meta.env.ELMAPI_PROJECT_ID;
 
if (!url || !key || !projectId) {
  throw new Error("Missing ElmapiCMS environment variables");
}
 
export const cms = createClient(url, key, projectId);

The client exposes methods like getEntries, getEntry, getCollections, and getAssets. Full API details are in the Content API docs.

Pages

Fetch content in Astro pages

In Astro, fetch CMS content in page frontmatter at build time for static routes, or on request in SSR mode.

List page

Query a collection and render links. The SDK may return either an array (non-paginated) or an object with data when pagination is used.

---
// src/pages/blog/index.astro
import Layout from "../../layouts/Layout.astro";
import { cms } from "../../lib/elmapi";
 
const result = await cms.getEntries("posts", {
  state: "published",
  sort: "created_at:desc",
  paginate: 12,
  page: 1,
});
 
const posts = Array.isArray(result) ? result : result.data;
---
 
<Layout title="Blog">
  <h1>Blog</h1>
  <ul>
    {posts.map((post) => (
      <li>
        <a href={`/blog/${post.fields.slug}/`}>{post.fields.title}</a>
      </li>
    ))}
  </ul>
</Layout>

Dynamic route page

Use getStaticPaths to generate slug routes from CMS content during the build.

---
// src/pages/blog/[slug].astro
import Layout from "../../layouts/Layout.astro";
import { cms } from "../../lib/elmapi";
 
export async function getStaticPaths() {
  const result = await cms.getEntries("posts", {
    state: "published",
    paginate: 100,
    page: 1,
  });
 
  const posts = Array.isArray(result) ? result : result.data;
 
  return posts
    .filter((p) => p.fields?.slug)
    .map((p) => ({
      params: { slug: p.fields.slug },
      props: { post: p },
    }));
}
 
const { post } = Astro.props;
---
 
<Layout title={post.fields.title}>
  <article>
    <h1>{post.fields.title}</h1>
    <Fragment set:html={post.fields.body} />
  </article>
</Layout>
Queries

Filtering, sorting, and pagination

Use where for filtering, sort with field:asc or field:desc, and paginate + page for pagination.

// Filter by field value
const featured = await cms.getEntries("posts", {
  where: { featured: true },
  sort: "created_at:desc",
  state: "published",
});
 
// Operator-based filtering
const recent = await cms.getEntries("posts", {
  where: { created_at: { gte: "2026-01-01" } },
  sort: "created_at:desc",
  paginate: 10,
  page: 1,
});
 
// Get a single entry by UUID
const entry = await cms.getEntry("posts", "entry-uuid-here");

For more advanced operators and compound filters, see advanced filtering and filtering examples.

Fresh Content

Webhooks and rebuild strategy

Astro static pages do not support Next.js-style route revalidation APIs. The standard approach is: ElmapiCMS webhook -> your deploy hook -> rebuild + redeploy.

  • Static mode: Trigger your platform build hook from an authenticated webhook endpoint.
  • SSR mode: Fetch on request and use HTTP caching strategy at CDN/edge level.
// Example payload handler for a platform function
// Validate webhook signature, then trigger your host deploy hook.
import { createHmac, timingSafeEqual } from "node:crypto";
 
export async function POST({ request }) {
  const rawBody = await request.text();
  const signature = request.headers.get("x-webhook-signature");
  const secret = process.env.ELMAPI_REVALIDATE_SECRET;
 
  if (secret) {
    if (!signature) return new Response("Missing signature", { status: 401 });
 
    const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
    const valid =
      signature.length === expected.length &&
      timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
 
    if (!valid) return new Response("Invalid signature", { status: 401 });
  }
 
  await fetch(process.env.DEPLOY_HOOK_URL, { method: "POST" });
  return new Response("ok");
}

Configure your webhook in Project Settings → Webhooks, and set queue workers in production as described in the webhook deployment docs.

Checklist

Before you deploy

  • API credentials stay server-side and are never shipped to the browser.
  • Public routes request state: "published" content only.
  • List rendering supports both paginated and non-paginated SDK responses.
  • Dynamic routes validate missing data and return proper 404 pages where needed.
  • Webhook-triggered rebuild flow is tested end-to-end in your host environment.

Last updated: June 27, 2026