
Nuxt Contentstack
Contentstack integration for Nuxt 4 (Nuxt 3.20.1+ also supported).
Notice: This is an OSS project by @timbenniks and not an officially maintained package by the Contentstack team. Support requests can come through Github issues and via direct channels to @timbenniks.
Requirements
- Nuxt 4 (recommended) or Nuxt 3
>=3.20.1 - @nuxt/image
^2.0.0(optional, for image provider functionality)
Features
- Complete set of Vue composables (entries, assets, by URL)
- Typed
useContentstack()helper for accessing SDKs and config - Advanced filtering, pagination, and sorting
- Live Preview & Visual Builder support
- Personalization support with server-side middleware
- Image transformations with
useImageTransformcomposable - @nuxt/image integration with automatic optimization
- TypeScript support with full type safety
- Exposed SDKs: Delivery SDK, Live Preview Utils SDK, Personalize SDK
Quick Setup
npx nuxi module add nuxt-contentstack
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
"nuxt-contentstack",
"@nuxt/image", // optional, for image provider support
],
"nuxt-contentstack": {
// Required
apiKey: "your_contentstack_api_key",
deliveryToken: "your_delivery_token",
environment: "your_environment",
// Optional
region: "eu", // 'us' | 'eu' | 'au' | 'azure-na' | 'azure-eu' | 'gcp-na' | 'gcp-eu'
branch: "main",
locale: "en-us",
// Live Preview
livePreview: {
enable: true,
previewToken: "your_preview_token",
editableTags: true,
editButton: true, // or object with enable, position, exclude, includeByQueryParameter
mode: "builder", // 'builder' | 'preview'
ssr: false,
},
// Personalization
personalization: {
enable: true,
projectUid: "your_project_uid",
},
debug: true,
},
});
Configuration
Core Settings
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
apiKey | string | Yes | - | Contentstack stack API key (starts with "blt") |
deliveryToken | string | Yes | - | Delivery token (starts with "cs") |
environment | string | Yes | - | Target environment ('preview' | 'production') |
region | string | No | 'us' | Contentstack region |
branch | string | No | 'main' | Content branch |
locale | string | No | 'en-us' | Default locale |
host | string | No | - | Override the Delivery SDK host URL |
extraTranspile | string[] | No | [] | Extra packages to transpile for CJS/ESM compat |
debug | boolean | No | false | Enable debug logging |
Live Preview
| Option | Type | Default | Description |
|---|---|---|---|
enable | boolean | false | Enable live preview |
previewToken | string | - | Preview token (required if enabled) |
editableTags | boolean | false | Add editable tags for visual building |
editButton | boolean | object | false | Enable/edit button config |
mode | 'builder' | 'preview' | 'builder' | Live preview mode |
ssr | boolean | false | Enable SSR mode (experimental) |
host | string | - | Override the Live Preview host URL |
Edit button object:
editButton: {
enable: boolean
position?: 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'top-center' | 'bottom-left' | 'bottom-right' | 'bottom-center'
exclude?: ('insideLivePreviewPortal' | 'outsideLivePreviewPortal')[]
includeByQueryParameter?: boolean
}
Personalization
| Option | Type | Required | Description |
|---|---|---|---|
enable | boolean | Yes | Enable personalization |
projectUid | string | Yes | Personalization project UID |
Provides
useContentstack()
Typed composable for accessing Contentstack SDKs and configuration. This is the recommended way to access the plugin provides.
const {
stack, // Delivery SDK Stack instance
ContentstackLivePreview, // Live Preview Utils SDK
personalizeSdk, // Personalize SDK instance (client-side only)
livePreviewEnabled, // boolean
editableTags, // boolean
variantAlias, // Variant aliases for personalization
VB_EmptyBlockParentClass, // Visual Builder empty block class
} = useContentstack();
You can also access the same values via useNuxtApp().$contentstack, but useContentstack() provides full TypeScript types out of the box.
Personalization SDK Usage
The module uses the instance-based Personalize SDK (v1.0.9+). Server-side personalization (variant resolution, cookie management) is handled automatically by the built-in server middleware. The client-side SDK instance is available for user interactions:
const { personalizeSdk } = useContentstack();
// Set user attributes
await personalizeSdk?.set({ age: 20 });
// Trigger impression for an experience
await personalizeSdk?.triggerImpression(experienceShortUid);
// Batch impressions
await personalizeSdk?.triggerImpressions({
experienceShortUids: ["exp1", "exp2"],
});
// Trigger conversion event
await personalizeSdk?.triggerEvent("eventKey");
// Set explicit user ID
await personalizeSdk?.setUserId("user-123");
Note:
personalizeSdkisnullon the server. Use optional chaining (?.) or guard withimport.meta.clientwhen calling SDK methods.
Composables
All composables support live preview updates, personalization variants, and Nuxt's caching system.
Note: All data-fetching composables are async and must be
awaited. This means they need to be called at the top level of<script setup>(which supports top-level await via<Suspense>automatically). They cannot be called conditionally insidesetup().
useGetEntryByUrl
Fetch entry by URL field.
const { data, status, refresh } = await useGetEntryByUrl<Page>({
contentTypeUid: "page",
url: "/about",
referenceFieldPath: ["author", "category"],
jsonRtePath: ["rich_text_field"],
locale: "en-us",
replaceHtmlCslp: true,
});
useGetEntry
Fetch single entry by UID.
const { data } = await useGetEntry<Article>({
contentTypeUid: "article",
entryUid: "your_entry_uid",
referenceFieldPath: ["author"],
jsonRtePath: ["content"],
locale: "en-us",
});
useGetEntries
Fetch multiple entries with filtering, pagination, and sorting.
const { data } = await useGetEntries<Article>({
contentTypeUid: "article",
referenceFieldPath: ["author"],
locale: "en-us",
limit: 10,
skip: 0,
orderBy: "created_at",
includeCount: true,
where: {
status: "published",
view_count: { $gt: 1000 },
created_at: { $gte: "2024-01-01", $lt: "2024-12-31" },
featured_image: { $exists: true },
title: { $regex: "nuxt.*contentstack" },
tags: ["tech", "news"],
author: { $ne: "guest" },
},
});
// Access results
console.log(data.value?.entries); // Article[]
console.log(data.value?.count); // number (if includeCount: true)
useGetAsset
Fetch single asset by UID.
const { data } = await useGetAsset<Asset>({
assetUid: "your_asset_uid",
locale: "en-us",
});
useGetAssets
Fetch multiple assets with filtering.
const { data } = await useGetAssets<Asset>({
locale: "en-us",
limit: 20,
orderBy: "created_at",
includeCount: true,
where: {
content_type: "image/jpeg",
// Note: Most asset filters are applied client-side
},
});
Query Operators
Supported in where clause for entry composables:
- Exact match:
field: "value" - Array contains:
tags: ["tech", "news"] - Comparison:
field: { $gt: 100, $gte: 50, $lt: 200, $lte: 150, $ne: "value" } - Existence:
field: { $exists: true } - Regex:
field: { $regex: "pattern" }
useImageTransform
Transform Contentstack images programmatically.
const { transformedUrl, updateTransform, resetTransform } = useImageTransform(
imageUrl,
{
width: 800,
height: 600,
quality: 80,
format: "webp",
fit: "crop",
blur: 5,
saturation: 10,
brightness: 5,
overlay: {
relativeURL: "/watermark.png",
align: "bottom-right",
width: "20p",
},
sharpen: {
amount: 5,
radius: 2,
threshold: 0,
},
}
);
// Update reactively
updateTransform({ width: 1200, quality: 90 });
Components
ContentstackModularBlocks
Renders Contentstack modular blocks as Vue components with optional auto-fetch capability. Auto-fetch is disabled by default. To enable auto-fetch, provide both contentTypeUid and url props. When using pre-fetched blocks, pass blocks via the blocks prop without providing contentTypeUid or url.
Pattern 1: Auto-fetch Entry
<script setup>
const componentMapping = {
hero: Hero,
grid: Grid,
text_block: TextBlock,
};
</script>
<template>
<ContentstackModularBlocks
content-type-uid="page"
:url="$route.path"
blocks-field-path="components"
:reference-field-path="['author']"
:json-rte-path="['rich_text']"
:auto-seo-meta="true"
:component-map="componentMapping"
>
<template #loading>Loading...</template>
<template #error>Failed to load</template>
<template #empty>No content</template>
</ContentstackModularBlocks>
</template>
Pattern 2: Pre-fetched Blocks
<script setup>
const { data: page } = await useGetEntryByUrl({
contentTypeUid: "page",
url: useRoute().path,
});
const componentMapping = {
hero: Hero,
grid: Grid,
};
</script>
<template>
<ContentstackModularBlocks
:blocks="page.components"
:component-map="componentMapping"
/>
</template>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
blocks | ContentstackBlock[] | [] | Array of modular blocks |
componentMap | ComponentMapping | {} | Block type → Vue component mapping |
fallbackComponent | Component | ContentstackFallbackBlock | Component for unmapped blocks |
contentTypeUid | string | undefined | Content type for auto-fetch (required if using auto-fetch) |
url | string | undefined | URL for auto-fetch (required if using auto-fetch) |
blocksFieldPath | string | 'components' | Field path to extract blocks |
referenceFieldPath | string[] | [] | Reference fields to include |
jsonRtePath | string[] | [] | JSON RTE field paths |
locale | string | 'en-us' | Locale |
replaceHtmlCslp | boolean | editableTags | Replace HTML CSLP tags |
seoMeta | SeoMetaInput | - | SEO metadata (passed to useSeoMeta) |
autoSeoMeta | boolean | Record<string, string> | false | Auto-generate SEO from entry |
containerClass | string | 'contentstack-modular-blocks' | Container CSS class |
emptyBlockClass | string | 'visual-builder__empty-block-parent' | Empty block CSS class |
showEmptyState | boolean | true | Show empty state |
keyField | string | '_metadata.uid' | Key field for blocks |
autoExtractBlockName | boolean | true | Auto-extract block name |
blockNamePrefix | string | '' | Prefix to remove from block names when mapping components |
containerProps | Record<string, any> | {} | Additional props to bind to the container element |
emptyStateClass | string | 'contentstack-empty-state' | CSS class for empty state container |
emptyStateMessage | string | 'No content blocks available' | Message shown in empty state |
SEO Metadata
Manual SEO:
<ContentstackModularBlocks
:seo-meta="{
title: 'Page Title',
description: 'Page description',
ogImage: 'https://example.com/image.jpg',
}"
/>
Auto-generate SEO:
<!-- Default field mapping -->
<ContentstackModularBlocks :auto-seo-meta="true" />
<!-- Custom field mapping -->
<ContentstackModularBlocks
:auto-seo-meta="{
title: 'seo_title|title|name',
description: 'meta_description|description',
ogImage: 'featured_image.url',
robots: 'noindex',
}"
/>
Default auto-SEO mapping:
title:seo_title→title→namedescription:seo_description→description→summaryogImage:featured_image.url→og_image.url→image.url
Field mapping syntax:
- Pipe-separated fallbacks:
'seo_title|title|name'— tries each field in order, uses the first with a value - Dot notation:
'featured_image.url'— resolves nested fields - Single field:
'title'— looks up the field on the entry - Static value: If a single value doesn't match any entry field, it's used as a literal (e.g.,
robots: 'noindex')
Exposed Methods
The component exposes a refreshEntry() method via template refs that can be used to programmatically refresh the fetched entry data:
<script setup>
const blocksRef = ref();
function handleRefresh() {
blocksRef.value?.refreshEntry();
}
</script>
<template>
<ContentstackModularBlocks
ref="blocksRef"
content-type-uid="page"
:url="$route.path"
:component-map="componentMapping"
/>
<button @click="handleRefresh">Refresh</button>
</template>
Slots
loading- Custom loading state (auto-fetch mode)error- Custom error state (auto-fetch mode)empty- Custom empty state
ContentstackFallbackBlock
Built-in fallback component for unmapped block types. Displays block title, type badge, and props as formatted JSON.
@nuxt/image Integration
The Contentstack image provider is automatically registered when @nuxt/image is installed. No manual provider configuration needed.
Setup
npm install @nuxt/image@^2.0.0
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
"nuxt-contentstack", // must be listed before @nuxt/image
"@nuxt/image",
],
});
Important:
nuxt-contentstackmust be listed before@nuxt/imagein the modules array so the provider is registered before@nuxt/imageprocesses its configuration.
To set Contentstack as the default image provider (so you don't need provider="contentstack" on every <NuxtImg>):
// nuxt.config.ts
export default defineNuxtConfig({
image: {
provider: "contentstack",
},
});
Usage
<template>
<!-- Basic usage -->
<NuxtImg
:src="image.url"
:alt="image.title"
width="800"
height="400"
:modifiers="{ auto: 'webp,compress', quality: 90 }"
provider="contentstack"
/>
<!-- Responsive -->
<NuxtImg
:src="image.url"
sizes="100vw sm:50vw lg:33vw"
:modifiers="{ auto: 'webp,compress' }"
provider="contentstack"
/>
<!-- Art direction -->
<NuxtPicture
:src="image.url"
sizes="100vw md:50vw"
:modifiers="{ auto: 'webp,compress' }"
provider="contentstack"
/>
</template>
Supported Modifiers
auto:'webp'|'webp,compress'quality:numberformat:'webp'|'png'|'jpg'|'jpeg'|'gif'|'auto'width,height,dpr:numberfit:'bounds'|'crop'blur,brightness,contrast,saturation:numbersharpen:{ amount, radius, threshold }overlay:{ relativeURL, align, repeat, width, height, pad }
Development
# Install dependencies
npm install
# Generate type stubs
npm run dev:prepare
# Develop with playground
npm run dev
# Build playground
npm run dev:build
# Lint
npm run lint
# Test
npm run test
npm run test:watch
# Release
npm run release