Nuxt layer
@avvyr/cms-nuxt is a Nuxt 4 layer. The site's published content lives in the Nitro server's memory, so rendering a page is a lookup, not an API call. It includes composables, a block renderer, rich text, responsive images, preview, live editing and a sitemap helper. It does not depend on the storefront packages, but works alongside @avvyr/storefront-nuxt.
Install
npm install @avvyr/cms-nuxt
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@avvyr/cms-nuxt'],
cms: {
siteId: '<site id>',
locales: ['sv', 'en'], // optional, default: every language on the site
defaultLocale: 'sv', // optional, default: the site's default
syncIntervalMs: 5000, // optional
editorOrigins: ['https://app.avvyr.com'], // optional, who may drive live editing
},
})
| Environment variable | |
|---|---|
NUXT_CMS_KEY | Public delivery key. Server only: every read goes through your server. |
NUXT_CMS_PREVIEW_KEY | Preview key, for drafts. Server only. |
NUXT_CMS_WEBHOOK_SECRET | Secret for POST /api/_cms/sync. The route is off until it is set. |
NUXT_CMS_BASE_URL | Default https://api.avvyr.com. |
NUXT_CMS_SITE_ID, NUXT_CMS_LOCALES, NUXT_CMS_SYNC_INTERVAL_MS | Override the cms config at runtime. |
NUXT_PUBLIC_CMS_EDITOR_ORIGINS | Overrides editorOrigins: a JSON array or a comma-separated list. |
A catch-all page
<!-- app/pages/[...slug].vue -->
<script setup lang="ts">
import type { CmsBlock } from '@avvyr/cms'
const route = useRoute()
const { data: page, error } = await useCmsPage<{ body?: CmsBlock[] }>(() => route.path)
if (error.value) throw createError({ statusCode: 404, fatal: true })
useCmsAlternates(page, { xDefault: 'sv' }) // hreflang links
useSeoMeta({ title: () => page.value?.title })
</script>
<template>
<CmsBlocks :blocks="page?.fields.body" />
</template>
Composables
| Composable | Returns |
|---|---|
useCmsPage(path, { locale, lazy, server, preview }) | The document, or null with error set on 404. Loads the draft while a preview token is in the URL. |
useCmsEntries(query) | { data, total }: manifest rows, or documents with include: 'documents' |
useCmsSingleton(type) | The singleton's document |
useCmsManifest() | Every published entry and folder |
useCmsAlternates(doc, { origin, xDefault, hreflang, head }) | hreflang links for the document's languages |
useCmsLocale() | The language used when none is passed: the @nuxtjs/i18n locale if it is a CMS language, otherwise defaultLocale |
useCmsPreview() | { path, locale, ... } from the preview URL, for a dedicated preview route |
All of them use useAsyncData with stable keys. Pass locale explicitly when your i18n codes differ from the CMS language codes.
Blocks
Register a component per block type, once, in app.vue or a layout:
// app/app.vue
import Hero from '~/components/blocks/Hero.vue'
import TextSection from '~/components/blocks/TextSection.vue'
provideCmsBlocks({ hero: Hero, textSection: TextSection })
A block component gets block, fields, index and first, whichever it declares as props:
<!-- app/components/blocks/Hero.vue -->
<script setup lang="ts">
import type { CmsMedia, CmsRichTextDoc } from '@avvyr/cms'
defineProps<{ fields: { title?: string, image?: CmsMedia | null, body?: CmsRichTextDoc | null }, first?: boolean }>()
</script>
<template>
<section>
<component :is="first ? 'h1' : 'h2'">{{ fields.title }}</component>
<CmsImage :media="fields.image" :width="1200" sizes="100vw" />
<CmsRichText :doc="fields.body" />
</section>
</template>
<CmsBlocks> can also take the map as a components prop, or read it from cms.blocks in app.config.ts. Unknown block types render nothing, with a warning in development.
Components
<CmsBlocks :blocks :components>renders blocks by type, each wrapped in adata-cms-blockelement for live editing (display: contentsby default).<CmsRichText :doc>renders rich text as VNodes, notv-html. Links to entries become<NuxtLink>, and unsafe hrefs are dropped.<CmsImage :media :width :height :sizes>renders a<picture>with AVIF and WebP sources and the image's ownwidthandheight, so nothing shifts. The focal point becomesobject-position.
How content stays fresh
- At startup the server loads each language's bundle into memory.
- Every
syncIntervalMs(and on requests, on serverless hosts) it checks/version. When the version moved, it fetches/changesand refetches only what changed. Large change sets or errors fall back to the full bundle. - Reads never wait for a sync. The previous version is served until the new one is swapped in whole.
- Each bundle is also written to Nitro storage at
cache:avvyr-cms:<siteId>:<locale>, so a cold instance starts from it. Mountcacheon persistent storage (for example Azure Blob) in production. POST /api/_cms/syncwithx-cms-webhook-secret: <NUXT_CMS_WEBHOOK_SECRET>syncs at once.
A publish therefore reaches the site within syncIntervalMs.
Preview and live editing
Both work without code. While ?token= is in the URL, pages load drafts and get noindex, and the live editing bridge starts when the editor frames the site. Your site must allow the app to frame it:
// nuxt.config.ts
routeRules: {
'/**': { headers: { 'Content-Security-Policy': "frame-ancestors 'self' https://app.avvyr.com" } },
},
See Preview and live editing for how it works.
Sitemap
// server/routes/sitemap-cms.json.get.ts
export default defineEventHandler(event => getCmsSitemapEntries(event, { locale: 'sv' }))
// → [{ path, lastmod, alternates: { en: '/about-us' } }]
Feed it to @nuxtjs/sitemap as a source, or build the XML yourself.