Essentials

Components

Learn about the Vue components provided by Nuxt Contentstack, including the powerful ContentstackModularBlocks component for dynamic content rendering.

ContentstackModularBlocks Component

A flexible, generic component for rendering Contentstack modular blocks as Vue components. Perfect for dynamic page layouts, component libraries, and content-driven UIs.

What are Modular Blocks?

Modular Blocks are a powerful content management feature in Contentstack that allows you to create and manage reusable content elements. Think of them as building blocks that can be assembled and rearranged to construct unique page structures.

With Modular Blocks, content managers can:

  • Dynamically create pages by selecting, arranging, and combining different block types
  • Reuse content elements across multiple pages while maintaining consistency
  • Customize layouts without developer intervention by choosing which blocks to include
  • Manage content centrally so updates to a block are automatically reflected everywhere it's used

Each modular block represents a distinct content element (like a hero section, image gallery, or text block) that can be combined with other blocks to create flexible, dynamic page layouts. Content managers can rearrange, include, or exclude blocks as needed, providing enhanced control over page structure.

For example, a "Photo Showcase" modular block field might offer three available blocks: "Image," "Carousel," and "Paragraph." In any given entry, content managers can choose to use only the "Paragraph" and "Carousel" blocks, skipping the "Image" block entirely.

Learn More: Want to dive deeper into Modular Blocks? Check out the Contentstack Academy course on Modular Blocks for comprehensive training, or visit the Contentstack documentation for technical details.
The component now supports auto-fetch capability - it can fetch entry data and extract blocks automatically, making it even easier to build dynamic pages.

Features

  • Auto-component mapping - Automatically maps Contentstack block types to Vue components
  • Auto-fetch capability - Can fetch entry data and extract blocks automatically
  • SEO metadata support - Automatic SEO generation with useSeoMeta integration
  • Flexible data structure - Works with various Contentstack modular block formats
  • Live Preview ready - Full support for Contentstack Live Preview with data-cslp attributes
  • Visual Builder support - Includes empty state classes for visual building
  • TypeScript support - Comprehensive type definitions with generics
  • SSR compatible - Renders perfectly on server and hydrates seamlessly
  • Customizable styling - Configurable CSS classes and container props
  • Error handling - Graceful fallbacks for unmapped components
  • Slot support - Custom loading, error, and empty state content via slots

Usage Patterns

The component supports two usage patterns for maximum flexibility:

Pattern 1: Auto-fetch Entry + Render Blocks (NEW)

Perfect for simple page rendering - just provide entry details and let the component handle everything:

pages/[...slug].vue
<script setup>
import Hero from "~/components/blocks/Hero.vue"
import Grid from "~/components/blocks/Grid.vue"
import TextBlock from "~/components/blocks/TextBlock.vue"

// Map Contentstack block types to Vue components
const componentMapping = {
  hero: Hero,
  grid: Grid,
  text_block: TextBlock,
}
</script>

<template>
  <!-- Component fetches entry and renders blocks automatically -->
  <ContentstackModularBlocks
    content-type-uid="page"
    :url="$route.path"
    blocks-field-path="components"
    :reference-field-path="['blocks.block.image']"
    :json-rte-path="['rich_text', 'blocks.block.copy']"
    locale="en-us"
    :component-map="componentMapping"
  >
    <!-- Custom loading state -->
    <template #loading>
      <div class="loading-spinner">Loading page content...</div>
    </template>

    <!-- Custom error state -->
    <template #error>
      <div class="error-message">Failed to load content</div>
    </template>
  </ContentstackModularBlocks>
</template>

Pattern 2: Traditional with Pre-fetched Blocks

For when you need more control over data fetching:

pages/dynamic-page.vue
<script setup>
import Hero from "~/components/blocks/Hero.vue"
import Grid from "~/components/blocks/Grid.vue"
import TextBlock from "~/components/blocks/TextBlock.vue"

// Map Contentstack block types to Vue components
const componentMapping = {
  hero: Hero,
  grid: Grid,
  text_block: TextBlock,
}

// Fetch your page data manually
const { data: page } = await useGetEntryByUrl({
  contentTypeUid: "page",
  url: useRoute().path,
})
</script>

<template>
  <!-- Pass pre-fetched blocks -->
  <ContentstackModularBlocks
    :blocks="page.components"
    :component-map="componentMapping"
  />
</template>

Advanced Usage

pages/advanced-example.vue
<script setup>
import Hero from "~/components/blocks/Hero.vue"
import Grid from "~/components/blocks/Grid.vue"
import DefaultBlock from "~/components/blocks/DefaultBlock.vue"

const componentMapping = {
  hero: Hero,
  grid: Grid,
  text_section: TextSection,
  image_gallery: ImageGallery,
}
</script>

<template>
  <ContentstackModularBlocks
    :blocks="page.modular_blocks"
    :component-map="componentMapping"
    :fallback-component="DefaultBlock"
    :auto-extract-block-name="true"
    :show-empty-state="true"
    container-class="page-blocks"
    empty-block-class="visual-builder__empty-block-parent"
    empty-state-message="No content blocks available"
    key-field="_metadata.uid"
    block-name-prefix="block_"
    :container-props="{ 'data-page-id': page.uid }"
  >
    <!-- Custom empty state -->
    <template #empty>
      <div class="custom-empty-state">
        <h3>No content blocks found</h3>
        <p>Please add some content in Contentstack</p>
      </div>
    </template>
  </ContentstackModularBlocks>
</template>

Props Reference

Core Props

PropTypeDefaultDescription
blocksContentstackBlock[][]Array of Contentstack modular blocks
componentMapComponentMapping{}Object mapping block types to Vue components
fallbackComponentComponent | stringContentstackFallbackBlockFallback component for unmapped block types

Auto-fetch Props (NEW)

PropTypeDefaultDescription
contentTypeUidstringundefinedContent type UID for fetching entry
urlstringundefinedURL to fetch entry by
referenceFieldPathstring[][]Reference field paths to include
jsonRtePathstring[][]JSON RTE field paths
localestring'en-us'Locale for the entry
replaceHtmlCslpbooleanfalseReplace HTML CSLP tags
blocksFieldPathstring'components'Field path to extract modular blocks from
seoMetaSeoMetaInputundefinedSEO metadata object (passed directly to useSeoMeta)
autoSeoMetaboolean | Record<string, string>falseAuto-generate SEO from entry data

Styling Props

PropTypeDefaultDescription
containerClassstring'contentstack-modular-blocks'CSS class for the container
emptyBlockClassstring'visual-builder__empty-block-parent'CSS class for empty blocks (Visual Builder)
containerPropsRecord<string, any>{}Additional props to bind to the container
showEmptyStatebooleantrueShow empty state when no blocks
emptyStateClassstring'contentstack-empty-state'CSS class for empty state
emptyStateMessagestring'No content blocks available'Message to show in empty state

Advanced Props

PropTypeDefaultDescription
keyFieldstring'_metadata.uid'Custom key field for blocks
autoExtractBlockNamebooleantrueAuto-extract block name from object keys
blockNamePrefixstring''Prefix to remove from block names

SEO Metadata Support

The component can automatically set SEO metadata using Nuxt's native useSeoMeta. You can pass SEO directly, or auto-generate it from entry data.

Manual SEO

Pass SEO metadata directly:

pages/[...slug].vue
<template>
  <ContentstackModularBlocks
    content-type-uid="page"
    :url="$route.path"
    :seo-meta="{
      title: 'My Page Title',
      description: 'My page description',
      ogImage: 'https://example.com/image.jpg',
    }"
    :component-map="componentMapping"
  />
</template>

Auto-Generate SEO

Automatically extract SEO from entry data:

pages/[...slug].vue
<template>
  <!-- Auto-generate SEO using default field mapping -->
  <ContentstackModularBlocks
    content-type-uid="page"
    :url="$route.path"
    :auto-seo-meta="true"
    :component-map="componentMapping"
  />
</template>

Custom Field Mapping

Use custom field mapping with fallback support:

pages/[...slug].vue
<template>
  <ContentstackModularBlocks
    content-type-uid="page"
    :url="$route.path"
    :auto-seo-meta="{
      title: 'page_title|title',
      description: 'meta_description|description',
      ogImage: 'featured_image.url',
    }"
    :component-map="componentMapping"
  />
</template>

Default Field Mapping

When autoSeoMeta: true, these fields are automatically mapped with fallbacks:

SEO Meta TagEntry Fields (fallback order)
titleseo_titletitlename
descriptionseo_descriptiondescriptionsummary
ogTitleseo_titletitlename
ogDescriptionseo_descriptiondescriptionsummary
ogImagefeatured_image.urlog_image.urlimage.url

The seoMeta prop accepts all useSeoMeta options, including Open Graph, Twitter Cards, and canonical URLs. Manual seoMeta values override auto-generated ones.

Data Structure Support

The component supports two common Contentstack modular block structures:

Auto-extraction (default)

{
  "components": [
    {
      "hero": {
        "title": "Welcome",
        "subtitle": "To our site"
      },
      "_metadata": { "uid": "hero_123" }
    },
    {
      "grid": {
        "columns": 3,
        "items": [...]
      },
      "_metadata": { "uid": "grid_456" }
    }
  ]
}

Content type based

{
  "modular_blocks": [
    {
      "_content_type_uid": "hero_block",
      "title": "Welcome",
      "subtitle": "To our site",
      "_metadata": { "uid": "hero_123" }
    },
    {
      "_content_type_uid": "grid_block",
      "columns": 3,
      "items": [...],
      "_metadata": { "uid": "grid_456" }
    }
  ]
}

Component Props

Each rendered component receives:

// Original block props
{
  title: "Welcome",
  subtitle: "To our site",
  // ... other block fields

  // Additional meta props
  blockType: "hero",
  blockMetadata: { uid: "hero_123", ... }
}

Live Preview Integration

The component automatically adds Live Preview attributes:

<section class="contentstack-modular-blocks">
  <component
    :is="Hero"
    :title="Welcome"
    data-block-type="hero"
    data-block-index="0"
    data-cslp="hero.title"
  />
</section>

Exposed Methods

The component exposes methods via template refs for programmatic control:

MethodDescription
refreshEntryRe-fetch the entry data (only works in auto-fetch mode)
pages/[...slug].vue
<script setup>
const blocksRef = ref()

// Programmatically refresh the entry data
const handleRefresh = () => {
  blocksRef.value?.refreshEntry()
}
</script>

<template>
  <ContentstackModularBlocks
    ref="blocksRef"
    content-type-uid="page"
    :url="$route.path"
    :component-map="componentMapping"
  />
  <button @click="handleRefresh">Refresh Content</button>
</template>

Slots

The component provides several slots for customizing different states:

SlotDescriptionAvailable When
loadingCustom loading state contentAuto-fetch is enabled and data is loading
errorCustom error state contentAuto-fetch fails or encounters an error
emptyCustom empty state contentNo blocks are available to render

Custom Slot Examples

<template>
  <ContentstackModularBlocks
    content-type-uid="page"
    :url="$route.path"
    :component-map="componentMapping"
  >
    <!-- Custom loading spinner -->
    <template #loading>
      <div class="flex items-center justify-center py-12">
        <div
          class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"
        ></div>
        <span class="ml-3">Loading page content...</span>
      </div>
    </template>

    <!-- Custom error message -->
    <template #error>
      <div class="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
        <h3 class="text-red-800 font-semibold">Content Unavailable</h3>
        <p class="text-red-600 mt-2">
          Unable to load page content. Please try again later.
        </p>
      </div>
    </template>

    <!-- Custom empty state -->
    <template #empty>
      <div class="text-center py-12 text-gray-500">
        <h3 class="text-lg font-medium">No Content Available</h3>
        <p class="mt-2">This page doesn't have any content blocks yet.</p>
      </div>
    </template>
  </ContentstackModularBlocks>
</template>

Error Handling

The component gracefully handles various error scenarios:

  • Missing components: Falls back to fallbackComponent
  • Empty blocks: Shows configurable empty state (customizable via #empty slot)
  • Invalid data: Gracefully handles malformed block data
  • Missing keys: Uses index-based keys as fallback
  • Auto-fetch errors: Shows error state (customizable via #error slot)
  • Loading states: Shows loading state during auto-fetch (customizable via #loading slot)

ContentstackFallbackBlock

The module includes a built-in fallback component that provides a developer-friendly display for unmapped block types. This component automatically:

  • Displays the block title (from title, name, heading, or blockType fields)
  • Shows the block type in a styled badge
  • Renders all props as formatted JSON in an expandable details section
  • Provides helpful guidance on how to map the component properly
  • Supports dark mode for better developer experience

Features

  • 🎨 Styled interface with clear visual hierarchy
  • 📱 Responsive design that works on all screen sizes
  • 🌙 Dark mode support with prefers-color-scheme
  • 🔍 Collapsible JSON to avoid cluttering the UI
  • 🛠️ Developer hints showing how to fix unmapped components

Example Output

┌─────────────────────────────────────┐
│ Welcome Hero                Type: hero │
├─────────────────────────────────────┤
│ ▶ View Props                        │
│   {                                 │
│     "title": "Welcome Hero",        │
│     "subtitle": "Get started now",  │
│     "cta_text": "Learn More"       │
│   }                                 │
├─────────────────────────────────────┤
│ This is a fallback component.       │
│ Map "hero" to a proper Vue component│
└─────────────────────────────────────┘

Custom Fallback Component

You can override the default fallback by providing your own:

<ContentstackModularBlocks
  :blocks="page.components"
  :component-map="componentMapping"
  :fallback-component="MyCustomFallback"
/>

Creating Block Components

When creating your block components, follow these patterns for the best experience:

Basic Block Component

components/blocks/Hero.vue
<script setup lang="ts">
interface Props {
  title: string
  subtitle?: string
  cta_text?: string
  cta_link?: string
  background_image?: {
    url: string
    title: string
  }
  // Meta props automatically added by ContentstackModularBlocks
  blockType?: string
  blockMetadata?: any
}

const props = defineProps<Props>()
</script>

<template>
  <section class="hero" :style="{ backgroundImage: `url(${background_image?.url})` }">
    <div class="hero-content">
      <h1>{{ title }}</h1>
      <p v-if="subtitle">{{ subtitle }}</p>
      <NuxtLink v-if="cta_link" :to="cta_link" class="cta-button">
        {{ cta_text || 'Learn More' }}
      </NuxtLink>
    </div>
  </section>
</template>

<style scoped>
.hero {
  @apply min-h-screen flex items-center justify-center bg-cover bg-center;
}

.hero-content {
  @apply text-center text-white max-w-4xl mx-auto px-6;
}

.hero h1 {
  @apply text-5xl font-bold mb-4;
}

.hero p {
  @apply text-xl mb-8;
}

.cta-button {
  @apply inline-block bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-8 rounded-lg transition-colors;
}
</style>

Block Component with References

components/blocks/ArticleList.vue
<script setup lang="ts">
interface Article {
  uid: string
  title: string
  url: string
  excerpt: string
  featured_image?: {
    url: string
    title: string
  }
  author?: {
    name: string
    avatar?: {
      url: string
    }
  }
}

interface Props {
  title: string
  articles: Article[]
  show_author?: boolean
  blockType?: string
  blockMetadata?: any
}

const props = defineProps<Props>()
</script>

<template>
  <section class="article-list">
    <div class="container mx-auto px-6 py-12">
      <h2 class="text-3xl font-bold mb-8">{{ title }}</h2>
      
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        <article 
          v-for="article in articles" 
          :key="article.uid"
          class="article-card"
        >
          <NuxtImg
            v-if="article.featured_image"
            :src="article.featured_image.url"
            :alt="article.featured_image.title"
            class="article-image"
            width="400"
            height="250"
            provider="contentstack"
          />
          
          <div class="article-content">
            <h3 class="article-title">
              <NuxtLink :to="`/blog/${article.url}`">
                {{ article.title }}
              </NuxtLink>
            </h3>
            
            <p class="article-excerpt">{{ article.excerpt }}</p>
            
            <div v-if="show_author && article.author" class="article-author">
              <img 
                v-if="article.author.avatar"
                :src="article.author.avatar.url" 
                :alt="article.author.name"
                class="author-avatar"
              >
              <span>{{ article.author.name }}</span>
            </div>
          </div>
        </article>
      </div>
    </div>
  </section>
</template>

<style scoped>
.article-card {
  @apply bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow;
}

.article-image {
  @apply w-full h-48 object-cover;
}

.article-content {
  @apply p-6;
}

.article-title a {
  @apply text-xl font-semibold text-gray-900 hover:text-blue-600 transition-colors;
}

.article-excerpt {
  @apply text-gray-600 mt-2 mb-4;
}

.article-author {
  @apply flex items-center space-x-2 text-sm text-gray-500;
}

.author-avatar {
  @apply w-6 h-6 rounded-full;
}
</style>

Best Practices

Component Organization

components/
├── blocks/
│   ├── Hero.vue
│   ├── Grid.vue
│   ├── TextBlock.vue
│   ├── ImageGallery.vue
│   └── ArticleList.vue
└── ui/
    ├── Button.vue
    └── Card.vue

Component Mapping

Nuxt 4 automatically imports exports from /utils files, making component mapping simple:

utils/index.ts
import Hero from '~/components/blocks/Hero.vue'
import Grid from '~/components/blocks/Grid.vue'
import TextBlock from '~/components/blocks/TextBlock.vue'
import ImageGallery from '~/components/blocks/ImageGallery.vue'
import ArticleList from '~/components/blocks/ArticleList.vue'

export const componentMap = {
  hero: Hero,
  grid: Grid,
  text_block: TextBlock,
  image_gallery: ImageGallery,
  article_list: ArticleList,
}
pages/[...slug].vue
<script setup>
// componentMap is auto-imported from utils/index.ts
</script>

<template>
  <ContentstackModularBlocks
    content-type-uid="page"
    :url="$route.path"
    :component-map="componentMap"
  />
</template>
Auto-imports: Nuxt 4 automatically imports exports from /utils files. Simply export your componentMap and use it directly - no imports needed!

Alternative: Using a Composable

If you prefer a composable pattern:

composables/useComponentMapping.ts
import Hero from '~/components/blocks/Hero.vue'
import Grid from '~/components/blocks/Grid.vue'
import TextBlock from '~/components/blocks/TextBlock.vue'
import ImageGallery from '~/components/blocks/ImageGallery.vue'
import ArticleList from '~/components/blocks/ArticleList.vue'

export const useComponentMapping = () => {
  return {
    hero: Hero,
    grid: Grid,
    text_block: TextBlock,
    image_gallery: ImageGallery,
    article_list: ArticleList,
  }
}
pages/[...slug].vue
<script setup>
const componentMapping = useComponentMapping()
</script>

<template>
  <ContentstackModularBlocks
    content-type-uid="page"
    :url="$route.path"
    :component-map="componentMapping"
  />
</template>

TypeScript Integration

types/blocks.ts
// Define interfaces for your block types
export interface HeroBlock {
  title: string
  subtitle?: string
  cta_text?: string
  cta_link?: string
  background_image?: ContentstackAsset
}

export interface GridBlock {
  columns: number
  items: GridItem[]
}

export interface GridItem {
  title: string
  description: string
  image?: ContentstackAsset
}

// Union type for all blocks
export type ContentBlock = HeroBlock | GridBlock | TextBlock

Next Steps

Live Preview

Enable real-time editing for your modular blocks with Live Preview integration.

Image Optimization

Optimize images in your block components with @nuxt/image integration.

Composables

Learn about the composables that power the auto-fetch functionality.