Essentials

Composables

Learn how to use Nuxt Contentstack composables to fetch entries, assets, and content with type safety, caching, and live preview support.

Nuxt Contentstack provides a comprehensive set of Vue composables that make fetching content from Contentstack simple, type-safe, and performant. All composables support caching, live preview, personalization, and include built-in error handling.

Master the Contentstack API: Learn how to effectively query and manage content with the TypeScript Delivery SDK documentation. Understand the Content Delivery API, filtering, references, pagination, and best practices for working with Contentstack programmatically.

Overview

The module provides seven composables for different scenarios:

ComposablePurposeReturn Type
useContentstackAccess SDK instances and runtime stateContentstackContext
useGetEntryFetch a single entry by UIDAsyncData<T | null>
useGetEntryByUrlQuery entries by URL fieldAsyncData<T | null>
useGetEntriesFetch multiple entries with filteringAsyncData<{entries: T[], count?: number} | null>
useGetAssetFetch a single asset by UIDAsyncData<Asset | null>
useGetAssetsFetch multiple assets with filteringAsyncData<{assets: T[], count?: number} | null>
useImageTransformTransform Contentstack images dynamically{transformedUrl, transformOptions, updateTransform, resetTransform}
Note: useImageTransform is documented in detail in the Image Optimization guide.

useGetEntry

Fetch a single entry by its unique identifier (UID).

Basic Usage

pages/blog/[slug].vue
<script setup>
// Fetch a single blog post
const { data: post, status, refresh } = await useGetEntry({
  contentTypeUid: 'blog_post',
  entryUid: 'blt123456789',
  locale: 'en-us'
})
</script>

<template>
  <article v-if="post">
    <h1>{{ post.title }}</h1>
    <div v-html="post.content"></div>
  </article>
</template>

With References

Include referenced content like authors, categories, or related entries:

<script setup>
const { data: post } = await useGetEntry({
  contentTypeUid: 'blog_post',
  entryUid: 'blt123456789',
  referenceFieldPath: ['author', 'category', 'related_posts'],
  jsonRtePath: ['content', 'description'],
  locale: 'en-us'
})
</script>

<template>
  <article v-if="post">
    <h1>{{ post.title }}</h1>
    
    <!-- Author information -->
    <div v-if="post.author">
      <p>By {{ post.author.name }}</p>
      <img :src="post.author.avatar.url" :alt="post.author.name">
    </div>
    
    <!-- Category -->
    <span v-if="post.category" class="category">
      {{ post.category.title }}
    </span>
    
    <!-- Content with converted rich text -->
    <div v-html="post.content"></div>
    
    <!-- Related posts -->
    <div v-if="post.related_posts?.length">
      <h3>Related Posts</h3>
      <div v-for="related in post.related_posts" :key="related.uid">
        <NuxtLink :to="`/blog/${related.url}`">
          {{ related.title }}
        </NuxtLink>
      </div>
    </div>
  </article>
</template>

Parameters

ParameterTypeRequiredDefaultDescription
contentTypeUidstring-Content type identifier
entryUidstring-Unique entry identifier
referenceFieldPathstring[][]Reference fields to include
jsonRtePathstring[][]Rich text fields to convert
localestring'en-us'Content locale
replaceHtmlCslpbooleaneditableTagsReplace $ CSLP keys with cslp in response data. Defaults to the editableTags setting

useGetEntryByUrl

Query entries that have a URL field, perfect for page routing and SEO-friendly URLs.

Basic Usage

pages/[...slug].vue
<script setup>
const route = useRoute()
const path = Array.isArray(route.params.slug) 
  ? `/${route.params.slug.join('/')}` 
  : `/${route.params.slug}`

const { data: page } = await useGetEntryByUrl({
  contentTypeUid: 'page',
  url: path,
  referenceFieldPath: ['seo', 'components'],
  jsonRtePath: ['content']
})

// Handle 404 if page not found
if (!page.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Page Not Found'
  })
}
</script>

<template>
  <main v-if="page">
    <Head>
      <Title>{{ page.seo?.title || page.title }}</Title>
      <Meta name="description" :content="page.seo?.description" />
    </Head>
    
    <h1>{{ page.title }}</h1>
    <div v-html="page.content"></div>
    
    <!-- Dynamic components -->
    <div v-for="component in page.components" :key="component.uid">
      <component :is="component._content_type_uid" v-bind="component" />
    </div>
  </main>
</template>

Parameters

ParameterTypeRequiredDefaultDescription
contentTypeUidstring-Content type identifier
urlstring-URL to match against URL field
referenceFieldPathstring[][]Reference fields to include
jsonRtePathstring[][]Rich text fields to convert
localestring'en-us'Content locale
replaceHtmlCslpbooleaneditableTagsReplace $ CSLP keys with cslp in response data. Defaults to the editableTags setting

useGetEntries

Fetch multiple entries with powerful filtering, sorting, and pagination capabilities.

Basic Usage

pages/blog/index.vue
<script setup>
const { data: result } = await useGetEntries({
  contentTypeUid: 'blog_post',
  referenceFieldPath: ['author'],
  limit: 10,
  orderBy: 'created_at',
  includeCount: true
})

const posts = computed(() => result.value?.entries || [])
const totalCount = computed(() => result.value?.count || 0)
</script>

<template>
  <div>
    <h1>Blog Posts ({{ totalCount }})</h1>
    
    <article v-for="post in posts" :key="post.uid">
      <h2>
        <NuxtLink :to="`/blog/${post.url}`">
          {{ post.title }}
        </NuxtLink>
      </h2>
      <p>By {{ post.author?.name }}</p>
      <p>{{ post.excerpt }}</p>
    </article>
  </div>
</template>

Advanced Filtering

Use query operators for complex filtering:

<script setup>
const currentYear = new Date().getFullYear()

const { data: result } = await useGetEntries({
  contentTypeUid: 'blog_post',
  referenceFieldPath: ['author', 'tags'],
  limit: 20,
  skip: 0,
  orderBy: 'published_at',
  includeCount: true,
  where: {
    // Exact match
    status: 'published',
    
    // Array contains
    tags: ['vue', 'nuxt'],
    
    // Comparison operators
    view_count: { $gt: 1000 },
    published_at: { 
      $gte: `${currentYear}-01-01`,
      $lt: `${currentYear + 1}-01-01`
    },
    
    // Existence checks
    featured_image: { $exists: true },
    
    // Pattern matching
    title: { $regex: 'nuxt.*contentstack' },
    
    // Not equal
    author: { $ne: 'guest' }
  }
})
</script>

Pagination

Implement pagination with skip and limit:

pages/blog/page/[page].vue
<script setup>
const route = useRoute()
const page = parseInt(route.params.page) || 1
const limit = 10
const skip = (page - 1) * limit

const { data: result } = await useGetEntries({
  contentTypeUid: 'blog_post',
  limit,
  skip,
  includeCount: true,
  orderBy: 'created_at'
})

const posts = computed(() => result.value?.entries || [])
const totalCount = computed(() => result.value?.count || 0)
const totalPages = computed(() => Math.ceil(totalCount.value / limit))
</script>

<template>
  <div>
    <div v-for="post in posts" :key="post.uid">
      <!-- Post content -->
    </div>
    
    <!-- Pagination -->
    <nav class="pagination">
      <NuxtLink 
        v-if="page > 1" 
        :to="`/blog/page/${page - 1}`"
      >
        Previous
      </NuxtLink>
      
      <span>Page {{ page }} of {{ totalPages }}</span>
      
      <NuxtLink 
        v-if="page < totalPages" 
        :to="`/blog/page/${page + 1}`"
      >
        Next
      </NuxtLink>
    </nav>
  </div>
</template>

Parameters

ParameterTypeRequiredDefaultDescription
contentTypeUidstring-Content type identifier
referenceFieldPathstring[][]Reference fields to include
jsonRtePathstring[][]Rich text fields to convert
localestring'en-us'Content locale
replaceHtmlCslpbooleaneditableTagsReplace $ CSLP keys with cslp in response data. Defaults to the editableTags setting
limitnumber10Number of entries to fetch
skipnumber0Number of entries to skip
orderBystring-Field to sort by
includeCountbooleanfalseInclude total count in response
whereobject{}Query conditions

Query Operators

Use these operators in the where parameter for advanced filtering:

OperatorDescriptionExample
Direct valueExact matchstatus: 'published'
ArrayContains any valuetags: ['vue', 'nuxt']
$gtGreater thanviews: { $gt: 1000 }
$gteGreater than or equaldate: { $gte: '2024-01-01' }
$ltLess thanprice: { $lt: 100 }
$lteLess than or equaldate: { $lte: '2024-12-31' }
$neNot equalauthor: { $ne: 'guest' }
$existsField existsimage: { $exists: true }
$regexPattern matchtitle: { $regex: 'vue.*guide' }

useGetAsset & useGetAssets

Fetch Contentstack assets with filtering capabilities.

Single Asset

<script setup>
const { data: asset } = await useGetAsset({
  assetUid: 'blt123456789',
  locale: 'en-us'
})
</script>

<template>
  <div v-if="asset">
    <img :src="asset.url" :alt="asset.title">
    <p>{{ asset.description }}</p>
  </div>
</template>

Multiple Assets

<script setup>
const { data: result } = await useGetAssets({
  locale: 'en-us',
  limit: 20,
  includeCount: true,
  where: {
    content_type: 'image/jpeg'
  }
})

const images = computed(() => result.value?.assets || [])
const totalCount = computed(() => result.value?.count || 0)
</script>

<template>
  <div class="gallery">
    <p>Total: {{ totalCount }}</p>
    <div v-for="image in images" :key="image.uid">
      <img :src="image.url" :alt="image.title">
    </div>
  </div>
</template>

Parameters

useGetAsset Parameters

ParameterTypeRequiredDefaultDescription
assetUidstring-Unique asset identifier
localestring'en-us'Content locale

useGetAssets Parameters

ParameterTypeRequiredDefaultDescription
localestring'en-us'Content locale
limitnumber10Number of assets to fetch
skipnumber0Number of assets to skip
orderBystring-Field to sort by (e.g., 'created_at', 'updated_at')
includeCountbooleanfalseInclude total count in response
whereobject{}Query conditions (note: most filters are client-side)
Asset Filtering: Only content_type is filtered server-side via the Contentstack API. All other where filters (like $gt, $lt, $exists, $regex, exact match, and array containment) are applied client-side after fetching. Keep this in mind when working with large asset libraries.

Advanced Asset Filtering

<script setup>
const { data: result } = await useGetAssets({
  locale: 'en-us',
  limit: 50,
  skip: 0,
  orderBy: 'created_at',
  includeCount: true,
  where: {
    content_type: 'image/jpeg',
    // Note: content_type is filtered server-side, other filters are applied client-side
    file_size: { $gt: '10000' },
    title: { $regex: 'hero' }
  }
})

const totalAssets = computed(() => result.value?.assets?.length || 0)
</script>

Automatic Features

All entry composables automatically call includeFallback() and includeEmbeddedItems() on queries, providing:

  • Locale Fallback: If content isn't available in the requested locale, the API falls back to the default locale
  • Embedded Items: Referenced items embedded in Rich Text Editor fields are automatically included in responses

Live Preview Integration

All composables automatically support live preview when enabled:

<script setup>
const { data: page, refresh } = await useGetEntryByUrl({
  contentTypeUid: 'page',
  url: '/about'
})

// refresh() is automatically called when content changes in live preview
</script>

Caching

All composables leverage Nuxt's built-in caching system for optimal performance. Understanding how caching works helps you optimize your application and handle cache invalidation properly.

How Caching Works

Nuxt Contentstack composables use Nuxt's useAsyncData under the hood, which provides:

  • Automatic request deduplication: Multiple calls to the same composable with the same parameters share a single request
  • SSR caching: Responses are cached during server-side rendering
  • Client-side caching: Responses are cached in the browser
  • Revalidation: Automatic cache revalidation on navigation

Cache Keys

Cache keys are automatically generated based on composable parameters. For example:

  • useGetEntry: {contentTypeUid}-{entryUid}-{locale}-{variantAlias}
  • useGetEntryByUrl: {contentTypeUid}-{url}-{locale}-{variantAlias}
  • useGetEntries: {contentTypeUid}-entries-{locale}-{limit}-{skip}-{where}-{variantAlias}
  • useGetAsset: asset-{assetUid}-{locale}
  • useGetAssets: assets-{locale}-{limit}-{skip}-{where}

This means identical queries share the same cache:

<!-- Both components share the same cache -->
<ComponentA>
  const { data } = await useGetEntry({ contentTypeUid: 'page', entryUid: 'blt123' })
</ComponentA>

<ComponentB>
  const { data } = await useGetEntry({ contentTypeUid: 'page', entryUid: 'blt123' })
</ComponentB>

Cache Invalidation

Manual Refresh

Use the refresh() method to manually invalidate and refetch:

<script setup>
const { data: page, refresh } = await useGetEntryByUrl({
  contentTypeUid: 'page',
  url: '/about'
})

// Refresh content manually
const handleRefresh = async () => {
  await refresh()
}
</script>

Live Preview Auto-Refresh

When Live Preview is enabled, composables automatically refresh when content changes:

<script setup>
// Automatically refreshes when content changes in Contentstack
const { data: page, refresh } = await useGetEntryByUrl({
  contentTypeUid: 'page',
  url: '/about'
})

// refresh() is called automatically by Live Preview SDK
</script>

Programmatic Cache Control

For advanced cache control, use Nuxt's refreshCookie:

// Force refresh all cached data
await refreshCookie('contentstack')

// Or use the composable's refresh method
await refresh()

Cache Configuration

Custom Cache TTL

Control cache duration (requires Nuxt configuration):

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/blog/**': {
      // Cache blog pages for 1 hour
      swr: 3600
    }
  }
})

Cache Strategies

Static Content

For content that rarely changes, rely on default caching:

<script setup>
// Cached automatically, refreshed on navigation
const { data: aboutPage } = await useGetEntryByUrl({
  contentTypeUid: 'page',
  url: '/about'
})
</script>

Dynamic Content

For frequently updated content, implement manual refresh:

<script setup>
const { data: posts, refresh } = await useGetEntries({
  contentTypeUid: 'blog_post',
  limit: 10
})

// Refresh every 5 minutes
const interval = setInterval(() => {
  refresh()
}, 5 * 60 * 1000)

onUnmounted(() => clearInterval(interval))
</script>

Live Preview Mode

In preview environments, disable aggressive caching:

nuxt.config.ts
const isPreview = process.env.NUXT_CONTENTSTACK_ENVIRONMENT === 'preview'

export default defineNuxtConfig({
  routeRules: {
    // Disable caching in preview mode
    '/**': isPreview ? { swr: false } : { swr: 3600 }
  }
})

Cache Best Practices

  • Use appropriate cache strategies: Static content can be cached longer, dynamic content needs frequent updates
  • Invalidate on mutations: If you modify content via API, manually refresh affected composables
  • Monitor cache performance: Use browser DevTools to monitor cache hits
  • Consider ISR: For production, consider Incremental Static Regeneration patterns
  • Handle stale data: Show loading states during cache revalidation
  • Use Live Preview: Automatically handles cache invalidation during content editing

Troubleshooting Cache Issues

Content not updating?

  • Check if Live Preview is enabled and working
  • Manually call refresh() to force update
  • Clear browser cache or use incognito mode
  • Verify cache keys are different for different queries

Too many requests?

  • Ensure identical queries share cache (check parameters match exactly)
  • Use server: false for client-only content
  • Implement proper loading states

Stale data in production?

  • Configure appropriate swr values in routeRules
  • Implement manual refresh mechanisms
  • Consider using webhooks for cache invalidation

TypeScript Support

Nuxt Contentstack provides full TypeScript support for type-safe content management. You can define interfaces manually or generate types automatically from your Contentstack content models.

Manual Type Definitions

Define interfaces for your content types manually:

types/contentstack.ts
export interface BlogPost {
  uid: string
  title: string
  url: string
  content: string
  excerpt: string
  published_at: string
  author?: {
    uid: string
    name: string
    avatar?: {
      url: string
      title: string
    }
  }
  tags?: string[]
  featured_image?: {
    url: string
    title: string
  }
}

export interface Page {
  uid: string
  title: string
  url: string
  content: string
  seo?: {
    title: string
    description: string
    og_image?: {
      url: string
    }
  }
}
<script setup lang="ts">
// Type-safe usage
const { data: post } = await useGetEntry<BlogPost>({
  contentTypeUid: 'blog_post',
  entryUid: 'blt123456789',
})

// TypeScript knows the structure of post.value
if (post.value) {
  console.log(post.value.title) // ✅ Type-safe
  console.log(post.value.author?.name) // ✅ Type-safe with optional chaining
}
</script>

Automatic Type Generation

Use the Contentstack CLI tsgen plugin to automatically generate TypeScript types from your Contentstack content models.

Installation

npm install -g @contentstack/cli

Configuration

  1. Login to Contentstack CLI:
csdx auth:login
  1. Initialize Type Generation:
csdx plugins:install @contentstack/cli-plugin-tsgen

Generate Types

Generate types for your stack:

# Generate types for all content types
csdx tsgen --stack-api-key YOUR_API_KEY --output-dir ./types/contentstack

# Generate types for specific content types
csdx tsgen --stack-api-key YOUR_API_KEY --content-types blog_post,page --output-dir ./types/contentstack

# Generate types with specific environment
csdx tsgen --stack-api-key YOUR_API_KEY --environment production --output-dir ./types/contentstack

Using Generated Types

After generation, import and use the types:

types/contentstack.ts
// Auto-generated types
export type BlogPost = {
  uid: string
  title: string
  // ... other fields from Contentstack
}

export type Page = {
  uid: string
  title: string
  // ... other fields from Contentstack
}
<script setup lang="ts">
import type { BlogPost } from '~/types/contentstack'

const { data: post } = await useGetEntry<BlogPost>({
  contentTypeUid: 'blog_post',
  entryUid: 'blt123456789',
})
</script>

Type Generation Workflow

Create a script to automate type generation:

package.json
{
  "scripts": {
    "generate:types": "csdx tsgen --stack-api-key $NUXT_CONTENTSTACK_API_KEY --output-dir ./types/contentstack",
    "dev": "npm run generate:types && nuxt dev"
  }
}

Type Safety Best Practices

Use Generic Types

Always use generic types with composables:

// ✅ Good - Type-safe
const { data: post } = await useGetEntry<BlogPost>({
  contentTypeUid: 'blog_post',
  entryUid: 'blt123'
})

// ❌ Bad - No type safety
const { data: post } = await useGetEntry({
  contentTypeUid: 'blog_post',
  entryUid: 'blt123'
})

Handle Nullable Types

Composables return T | null, always check for null:

const { data: post } = await useGetEntry<BlogPost>({
  contentTypeUid: 'blog_post',
  entryUid: 'blt123'
})

// ✅ Safe - Check for null
if (post.value) {
  console.log(post.value.title)
}

// ✅ Safe - Use optional chaining
console.log(post.value?.title)

Type References

Type reference fields properly:

interface BlogPost {
  uid: string
  title: string
  author: {
    uid: string
    name: string
  }
  related_posts?: BlogPost[] // Self-referencing type
}

Type Guards

Create type guards for runtime type checking:

function isBlogPost(entry: unknown): entry is BlogPost {
  return (
    typeof entry === 'object' &&
    entry !== null &&
    'uid' in entry &&
    'title' in entry &&
    'content' in entry
  )
}

const { data: entry } = await useGetEntry({
  contentTypeUid: 'blog_post',
  entryUid: 'blt123'
})

if (entry.value && isBlogPost(entry.value)) {
  // TypeScript knows entry.value is BlogPost
  console.log(entry.value.title)
}

Type Definitions Structure

Generated types typically include:

  • Entry fields: All fields from your content type
  • Metadata: _metadata object with UID, created_at, updated_at
  • References: Typed reference fields
  • Assets: Typed asset fields with URL and metadata
  • Modular Blocks: Typed modular block structures
  • Live Preview: CSLP attributes (when enabled)

Troubleshooting Types

Types not updating?

  • Regenerate types after content model changes
  • Clear TypeScript cache: rm -rf .nuxt .output node_modules/.cache
  • Restart TypeScript server in your IDE

Missing fields?

  • Ensure content type is published in Contentstack
  • Check environment matches your configuration
  • Verify API key has access to content types

Type errors?

  • Check for nullable fields (field?: type vs field: type)
  • Verify reference types match actual content types
  • Ensure modular block types are properly defined

Error Handling

All composables include built-in error handling and return standard Nuxt AsyncData objects with status, error, and data properties.

Basic Error Handling

<script setup>
const { data: post, status, error } = await useGetEntry({
  contentTypeUid: 'blog_post',
  entryUid: 'invalid-uid'
})

// Handle different states
</script>

<template>
  <div>
    <div v-if="status === 'pending'">Loading...</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else-if="post">{{ post.title }}</div>
    <div v-else>No content found</div>
  </div>
</template>

Error Types

The module uses standardized error codes internally:

Error CodeDescriptionCommon Causes
NETWORK_ERRORFailed API requestsNetwork issues, connection refused, timeouts, fetch failures
NOT_FOUNDContent doesn't existInvalid UID, deleted entry, wrong content type, 404 response
VALIDATION_ERRORInvalid query parametersWrong field names, invalid operators, 400/422 responses
UNKNOWNUnclassified errorsUnexpected failures, server errors

Advanced Error Handling

Handle specific error types and implement retry logic:

<script setup>
const { data: post, error, status, refresh } = await useGetEntry({
  contentTypeUid: 'blog_post',
  entryUid: 'blt123456789'
})

// Check for specific error types
const handleError = () => {
  if (!error.value) return
  
  // Network errors - retry logic
  if (error.value.message?.includes('network') || error.value.message?.includes('fetch')) {
    console.warn('Network error, retrying...')
    setTimeout(() => refresh(), 2000)
    return
  }
  
  // Authentication errors - redirect to error page
  if (error.value.statusCode === 401 || error.value.statusCode === 403) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Authentication failed'
    })
  }
  
  // Not found - show 404
  if (error.value.statusCode === 404) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Content not found'
    })
  }
}

watch(error, handleError, { immediate: true })
</script>

Error Boundaries

Create reusable error handling components:

components/ContentErrorBoundary.vue
<script setup>
interface Props {
  error: Error | null
  status: 'idle' | 'pending' | 'error' | 'success'
  onRetry?: () => void
}

const props = defineProps<Props>()

const errorMessage = computed(() => {
  if (!props.error) return 'Unknown error'
  
  if (props.error.statusCode === 404) {
    return 'Content not found'
  }
  
  if (props.error.statusCode === 401 || props.error.statusCode === 403) {
    return 'Access denied'
  }
  
  return props.error.message || 'Failed to load content'
})
</script>

<template>
  <div v-if="status === 'error'" class="error-boundary">
    <div class="error-content">
      <h3>Error Loading Content</h3>
      <p>{{ errorMessage }}</p>
      <button v-if="onRetry" @click="onRetry" class="retry-button">
        Try Again
      </button>
    </div>
  </div>
</template>

Retry Strategies

Implement retry logic for transient errors:

composables/useRetryableContent.ts
export const useRetryableContent = <T>(
  composable: () => Promise<AsyncData<T | null, Error | null>>,
  maxRetries = 3
) => {
  const result = ref<AsyncData<T | null, Error | null> | null>(null)
  const retryCount = ref(0)
  
  const fetchWithRetry = async () => {
    try {
      result.value = await composable()
      
      if (result.value.error && retryCount.value < maxRetries) {
        retryCount.value++
        await new Promise(resolve => setTimeout(resolve, 1000 * retryCount.value))
        return fetchWithRetry()
      }
    } catch (error) {
      if (retryCount.value < maxRetries) {
        retryCount.value++
        await new Promise(resolve => setTimeout(resolve, 1000 * retryCount.value))
        return fetchWithRetry()
      }
      throw error
    }
  }
  
  return {
    result: readonly(result),
    retryCount: readonly(retryCount),
    fetchWithRetry
  }
}

Best Practices

  • Always check error state: Use status === 'error' or check error value before accessing data
  • Handle 404 gracefully: Show user-friendly messages for missing content
  • Implement retry logic: For network errors, implement exponential backoff
  • Log errors: Use error tracking services for production debugging
  • Provide fallbacks: Show default content or placeholders when content fails to load
  • Validate input: Check UIDs and parameters before making requests

Best Practices

Performance

  • Use appropriate limit values to avoid over-fetching
  • Include only necessary reference fields
  • Enable caching for frequently accessed content

SEO

  • Use useGetEntryByUrl for page routing
  • Include structured data and meta tags
  • Handle 404 cases gracefully

Content Management

  • Enable live preview for content teams
  • Structure reference fields logically

Development

  • Use TypeScript interfaces for content types
    • use contentstack CLI tsgen plugin to fetch types.
  • Handle loading and error states
  • Test with different content scenarios

Runtime Utilities

Access the Contentstack SDK and runtime utilities through the useContentstack() composable or the $contentstack plugin object.

useContentstack Composable

The useContentstack() composable provides typed access to all Contentstack runtime objects:

<script setup>
const {
  stack,                      // Contentstack Delivery SDK Stack instance
  livePreviewEnabled,         // Whether live preview is active
  editableTags,               // Whether editable tags are enabled
  variantAlias,               // Current personalization variant alias (reactive)
  ContentstackLivePreview,    // Live Preview SDK instance
  personalizeSdk,             // Personalize Edge SDK instance (client-side only, null on server)
  VB_EmptyBlockParentClass,   // CSS class for Visual Builder empty blocks
} = useContentstack()
</script>

Direct SDK Access

Use the Contentstack Delivery SDK directly for advanced use cases:

const { stack } = useContentstack()

// Create custom queries using the Stack instance
const query = stack.contentType('blog_post')
  .entry()
  .locale('en-us')
  .includeFallback()
  .includeEmbeddedItems()

const result = await query.query().find()

Live Preview SDK

Access Live Preview SDK directly:

const { livePreviewEnabled, ContentstackLivePreview } = useContentstack()

if (livePreviewEnabled && import.meta.client) {
  // Custom live preview handlers
  ContentstackLivePreview.onEntryChange((data) => {
    console.log('Entry changed:', data)
    // Custom handling
  })
}

Personalization SDK

Access Personalization SDK directly:

const { personalizeSdk, variantAlias } = useContentstack()

if (personalizeSdk) {
  // Set custom attributes
  await personalizeSdk.set({
    custom_attribute: 'value'
  })

  // Get variants
  const variants = personalizeSdk.getVariants()

  // Trigger custom events
  await personalizeSdk.triggerEvent('custom_event')

  // Check current variant alias
  console.log('Current variant:', variantAlias?.value)
}

Custom Utilities

Create custom utilities using the composable:

composables/useContentstackUtils.ts
export const useContentstackUtils = () => {
  const { stack, livePreviewEnabled, personalizeSdk } = useContentstack()

  const getStackInstance = () => stack
  const isLivePreviewEnabled = () => livePreviewEnabled
  const isPersonalizationEnabled = () => !!personalizeSdk

  return {
    getStackInstance,
    isLivePreviewEnabled,
    isPersonalizationEnabled
  }
}

Next Steps

Image Optimization

Learn about image transformations and @nuxt/image integration.

Performance Optimization

Optimize your application for speed and efficiency.

Live Preview

Enable real-time content editing and visual building.