How to Integrate a Headless CMS Into a Nuxt Application

Module names, config, data fetching, previews and route rules for wiring Storyblok, Sanity, Prismic, Strapi, Directus or Nuxt Studio into Nuxt.

Smiling team celebrating with a high five in a bright office
Photo: Pexels

The Short Version of a Nuxt CMS Integration

Most headless CMS vendors give Nuxt a module, and the integration follows the same path each time. You install the module, add credentials to nuxt.config.ts, fetch content with useAsyncData or the module’s own composable, render it with components, then set route rules so pages are cached. Preview for editors comes last, once the public pages work.

The details change by vendor, so this guide works through the real module names, config keys and composables for Storyblok, Sanity, Prismic, Strapi, Directus and Nuxt Content with Nuxt Studio. Code follows each vendor’s published Nuxt docs, trimmed to the lines that matter.

Smiling developer and colleague working at a laptop together
Photo: Pexels

Nuxt Modules and Packages for Six Content Sources

Content sourcePackageInstall commandFetch with
Storyblok@storyblok/nuxt 10.x (needs Nuxt 4)npm install @storyblok/nuxt@latestuseAsyncStoryblok
Sanity@nuxtjs/sanitynpx nuxi@latest module add sanityuseSanityQuery
Prismic@nuxtjs/prismic with @prismicio/client 7 and @prismicio/vue 6npx prismic initusePrismic inside useAsyncData
Strapi@nuxtjs/strapi (Strapi v5 by default, v4 and v3 supported)npm install @nuxtjs/strapiuseStrapi().find
Directus@directus/sdk in a Nuxt pluginnpm install @directus/sdk$directus.request inside useAsyncData
Nuxt Content and Nuxt Studionuxt-studio (MIT) on top of Nuxt Contentnpx nuxt module add nuxt-studioNuxt Content queries

Pick the row before writing any code, since it decides the rest of the work. Hosted platforms such as Storyblok, Sanity and Prismic bill by seats or plan, while Strapi and Directus can run on your own server. Nuxt Content keeps Markdown in your Git repository, and Nuxt Studio, now a free MIT-licensed module, gives non-developers a visual editor for those files.

Storyblok documentation page for the Nuxt SDK version 10
Screenshot: storyblok.com

Installing the Module and Adding Credentials

Storyblok’s module takes its options in the modules array, and the region is required unless your space was created in the EU, and it is the most common cause of empty responses on a first run.

// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    ['@storyblok/nuxt', {
      accessToken: process.env.STORYBLOK_TOKEN,
      apiOptions: { region: 'us' },
    }],
  ],
})

Sanity’s module reads a top-level sanity key, and the project ID and dataset come from your Sanity project settings:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/sanity'],
  sanity: {
    projectId: 'myProject',
    dataset: 'production',
    apiVersion: '2021-10-18',
  },
})

Prismic does most of the setup for you, since running npx prismic init inside a fresh Nuxt app creates the Prismic repository, installs the packages and writes the config. Pass --repo your-domain instead if the repository already exists in your Prismic account.

Directus has no Nuxt module, so a small plugin creates the client and shares it with the rest of the app:

// plugins/directus.js
import { createDirectus, rest, readItem, readItems } from '@directus/sdk';

const directus = createDirectus('https://directus.example.com').with(rest());

export default defineNuxtPlugin(() => {
  return {
    provide: { directus, readItem, readItems },
  };
});

Keep tokens out of the repository by putting them in .env and read them through runtimeConfig, so private keys stay on the server and only values under public reach the browser.

Fetching Content Inside Nuxt Pages

Each module wraps Nuxt’s data fetching so the request runs once on the server and the result travels to the browser with the page. Storyblok’s composable fetches a story and turns on the Visual Editor bridge in one call:

<script setup>
const { story } = await useAsyncStoryblok('home', {
  api: { version: 'draft' },
});
</script>

<template>
  <StoryblokComponent v-if="story" :blok="story.content" />
</template>

Sanity uses GROQ queries, and passing a ref or computed value as a parameter makes the query run again when that value changes, which is handy for filters and tabs:

<script setup>
const query = groq`*[_type == "article" && topic == $topic]`
const { data, refresh } = useSanityQuery(query, { topic: 'News' })
</script>

Prismic’s guide uses plain useAsyncData with the client from usePrismic(). Give every page a unique key, here the page UID, or Nuxt may serve one page’s data on another route:

<script setup lang="ts">
import { components } from '~/slices';

const prismic = usePrismic();
const route = useRoute();
const { data: page } = await useAsyncData(route.params.uid as string, () =>
  prismic.client.getByUID('page', route.params.uid as string),
);
</script>

<template>
  <SliceZone :slices="page?.data.slices ?? []" :components="components" />
</template>

Strapi’s module exposes typed helpers through useStrapi(), and you wrap the call in useAsyncData so it runs during server rendering:

<script setup lang="ts">
const { find } = useStrapi()
const { data: restaurants } = await useAsyncData('restaurants', () =>
  find<Restaurant>('restaurants')
)
</script>

With Directus, pull the helpers from useNuxtApp() and pass a request to $directus.request(). When you adapt Directus examples, check that any [0] index applies to the returned data and not to the request call itself.

<script setup>
const { $directus, $readItems } = useNuxtApp()
const { data: posts } = await useAsyncData('posts', () =>
  $directus.request($readItems('posts', { sort: ['-publish_date'] }))
)
</script>

Turning On Previews for Editors

Previews are where the modules save the most time, although each one handles drafts a little differently.

  • Storyblok: The bridge is on by default in useAsyncStoryblok. If you leave out the bridge option, it takes resolveRelations and resolveLinks from your API settings. Add your local or staging URL as a preview URL in the Storyblok space.
  • Sanity: Add a visualEditing block under sanity with a server-side read token and the studioUrl. The token stays on the server, and Presentation must be installed in Sanity Studio, @sanity/client must be installed, and the module’s minimal client must be turned off.
  • Prismic: The module registers a /preview route, which you then register with npx prismic preview add http://localhost:3000/preview --name Development, then add the production URL after you deploy. For live slice previews, point the Page Builder at pages/slice-simulator.vue with npx prismic preview set-simulator.
  • Nuxt Studio: Configure studio.repository in nuxt.config.ts with the provider (GitHub or GitLab), owner, repo and branch. You also need a sign-in provider for editors: GitHub OAuth, GitLab OAuth, Google OAuth or a custom flow.
  • Strapi and Directus: Build a preview page that requests draft items with a server-side token. Keep that page behind a secret query parameter or a sign-in screen.
Nuxt Content blog post announcing Nuxt Studio as a free open-source module
Screenshot: content.nuxt.com

Nuxt Studio deserves a note of its own, because it used to be a paid hosted platform at nuxt.studio. It has been rebuilt as a self-hosted module under the MIT license, existing subscriptions were cancelled, and content stays in your Git repository. For small teams on Nuxt Content, that removes the last cost from a Git-based setup.

Caching CMS Pages With Nuxt Route Rules

Without caching, every visit to a CMS page calls the CMS API. Nuxt’s routeRules let you pick a rendering mode per path:

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/blog': { isr: 3600 },
    '/blog/**': { isr: true },
    '/admin/**': { ssr: false },
  },
})

In this setup the home page is built once at deploy time. The blog index is generated on demand and refreshed in the background, with CDN caching for 3,600 seconds. Each post is generated once and cached until the next deployment, and the admin area renders only in the browser.

That isr: true rule has a consequence for editors, since a post fixed in the CMS stays stale on the live site until you deploy again. Connect the CMS publish webhook to your host’s deploy hook, or use a number such as isr: 600 for pages that editors update often.

For a fully static site, nuxt generate works like nuxt build with nitro.static set to true. Add crawlLinks: true under the prerender key so Nuxt follows links and builds every CMS page it can reach.

Problems That Show Up in the First Hour

SymptomLikely causeFix
Storyblok returns nothing or a 401Space is outside the EU and region is missingSet apiOptions.region in the module options
One page shows another page’s contentTwo useAsyncData calls share a keyUse a unique key per page, such as the slug or UID
Sanity visual editing overlays never appearminimal client still on, or Presentation not installed in StudioTurn off minimal, install @sanity/client and Presentation
Prismic preview link opens a 404Preview URL not registeredRun npx prismic preview add with your local and production URLs
Strapi calls hit the wrong API shapeModule set to the wrong Strapi versionSet the version option to match your server (v5 is the default)
Edits never reach a live postRoute uses isr: true with no redeployAdd a deploy hook or a numeric isr value

What Each Content Source Costs a Nuxt Team

Module support is free for every option on this page, so the bill comes from the CMS plan or your hosting. These are the entry prices from each vendor’s pricing page, and they show why a small team and a large team often land on different rows of the module table.

Content sourceFree optionFirst paid plan
StoryblokStarter: 1 seat (up to 2), 100,000 API requests a monthGrowth: $99 a month, or $90.75 billed yearly, 5 seats
SanityFree: up to 20 seats, 10k documents, 2 rolesGrowth: $15 per seat a month
PrismicFree: 1 user, 4 million API calls a monthStarter: $10 a month per repository billed yearly, 3 users
StrapiSelf-hosted under the MIT licenseStrapi Cloud Starter: $35 a month per project
DirectusSelf-hosted free under $5M revenue and 50 staffCloud Core: $89 a month billed yearly, 3 seats
Nuxt Content and Nuxt StudioFree and MIT-licensedNone

For a five-person content team, Prismic Small at $25 a month billed yearly costs a third of Sanity Growth at $75 a month (five seats at $15), while Storyblok Growth at $90.75 a month billed yearly includes five seats and the Visual Editor. A solo developer writing Markdown pays nothing beyond hosting with Nuxt Content.

A Pre-Launch Test Plan for the Integration

Run these checks on a staging deploy before the first editor logs in, since each one catches a problem that is far harder to fix once real content exists.

  1. Publish a test entry in the CMS and time how long it takes to appear on the live staging page, then confirm the delay matches your route rules.
  2. Open a draft through the preview link while signed out, and confirm the page refuses to show unpublished content.
  3. View the page source of a server-rendered page and search for your API tokens, which must never appear in the HTML or the Nuxt payload.
  4. Delete an entry in the CMS and check that its route returns a proper 404 page and never a blank one.
  5. Add a second locale if the site will have one, and confirm the module fetches the right language for each route.
  6. Turn off JavaScript in the browser and reload a content page, which should still show the full text if server rendering works.

Picking the Content Source Before You Start

Six questions decide which row of the first table fits your project.

  1. If editors need to click on the page to change it, Storyblok and Prismic offer that kind of visual editing inside Nuxt today.
  2. If everyone who edits content is comfortable with Git, Nuxt Content with Nuxt Studio costs nothing beyond hosting.
  3. If content must stay on your own servers, Strapi is MIT-licensed, and Directus is free to self-host for organisations under $5 million in revenue with fewer than 50 staff.
  4. If content will feed apps besides this site, Sanity’s structured documents and GROQ queries suit content that is reused across several channels.
  5. Count the editors who will log in, since Sanity charges $15 per seat a month on Growth while Storyblok’s Growth plan includes five seats for $99 a month.
  6. Estimate API traffic as well, because Prismic’s Free plan includes 4 million API calls a month and Storyblok’s Starter plan includes 100,000.

Our list of the best content management systems for Vue.js developers covers the same vendors for Vue apps without Nuxt, and our guide to SvelteKit CMS platforms shows how API call counts change with prerendering.

Agencies quoting this work can price the hours with our freelance rate calculator and send the bill with the free invoice generator. If editors will publish long posts, the word counter helps them keep to field limits set in the CMS.

Nuxt CMS Integration Questions

Do I Need a Module to Use a Headless CMS With Nuxt?

No, because any CMS with an HTTP API works through useFetch or useAsyncData, as the Directus plugin above shows. Modules mainly save time on previews, image helpers and ready-made components.

Does @storyblok/nuxt Work With Nuxt 3?

Version 10.x of @storyblok/nuxt requires Nuxt 4.0 or later, so Nuxt 3 projects need an earlier major version of the module.

Is Nuxt Studio Still Paid?

No. Nuxt Studio is now a free, MIT-licensed module that runs on your own infrastructure next to your Nuxt Content site. The old hosted platform was retired, and its paid subscriptions were cancelled when the module launched.

Should CMS Pages Use ISR or Prerendering in Nuxt?

Prerender pages that change only with deploys, such as the home page. Use ISR with a time value for pages editors update often, so changes appear without a rebuild.

Nuxt and CMS Documentation Behind These Steps

Joydeep Bhattacharya Avatar

Written by

Founder, Editor and Business Reporter, Bizolaunch

Keep Reading