Skip to content

Instantly share code, notes, and snippets.

@SebbeJohansson
Created September 29, 2022 21:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SebbeJohansson/31b64c5c4f914abfec2660210ec7795a to your computer and use it in GitHub Desktop.
Save SebbeJohansson/31b64c5c4f914abfec2660210ec7795a to your computer and use it in GitHub Desktop.
In-Line Storyblok block rendering for Nuxt3 with SSR and Prerendering
<script setup lang="ts">
import { Richtext } from 'storyblok-js-client';
const props = defineProps({ blok: Object });
const nuxtApp = useNuxtApp();
const textObject = { ...props.blok.text };
const nodes = [];
// Proof of concept for custom handling of inline blok nodes.
Object.entries(textObject.content).forEach(([key, node]) => {
if (node.type === 'blok') {
const blok = {
content: node.attrs?.body?.[0],
};
nodes.push({
key,
type: 'blok',
content: {
blok,
},
});
} else {
nodes.push({
key,
type: 'html',
content: nuxtApp.$formatRichText(useStoryblokApi().richTextResolver.render({
type: 'doc',
content: [
node,
],
} as Richtext)),
});
}
});
</script>
<template>
<div v-editable="blok" class="text">
<div v-for="node in nodes" :key="node.key">
<component
:is="$resolveStoryBlokComponent(node.content.blok)"
v-if="node.type === 'blok'"
:blok="node.content.blok.content"
/>
<div v-else v-html="node.content" />
</div>
</div>
</template>
<style>
.text img {
max-width: 100%;
}
</style>
@kmukku
Copy link

kmukku commented Dec 8, 2022

Thanks!

I had to simplify it little bit to make it work on my case (RichText.vue):

<template>
  <template v-for="node in nodes" :key="node.key">
    <component
      :is="node.content.blok.content.component"
      v-if="node.type === 'blok'"
      :blok="node.content.blok.content"
    />
    <div v-else v-html="node.content" />
  </template>
</template>

<script setup>
const props = defineProps({ blok: Object })
const nodes = []
Object.entries(props.blok.content).forEach(([key, node]) => {
  if (node.type === 'blok') {
    const blok = {
      content: node.attrs?.body?.[0],
    }
    nodes.push({
      key,
      type: 'blok',
      content: {
        blok,
      },
    })
  } else {
    nodes.push({
      key,
      type: 'html',
      content: renderRichText(({
        type: 'doc',
        content: [node],
      }),
    })
  }
})
</script>

Usage: <RichText :blok="blok.richTextPropertyName" />

@SebbeJohansson
Copy link
Author

Sorry yea that might have been a bit confusing. formatRichText is a plugin specific for my solution. It mainly converts the normal image tag into an image tag with lazy loading. You can find that plugin here: https://github.com/SebbeJohansson/sebbejohansson-front/blob/main/plugins/media-handler.ts#L57-L64

Here is the Text.vue component as it is in my current version of that project: https://github.com/SebbeJohansson/sebbejohansson-front/blob/main/storyblok/Text.vue

Glad it could help!

@kmukku
Copy link

kmukku commented Dec 8, 2022

It helped a lot. I got another solution from Storyblok support, I find this one more simple.

@SebbeJohansson
Copy link
Author

Hmm interesting solution you got from them. Yes I agree that this solution is more easy to understand.

@joezimjs
Copy link

Do these solutions work with components embedded deeper? For example I have a Feature blok inside a bullet item:

{
    "type": "doc",
    "content": [
        {
            "type": "bullet_list",
            "content": [
                {
                    "type": "list_item",
                    "content": [
                        {
                            "type": "paragraph",
                            "content": [
                                {
                                    "text": "a bullet item",
                                    "type": "text"
                                }
                            ]
                        },
                        {
                            "type": "blok",
                            "attrs": {
                                "id": "44d8f268-e6ad-403a-8287-bb62f7b3da5e",
                                "body": [
                                    {
                                        "_uid": "i-3858d62b-1db5-4648-95af-94f968b5ec5f",
                                        "name": "Inside the feature",
                                        "component": "Feature",
                                        "_editable": "<!--#storyblok#{\"name\": \"Feature\", \"space\": \"1001749\", \"uid\": \"i-3858d62b-1db5-4648-95af-94f968b5ec5f\", \"id\": \"4874\"}-->"
                                    }
                                ]
                            }
                        }
                    ]
                },
                {
                    "type": "list_item",
                    "content": [
                        {
                            "type": "paragraph",
                            "content": [
                                {
                                    "text": "another bullet",
                                    "type": "text"
                                }
                            ]
                        }
                    ]
                }
            ]
        },
        {
            "type": "horizontal_rule"
        },
        {
            "type": "blok",
            "attrs": {
                "id": "020364a3-b998-49f8-9cd6-5f4008ce4072",
                "body": [
                    {
                        "_uid": "i-d016076c-7869-429e-acf7-22d7337766c8",
                        "name": "Internal Feature Block",
                        "component": "Feature",
                        "_editable": "<!--#storyblok#{\"name\": \"Feature\", \"space\": \"1001749\", \"uid\": \"i-d016076c-7869-429e-acf7-22d7337766c8\", \"id\": \"4874\"}-->"
                    }
                ]
            }
        }
    ]
}

It looks to me like the solutions you two have proposed only look at the first level of content.

@SebbeJohansson
Copy link
Author

@jelmerdemaat I havent tested, but i think renderRichText is going as deep as it can.

@jelmerdemaat
Copy link

@SebbeJohansson I think you mean @joezimjs :)

@SebbeJohansson
Copy link
Author

@SebbeJohansson I think you mean @joezimjs :)

100%! :D God damn autocomplete :P

@joezimjs
Copy link

@SebbeJohansson Yes, renderRichText does render deeply, but if it comes across a blok deep in there, it won't load this Vue component in order to render it as a component.

@SebbeJohansson
Copy link
Author

@joezimjs aaah i understand.
Should just be a case of infinite loop until it find the end no?

@joezimjs
Copy link

@SebbeJohansson No, it means you can't use renderRichText. You have to be able to render every kind of node yourself via a Vue component that is also able to traverse the rich text data structure and display each of its child nodes in the same way.

@joezimjs
Copy link

@marvr/storyblok-rich-text-vue-renderer Does this, but it hasn't been touched in a while and I'm not sure if there's a good way to set it up for Nuxt. I used it with iles but I had to register each component that could be used in it manually like this:

import { defineApp } from 'iles'
import { StoryblokVue, apiPlugin } from '@storyblok/vue';
import { plugin as VueRichTextRenderer, RichTextRenderer, defaultResolvers } from '@marvr/storyblok-rich-text-vue-renderer';
import { h, VNode } from 'vue';

import Grid from "@/components/Grid.vue";
import Page from "@/components/Page.vue";
import Teaser from "@/components/Teaser.vue";
import Feature from "@/components/Feature.vue";

export default defineApp({
    enhanceApp({app}) {
        /*
         * Register components for automatic import for use in the rich text renderer and StoryblokComponent
         * since they can't just use unplugin-vue-components
         */

        // LIST ALL COMPONENTS HERE 👇
        const components: Record<string, any> = {
            Grid, Page, Teaser, Feature, RichTextRenderer
        }

        // List of components for use by rich text renderer. (generated later)
        const componentRenderers: Record<string, (data: any) => VNode> = {}

        // The render function used by the rich text renderer for our components
        function componentRenderer ({id, component, _uid, fields} : {id:string, component: string, _uid: string, fields: Record<string, any>}) {
            return h(components[component], {blok:{id, component, _uid, ...fields}})
        }

        // Iterate through all the component we listed so we can register them
        Object.entries(components).forEach(([name, component]) => {
            // Register component for use inside other components without needing to import
            app.component(name, component)

            // Register component for use by rich text renderer
            componentRenderers[name] = componentRenderer
        })

        app.use(VueRichTextRenderer({
            resolvers: {
                ...defaultResolvers,
                // 👇 list of component renderers we just generated
                components: componentRenderers
            }
        }))
    },
})

I'm honestly REALLY surprised there hasn't been an official library/plugin that does this at this point. It's an extremely common use case and no one has an amazing solution.

@SebbeJohansson
Copy link
Author

@joezimjs Have you tried raising a feature request specifically for this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment