Skip to content

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.

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

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.

To add Component subclasses for the new blocks, update the BlockLibrary.kt file in the common source set.

shared/src/commonMain/kotlin/org/example/project/BlockLibrary.kt
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, which can represent any rich text node (a document in this case).
  • FeaturedArticles.articles is a List<Story<Article>>, so the API resolves the referenced stories and returns them. To receive only the UUIDs of the linked stories, change the field type to List<Uuid>.

Next, add composables for these blocks inside the blockProvider in App.kt.

shared/src/commonMain/kotlin/org/example/project/App.kt
…
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 with the article Story instance for immediate display of the selected article.
  • The block<Article> composable invokes RichText() to render the article’s rich text content.

The storyLinkListener parameter of blockProvider 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.

Was this page helpful?

What went wrong?

This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window).Terms of Service (opens in a new window) apply.