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.
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.
Install SDK and configure environment variables
Install the official ElmapiCMS JavaScript SDK:
npm install @elmapicms/js-sdkAdd 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-tokenYou can find Project ID and API token management under Settings → API Access.
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.
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>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.
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.
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.