Components
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.
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-cslpattributes - ✅ 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:
<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:
<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
<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
| Prop | Type | Default | Description |
|---|---|---|---|
blocks | ContentstackBlock[] | [] | Array of Contentstack modular blocks |
componentMap | ComponentMapping | {} | Object mapping block types to Vue components |
fallbackComponent | Component | string | ContentstackFallbackBlock | Fallback component for unmapped block types |
Auto-fetch Props (NEW)
| Prop | Type | Default | Description |
|---|---|---|---|
contentTypeUid | string | undefined | Content type UID for fetching entry |
url | string | undefined | URL to fetch entry by |
referenceFieldPath | string[] | [] | Reference field paths to include |
jsonRtePath | string[] | [] | JSON RTE field paths |
locale | string | 'en-us' | Locale for the entry |
replaceHtmlCslp | boolean | false | Replace HTML CSLP tags |
blocksFieldPath | string | 'components' | Field path to extract modular blocks from |
seoMeta | SeoMetaInput | undefined | SEO metadata object (passed directly to useSeoMeta) |
autoSeoMeta | boolean | Record<string, string> | false | Auto-generate SEO from entry data |
Styling Props
| Prop | Type | Default | Description |
|---|---|---|---|
containerClass | string | 'contentstack-modular-blocks' | CSS class for the container |
emptyBlockClass | string | 'visual-builder__empty-block-parent' | CSS class for empty blocks (Visual Builder) |
containerProps | Record<string, any> | {} | Additional props to bind to the container |
showEmptyState | boolean | true | Show empty state when no blocks |
emptyStateClass | string | 'contentstack-empty-state' | CSS class for empty state |
emptyStateMessage | string | 'No content blocks available' | Message to show in empty state |
Advanced Props
| Prop | Type | Default | Description |
|---|---|---|---|
keyField | string | '_metadata.uid' | Custom key field for blocks |
autoExtractBlockName | boolean | true | Auto-extract block name from object keys |
blockNamePrefix | string | '' | 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:
<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:
<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:
<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 Tag | Entry Fields (fallback order) |
|---|---|
title | seo_title → title → name |
description | seo_description → description → summary |
ogTitle | seo_title → title → name |
ogDescription | seo_description → description → summary |
ogImage | featured_image.url → og_image.url → image.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:
| Method | Description |
|---|---|
refreshEntry | Re-fetch the entry data (only works in auto-fetch mode) |
<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:
| Slot | Description | Available When |
|---|---|---|
loading | Custom loading state content | Auto-fetch is enabled and data is loading |
error | Custom error state content | Auto-fetch fails or encounters an error |
empty | Custom empty state content | No 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
#emptyslot) - Invalid data: Gracefully handles malformed block data
- Missing keys: Uses index-based keys as fallback
- Auto-fetch errors: Shows error state (customizable via
#errorslot) - Loading states: Shows loading state during auto-fetch (customizable via
#loadingslot)
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, orblockTypefields) - 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
<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
<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:
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,
}
<script setup>
// componentMap is auto-imported from utils/index.ts
</script>
<template>
<ContentstackModularBlocks
content-type-uid="page"
:url="$route.path"
:component-map="componentMap"
/>
</template>
/utils files. Simply export your componentMap and use it directly - no imports needed!Alternative: Using a Composable
If you prefer a composable pattern:
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,
}
}
<script setup>
const componentMapping = useComponentMapping()
</script>
<template>
<ContentstackModularBlocks
content-type-uid="page"
:url="$route.path"
:component-map="componentMapping"
/>
</template>
TypeScript Integration
// 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
Composables
Learn how to use Nuxt Contentstack composables to fetch entries, assets, and content with type safety, caching, and live preview support.
Image Optimization
Optimize images with Contentstack's Image Delivery API and @nuxt/image integration for responsive, fast-loading images with advanced transformations.