• Features
  • Pricing
Get Started

Next.jsAstroReact RouterNuxtSvelteKitManual

Nuxt

Server-side rendering with Nuxt for a simple Paragraph CMS blog.

This quickstart uses Nuxt's file-based routing and Nitro server routes and walks through the core setup for fetching and rendering Paragraph CMS content with server-side rendering (SSR). SSR is the recommended delivery model for Paragraph CMS because the API key and content requests stay on the server.

Want a ready-made project instead? Use the paragraphcms/nuxt-starter, which ships with the same integration pattern already wired into a blog.

The snippets below use the Nuxt 4 layout, so route files live in app/pages. If your app uses the classic structure instead, place the same route files in pages/ and keep the Nitro handlers in server/api and server/utils.

Install the Package

Add the Paragraph CMS client and Vue renderer to your Nuxt app:

npm install @paragraphcms/client @paragraphcms/parser-vue
pnpm add @paragraphcms/client @paragraphcms/parser-vue
yarn add @paragraphcms/client @paragraphcms/parser-vue
bun add @paragraphcms/client @paragraphcms/parser-vue

Set Environment Variables

Create a .env file in the project root:

.env
NUXT_PARAGRAPH_API_KEY=your_api_key

Create a Paragraph CMS Client

Create server/utils/paragraph.ts and initialize a shared client. In Nuxt, private runtime variables are read through useRuntimeConfig():

server/utils/paragraph.ts
import { Client } from "@paragraphcms/client";

let paragraphClient: Client | undefined;
let paragraphClientApiKey: string | undefined;

export function getParagraphClient() {
  const { paragraphApiKey } = useRuntimeConfig();
  const apiKey = paragraphApiKey?.trim();

  if (!apiKey) {
    throw new Error("NUXT_PARAGRAPH_API_KEY environment variable is not set");
  }

  if (!paragraphClient || paragraphClientApiKey !== apiKey) {
    paragraphClient = new Client({ apiKey });
    paragraphClientApiKey = apiKey;
  }

  return paragraphClient;
}

Configure SSR

Update nuxt.config.ts so Nuxt reads the API key on the server and keeps SSR enabled:

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    paragraphApiKey: process.env.NUXT_PARAGRAPH_API_KEY,
  },
  ssr: true,
});

Configure cache settings for the platform where the application runs.

Create Example Components

Create two very simple components that we will render in the next steps:

app/components/blog/Blog.vue
<script setup lang="ts">
import type { PageSummaryWithSlug } from "@paragraphcms/client";

defineProps<{
  posts: PageSummaryWithSlug[];
}>();
</script>

<template>
  <main>
    <h1>Blog</h1>

    <ul>
      <li v-for="post in posts" :key="post.id">
        <NuxtLink :to="`/blog/${post.slug}`">{{ post.title }}</NuxtLink>
      </li>
    </ul>
  </main>
</template>
app/components/blog/Post.vue
<script setup lang="ts">
import type { PageWithSlug } from "@paragraphcms/client";
import { ParagraphContent } from "@paragraphcms/parser-vue";

defineProps<{
  page: PageWithSlug;
}>();
</script>

<template>
  <main>
    <h1>{{ page.title }}</h1>
    <ParagraphContent :content="page.content" />
  </main>
</template>

Build /blog

Keep the Paragraph API key on the server by creating a Nitro route that loads posts, then fetch it from the page with useAsyncData():

server/api/blog.get.ts
import { defineEventHandler } from "h3";
import { getParagraphClient } from "../utils/paragraph";

export default defineEventHandler(async () => {
  const client = getParagraphClient();
  const { data: posts, error } = await client.pages.list({
    requiredSlug: true,
  });

  if (error) {
    throw error;
  }

  return posts;
});
app/pages/blog/index.vue
<script setup lang="ts">
import type { PageSummaryWithSlug } from "@paragraphcms/client";
import Blog from "../../components/blog/Blog.vue";

const { data: posts, error } = await useAsyncData<PageSummaryWithSlug[]>(
  "blog-posts",
  () => $fetch("/api/blog"),
);

if (error.value) {
  throw error.value;
}
</script>

<template>
  <Blog :posts="posts ?? []" />
</template>

Because requiredSlug: true is set, the /api/blog response is typed as PageSummaryWithSlug[], so post.slug is safe to use directly.

By default, client.pages.list() returns published pages only. If you want to include unpublished pages too or limit the query to a single collection, pass additional parameters under the same call inside server/api/blog.get.ts:

const client = getParagraphClient();

client.pages.list({
  requiredSlug: true,
  published: false,
  collection: "blog",
});

Build /blog/[slug]

Create a second Nitro route that resolves the requested page by slug, then fetch it from the dynamic page component:

server/api/blog/[slug].get.ts
import {
  createError,
  defineEventHandler,
  getRouterParam,
} from "h3";
import { getParagraphClient } from "../../utils/paragraph";

export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, "slug")?.trim();

  if (!slug) {
    throw createError({
      statusCode: 404,
      statusMessage: "Post not found",
    });
  }

  const client = getParagraphClient();
  const { data: page, error } = await client.page.getBySlug(slug);

  if (error) {
    throw error;
  }

  return page;
});
app/pages/blog/[slug].vue
<script setup lang="ts">
import type { PageWithSlug } from "@paragraphcms/client";
import Post from "../../components/blog/Post.vue";

const route = useRoute();
const slug = String(route.params.slug ?? "");

const { data: page, error } = await useAsyncData<PageWithSlug>(
  `blog-post:${slug}`,
  () => $fetch(`/api/blog/${encodeURIComponent(slug)}`),
);

if (error.value) {
  throw error.value;
}
</script>

<template>
  <Post v-if="page" :page="page" />
</template>

This resolves the requested slug on the server and keeps the API key out of the browser.

Once this is wired, the rest of the page can be standard Nuxt UI around the rendered Paragraph content.

React Router

SSR with React Router framework-mode loaders for a simple Paragraph CMS blog.

SvelteKit

SSR with SvelteKit server loads for a simple Paragraph CMS blog.

On this page

Install the PackageSet Environment VariablesCreate a Paragraph CMS ClientConfigure SSRCreate Example ComponentsBuild /blogBuild /blog/[slug]