CMS for Vue
ElmapiCMS is a self-hosted headless CMS with a REST API and official JavaScript SDK. This guide shows how to connect it to Vue apps: configure env vars, fetch in composables, build slug routes, and keep deployments fresh with webhooks.
A good fit for Vue projects
Vue apps benefit from a predictable API layer and composable data access. ElmapiCMS provides collection-based content APIs so Vue teams can ship editor-managed content without custom admin tooling.
It is especially useful when multiple frontend apps share one content backend, or when non-technical teams need to publish without Git workflows.
If you want platform-level comparisons first, read best headless CMS for Vue, then use this page for implementation patterns.
Install SDK and configure environment variables
Install the official JavaScript SDK:
npm install @elmapicms/js-sdkAdd credentials in your Vue environment file. For browser apps, treat API key scope carefully and prefer read-only tokens for public access.
VITE_ELMAPI_API_URL=https://your-cms.example.com/api
VITE_ELMAPI_PROJECT_ID=your-project-uuid
VITE_ELMAPI_API_KEY=your-tokenGet your Project ID and API token under Settings → API Access.
Create a shared CMS client
Centralize SDK setup so every composable and route uses the same config and avoids duplicated setup logic.
// src/lib/elmapi.ts
import { createClient } from "@elmapicms/js-sdk";
const url = import.meta.env.VITE_ELMAPI_API_URL;
const key = import.meta.env.VITE_ELMAPI_API_KEY;
const projectId = import.meta.env.VITE_ELMAPI_PROJECT_ID;
if (!url || !key || !projectId) {
throw new Error("Missing ElmapiCMS environment variables");
}
export const cms = createClient(url, key, projectId);The SDK client includes methods like getEntries, getEntry, getCollections, and getAssets. Full API details are documented in API introduction.
Fetch content in Vue composables and routes
Use composables to keep fetching logic reusable and consistent across pages.
List composable
Normalize SDK response shape once, then bind reactive state in components.
// src/composables/usePosts.ts
import { ref } from "vue";
import { cms } from "../lib/elmapi";
export function usePosts() {
const posts = ref([]);
const loading = ref(false);
const error = ref<string | null>(null);
async function load(page = 1) {
loading.value = true;
error.value = null;
try {
const result = await cms.getEntries("posts", {
state: "published",
sort: "created_at:desc",
paginate: 12,
page,
});
posts.value = Array.isArray(result) ? result : result.data;
} catch (e) {
error.value = e instanceof Error ? e.message : "Failed to load posts";
} finally {
loading.value = false;
}
}
return { posts, loading, error, load };
}Detail route by slug
Fetch by slug with a where clause and handle missing entries explicitly.
<!-- src/views/PostView.vue -->
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { cms } from "../lib/elmapi";
const route = useRoute();
const post = ref<any>(null);
const notFound = ref(false);
onMounted(async () => {
const result = await cms.getEntries("posts", {
where: { slug: route.params.slug },
limit: 1,
state: "published",
});
const rows = Array.isArray(result) ? result : result.data;
post.value = rows[0] || null;
notFound.value = !post.value;
});
</script>
<template>
<article v-if="post">
<h1>{{ post.fields.title }}</h1>
<div v-html="post.fields.body" />
</article>
<p v-else-if="notFound">Post not found.</p>
</template>Filtering, sorting, and pagination
Use where for filters, sort with field:asc/field:desc, and paginate + page for list views.
// Basic filtering
const featured = await cms.getEntries("posts", {
where: { featured: true },
sort: "created_at:desc",
state: "published",
});
// Operator filtering
const recent = await cms.getEntries("posts", {
where: { created_at: { gte: "2026-01-01" } },
sort: "created_at:desc",
paginate: 10,
page: 1,
});
// Fetch by UUID
const entry = await cms.getEntry("posts", "entry-uuid-here");For advanced operators and relation filters, see advanced filtering and filtering examples.
Webhooks and content refresh
For static Vue deployments, webhook-triggered rebuilds are the simplest reliable strategy. For SSR setups, fetch on request and add caching at the CDN/server layer.
// Example Node endpoint pseudo-code
import { createHmac, timingSafeEqual } from "node:crypto";
function isValidSignature(rawBody: string, signature: string, secret: string) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
return (
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}
// If valid, trigger your build hook for static deploys.Configure webhook events under Project Settings → Webhooks, and ensure queue workers are configured in production using the deployment webhooks guide.
Before you deploy
- API keys are scoped correctly and not over-privileged for browser usage.
- Public pages fetch
state: "published"content. - List code handles both paginated and non-paginated response shapes.
- Detail routes correctly handle missing slugs.
- Webhook signature validation and build-hook trigger flow are tested.