Nuxt · Vue · 2026

CMS for Nuxt

ElmapiCMS is a self-hosted headless CMS with a REST API and multi-project support. This guide shows how to connect it to Nuxt: set up runtime config, fetch content with useAsyncData, build slug routes, and keep pages fresh with webhooks.

Why

A good fit for Nuxt projects

Nuxt gives you flexible rendering modes and a strong data layer. ElmapiCMS adds an editor-friendly backend so content operations can move independently from frontend deploys.

This setup is especially useful for agency workflows and multi-site Nuxt stacks where you want one CMS installation to serve many projects.

If you want a broader market comparison first, read best CMS for Nuxt, then use this page for integration implementation.

Setup

Install SDK and configure environment variables

Install the official JavaScript SDK:

npm install @elmapicms/js-sdk

Add your Nuxt environment variables. Keep API keys private and server-side only.

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

Map values to Nuxt runtime config so server utilities can read credentials safely:

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    elmapiApiKey: process.env.ELMAPI_API_KEY,
    public: {
      elmapiApiUrl: process.env.ELMAPI_API_URL,
      elmapiProjectId: process.env.ELMAPI_PROJECT_ID,
    },
  },
});

Project ID and API tokens are available under Settings → API Access.

Client

Create a shared CMS client utility

Wrap SDK setup in one server utility to keep configuration and error handling in a single place.

// server/utils/elmapi.ts
import { createClient } from "@elmapicms/js-sdk";
 
export function cmsClient() {
  const config = useRuntimeConfig();
  const url = config.public.elmapiApiUrl;
  const key = config.elmapiApiKey;
  const projectId = config.public.elmapiProjectId;
 
  if (!url || !key || !projectId) {
    throw new Error("Missing ElmapiCMS runtime configuration");
  }
 
  return createClient(url, key, projectId);
}

The client exposes getEntries, getEntry, getCollections, and getAssets. Full endpoint behavior is documented in API introduction.

Pages

Fetch content with useAsyncData

Use Nuxt composables to centralize content fetching. This keeps rendering predictable across SSR and static output.

List page composable

Use pagination and normalize the SDK result shape in one place so components consume a stable structure.

// composables/usePosts.ts
export function usePosts(page = 1) {
  return useAsyncData(`posts-${page}`, async () => {
    const result = await cmsClient().getEntries("posts", {
      state: "published",
      sort: "created_at:desc",
      paginate: 12,
      page,
    });
 
    return {
      items: Array.isArray(result) ? result : result.data,
      raw: result,
    };
  });
}

Slug detail route

Query by slug with a where clause and throw a proper 404 when the entry does not exist.

// pages/posts/[slug].vue
<script setup lang="ts">
const route = useRoute();
const slug = route.params.slug as string;
 
const { data: post, error } = await useAsyncData(`post-${slug}`, async () => {
  const result = await cmsClient().getEntries("posts", {
    where: { slug },
    limit: 1,
    state: "published",
  });
 
  const rows = Array.isArray(result) ? result : result.data;
  return rows[0] || null;
});
 
if (error.value || !post.value) {
  throw createError({ statusCode: 404, statusMessage: "Post not found" });
}
</script>
 
<template>
  <article>
    <h1>{{ post.fields.title }}</h1>
    <div v-html="post.fields.body" />
  </article>
</template>
Queries

Filtering, sorting, and pagination

The SDK supports where filters, sort with field:asc/field:desc, and paginate + page. You can fetch one entry directly with getEntry.

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

For nested operators, OR groups, and relation filters, check advanced filtering and filtering examples.

Caching

Webhooks and refresh strategy

For static Nuxt sites, the reliable refresh path is webhook -> deploy hook -> rebuild. For SSR mode, fetch at request time and apply HTTP/Nitro cache strategy where appropriate.

// server/api/revalidate.post.ts
import { createHmac, timingSafeEqual } from "node:crypto";
 
export default defineEventHandler(async (event) => {
  const secret = process.env.ELMAPI_REVALIDATE_SECRET;
  const rawBody = await readRawBody(event, "utf8");
  const signature = getHeader(event, "x-webhook-signature");
 
  if (secret) {
    if (!signature || !rawBody) {
      throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
    }
 
    const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
    const valid =
      signature.length === expected.length &&
      timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
 
    if (!valid) {
      throw createError({ statusCode: 401, statusMessage: "Invalid signature" });
    }
  }
 
  // Trigger deploy hook for static Nuxt sites.
  if (process.env.DEPLOY_HOOK_URL) {
    await $fetch(process.env.DEPLOY_HOOK_URL, { method: "POST" });
  }
 
  return { revalidated: true };
});

Configure webhook events in Project Settings → Webhooks, and ensure queue workers are active in production per the deployment webhook guide.

Checklist

Before you deploy

  • Private API keys are not exposed to client bundles.
  • Public pages fetch state: "published" content only.
  • List and detail views handle paginated and non-paginated response shapes correctly.
  • 404 handling is implemented for missing slugs.
  • Webhook signature validation and deploy hook flow are tested.

Last updated: June 27, 2026