---
title: Content Modeling in CMP
description: Learn how to handle custom blocks, render rich text, and use story references to manage content across your Compose Multiplatform project.
url: https://www.storyblok.com/docs/quickstarts/compose-multiplatform/content-modeling
---

# Content Modeling in CMP

Learn how to handle different content type and nestable blocks, render rich text, and use story references to manage content globally in a Compose Multiplatform (CMP) project.

## Setup

In the existing space, create the following blocks:

-   An `article` content type block with the following fields:
    -   `title`: Text
    -   `content`: Rich text
-   A `featured-articles` nestable block with the following field:
    -   `articles`: References

> [!NOTE]
> Learn more about fields in the [fields concept](/docs/concepts/fields).

Next, create an `Articles` folder, open it, and create a few stories that use the `article` content type.

Finally, add the `featured-articles` block to the `body` field of the **Home** story, and select the created articles to feature.

## Create and register blocks

To add [`Component`](https://storyblok.github.io/storyblok-kotlin/content-api-client/com.storyblok.cdn.schema/-component/index.html) subclasses for the new blocks, update the `BlockLibrary.kt` file in the common source set.

shared/src/commonMain/kotlin/org/example/project/BlockLibrary.kt

```kotlin
package org.example.project

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import com.storyblok.cdn.schema.Component
import com.storyblok.cdn.schema.RichText
import com.storyblok.cdn.schema.Story
…
@Serializable
@SerialName("teaser")
data class Teaser(val headline: String): Component()

@Serializable
@SerialName("article")
data class Article(val title: String, val content: RichText) : Component()

@Serializable
@SerialName("featured-articles")
data class FeaturedArticles(val articles: List<Story<Article>>) : Component()
```

The new classes define the fields of the `article` and `featured-articles` blocks:

-   `Article.content` is a [`RichText`](https://storyblok.github.io/storyblok-kotlin/content-api-client/com.storyblok.cdn.schema/-rich-text/index.html), which can represent any rich text node (a [document](https://storyblok.github.io/storyblok-kotlin/content-api-client/com.storyblok.cdn.schema/-rich-text/-document/index.html) in this case).
-   `FeaturedArticles.articles` is a `List<Story<Article>>`, so the API [resolves](https://www.storyblok.com/docs/concepts/references#resolve-relations-in-api-requests) the [referenced](https://www.storyblok.com/docs/concepts/references) stories and returns them. To receive only the UUIDs of the linked stories, change the field type to `List<Uuid>`.

> [!WARNING]
> The optional `resolveLevel` parameter of the [`story()`](https://storyblok.github.io/storyblok-kotlin/content-api-client/com.storyblok.cdn/-storyblok-client/story.html) function controls how many levels of nested story references the API resolves, and defaults to one level. The API also limits the number of stories it resolves in a single request. Learn more in the [Content Delivery API documentation](/docs/api/content-delivery/v2/stories/retrieve-a-single-story) and the [References developer concept](/docs/concepts/references).
> 
> Handle unresolved references in one of two ways:
> 
> -   Make the field nullable (for example: `Story<Article>?`). Unresolved references are then set to `null`.
> -   Change the field type from [`Story<T>`](https://storyblok.github.io/storyblok-kotlin/content-api-client/com.storyblok.cdn.schema/-story/index.html) to [`Uuid`](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.uuid/-uuid/) and resolve the stories in [subsequent requests](https://www.storyblok.com/docs/concepts/references#fetch-references-manually).

Next, add composables for these blocks inside the [`blockProvider`](https://storyblok.github.io/storyblok-kotlin/storyblok-material3/com.storyblok.compose.provider/block-provider.html) in `App.kt`.

shared/src/commonMain/kotlin/org/example/project/App.kt

```kotlin
…
import androidx.navigation3.ui.NavDisplay
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.TextButton

…
  blockProvider = blockProvider(
    fallback = { it, modifier -> Text("Unknown block type '${it.component}'", modifier) }
    fallback = { it, modifier -> Text("Unknown block type '${it.component}'", modifier) },
    storyLinkListener = { uuid, _ -> backStack.add(StoryKey(uuid = uuid)) }
  ) {
    block<Page> { page, modifier ->
      LazyColumn(modifier) {
        items(page.body, key = { it.uid }) { Block(it, Modifier.fillMaxWidth()) }
      }
    }
    block<Feature> { feature, modifier ->
      Text(feature.name, modifier.padding(16.dp))
    }
    block<Teaser> { teaser, modifier ->
      Text(teaser.headline, modifier.padding(16.dp), style = typography.headlineMedium)
    }
    block<Grid> { grid, modifier ->
      FlowRow(modifier) { grid.columns.forEach { Block(it) } }
    }
    block<FeaturedArticles> { featured, modifier ->
      Column(modifier.padding(16.dp)) {
        Text("Featured Articles", style = MaterialTheme.typography.titleLarge)
        Spacer(Modifier.height(8.dp))
        featured.articles.forEach { article ->
          TextButton(onClick = { backStack.add(StoryKey(story = article)) }) {
            Text(article.content.title)
          }
        }
      }
    }
    block<Article> { article, modifier ->
      Column(modifier.padding(16.dp)) {
        Text(article.title, style = MaterialTheme.typography.headlineLarge)
        Spacer(Modifier.height(16.dp))
        RichText(article.content, Modifier.fillMaxWidth())
      }
    }
  }
…
```

The `blockProvider` now registers composables for `FeaturedArticles` and `Article` alongside the existing blocks:

-   The `block<FeaturedArticles>` composable renders links to the featured articles. Selecting a link pushes a [`StoryKey`](/docs/quickstarts/compose-multiplatform/dynamic-routing#create-a-story-navigation-key) with the article `Story` instance for immediate display of the selected article.
-   The `block<Article>` composable invokes [`RichText()`](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose/-block-scope/-rich-text.html) to render the article’s rich text content.

> [!TIP]
> The [`storyblok-material3`](https://storyblok.github.io/storyblok-kotlin/storyblok-material3/index.html) library provides default composables for all rich text [node types](https://storyblok.github.io/storyblok-kotlin/content-api-client/com.storyblok.cdn.schema/-rich-text/index.html) using Material 3 theming and components. You can [override](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose.provider/-block-provider-scope/rich-text.html) the composable for any rich text node or [replace](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose.provider/block-provider-without-rich-text.html) the library entirely with an [alternative](https://storyblok.github.io/storyblok-kotlin/storyblok-compose/com.storyblok.compose.provider/-block-provider-scope/default-rich-text.html) implementation.

The `storyLinkListener` parameter of [`blockProvider`](https://storyblok.github.io/storyblok-kotlin/storyblok-material3/com.storyblok.compose.provider/block-provider.html) handles story links in rich text by pushing a `StoryKey` onto the back stack.

Run the app on any target. The home story lists the featured articles, and selecting one opens the article.

## Related resources

[Concept: Fields](/docs/concepts/fields)

[Concept: References](/docs/concepts/references)

[Material 3 Rich Text Provider](https://storyblok.github.io/storyblok-kotlin/storyblok-material3/index.html)

## Pagination

-   [Previous: Visual Preview in CMP](/docs/quickstarts/compose-multiplatform/visual-preview)
-   [Next: Internationalization in CMP](/docs/quickstarts/compose-multiplatform/internationalization)
