Git Product home page Git Product logo

storyblok-rich-text-astro-renderer's Introduction

Storyblok Rich Text Renderer for Astro

Renders Storyblok rich text content to Astro elements.

GitHub NPM

Demo

If you are in a hurry, check out live demo:

Open in StackBlitz

Motivation

Official Storyblok + Astro integration (@storyblok/astro) provides the most basic possibility to render rich-text in Astro. The integration package re-exports the generic rich text utility from @storyblok/js package, which is framework-agnostic and universal.

This renderer utility outputs HTML markup, which can be used in Astro via the set:html directive:

---
import { renderRichText } from '@storyblok/astro';

const { blok } = Astro.props

const renderedRichText = renderRichText(blok.text)
---

<div set:html={renderedRichText}></div>

Nevertheless, it is possible to customise renderRichText to some extent by passing the options as the second parameter:

import { RichTextSchema, renderRichText } from "@storyblok/astro";
import cloneDeep from "clone-deep";

const mySchema = cloneDeep(RichTextSchema);

const { blok } = Astro.props;

const renderedRichText = renderRichText(blok.text, {
  schema: mySchema,
  resolver: (component, blok) => {
    switch (component) {
      case "my-custom-component":
        return `<div class="my-component-class">${blok.text}</div>`;
        break;
      default:
        return `Component ${component} not found`;
    }
  },
});

Although this works fine and may cover the most basic needs, it may quickly turn out to be limiting and problematic because of the following reasons:

  1. renderRichText utility cannot map rich text elements to actual Astro components, to be able to render embedded Storyblok components inside the rich text field in CMS.
  2. Links that you might want to pass through your app's router, are not possible to be reused as they require the actual function to be mapped with data.
  3. It is hard to maintain the string values, especially when complex needs appear, f.e. setting classes and other HTML properties dynamically. It may be possible to minimize the complexity by using some HTML parsers, like ultrahtml, but it does not eliminate the problem entirely.

Instead of dealing with HTML markup, storyblok-rich-text-astro-renderer outputs RichTextRenderer.astro helper component (and resolveRichTextToNodes resolver utility for the needy ones), which provides options to map any Storyblok rich text element to any custom component, f.e. Astro, SolidJS, Svelte, Vue, etc.

The package converts Storyblok CMS rich text data structure into the nested Astro component nodes structure, with the shape of:

export type ComponentNode = {
    component?: unknown;                 // <-- component function - Astro, SolidJS, Svelte, Vue etc
    props?: Record<string, unknown>;     // <-- properties object
    content?: string | ComponentNode[];  // <-- content, which can either be string or other component node
};

Installation

npm install storyblok-rich-text-astro-renderer

Usage

To get the most basic functionality, add RichText.astro Storyblok component to the project:

---
import RichTextRenderer from "storyblok-rich-text-astro-renderer/RichTextRenderer.astro";
import type { RichTextType } from "storyblok-rich-text-astro-renderer"
import { storyblokEditable } from "@storyblok/astro";

export interface Props {
  blok: {
    text: RichTextType;
  };
}

const { blok } = Astro.props;
const { text } = blok;
---

<RichTextRenderer content={text} {...storyblokEditable(blok)} />

Advanced usage

Sensible default resolvers for marks and nodes are provided out-of-the-box. You only have to provide custom ones if you want to override the default behavior.

Use resolver to enable and control the rendering of embedded components, and schema to control how you want the nodes and marks be rendered:

<RichTextRenderer
  content={text}
  schema={{
    nodes: {
      heading: ({ attrs: { level } }) => ({
        component: Text,
        props: { variant: `h${level}` },
      }),
      paragraph: () => ({
        component: Text,
        props: {
          class: "this-is-paragraph",
        },
      }),
    },
    marks: {
      link: ({ attrs }) => {
        const { custom, ...restAttrs } = attrs;

        return {
          component: Link,
          props: {
            link: { ...custom, ...restAttrs },
            class: "i-am-link",
          },
        };
      },
    }
  }}
  resolver={(blok) => {
    return {
      component: StoryblokComponent,
      props: { blok },
    };
  }}
  {...storyblokEditable(blok)}
/>

Content via prop

By default, content in nodes is handled automatically and passed via slots keeping configuration as follows:

heading: ({ attrs: { level } }) => ({
  component: Text,
  props: { variant: `h${level}` },
}),

This implies that implementation of Text is as simple as:

---
const { variant } = Astro.props;
const Component = variant || "p";
---

<Component>
  <slot />
</Component>

However in some cases, the users do implementation via props only, thus without slots:

---
const { variant, text } = Astro.props;
const Component = variant || "p";
---

<Component>
  {text}
</Component>

This way the content must be handled explictly in the resolver function and passed via prop:

heading: ({ attrs: { level }, content }) => ({
  component: Text,
  props: {
    variant: `h${level}`,
    text: content?.[0].text,
  },
}),

Schema

The schema has nodes and marks to be configurable:

schema={{
  nodes: {
    heading: (node) => ({ ... }),
    paragraph: () => ({ ... }),
    text: () => ({ ... }),
    hard_break: () => ({ ... }),
    bullet_list: () => ({ ... }),
    ordered_list: (node) => ({ ... }),
    list_item: () => ({ ... }),
    horizontal_rule: () => ({ ... }),
    blockquote: () => ({ ... }),
    image: (node) => ({ ... }),
    code_block: (node) => ({ ... }),
    emoji: (node) => ({ ... }),
  },
  marks: {
    link: (mark) => { ... },
    bold: () => ({ ... }),
    underline: () => ({ ... }),
    italic: () => ({ ... }),
    styled: (mark) => { ... },
    strike: () => ({ ... }),
    superscript: () => ({ ... }),
    subscript: () => ({ ... }),
    code: () => ({ ... }),
    anchor: (mark) => ({ ... }),
    textStyle: (mark) => ({ ... }),
    highlight: (mark) => ({ ... }),
  };
}}

NOTE: if any of the latest Storyblok CMS nodes and marks are not supported, please raise an issue or contribute.

Inspiration

Contributing

Please see our contributing guidelines and our code of conduct.

storyblok-rich-text-astro-renderer's People

Contributors

aivluk avatar dependabot[bot] avatar developer-ns avatar edo-san avatar edvinasjurele avatar jimtsikos avatar jsve avatar lukjok avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

storyblok-rich-text-astro-renderer's Issues

Improve attrs types for resolver functions

Context

It seems the ResponseSchemaAttrsFn are too generic, thus the users do not get sufficient attrs type when add resolver functions:

image

TODO

  • Improve types, so users can see exact attrs types instead of any right-now

Option to pass content as prop

It turns out that Astro has a built-in component that provides syntax highlighting for code blocks (<Code />). However, the string that needs to be converted should be passed as a prop, making it incompatible with your current implementation.

The following won't work:

---
import { Code } from "astro:components";
---
<RichTextRenderer
  content={text}
  schema={{
    nodes: {
      ...,
      code_block: ({ attrs }) => ({
        component: Code,
        props: {
          lang: attrs.class?.split("-")[1],
          theme: "solarized-dark",
        },
      }),
      ...
    },
    ...
  }}
  ...
/>

I've implemented a workaround by introducing a "metaprop" called contentPropName to specify the prop name to which the content will be passed. For instance, the following will render correctly because the component requires the content to be passed to a prop called code:

---
import { Code } from "astro:components";
---
<RichTextRenderer
  content={text}
  schema={{
    nodes: {
      ...,
      code_block: ({ attrs }) => ({
        component: Code,
        props: {
          lang: attrs.class?.split("-")[1],
          theme: "solarized-dark",
+         contentPropName: "code",
        },
      }),
      ...
    },
    ...
  }}
  ...
/>

I can create a pull request for these changes, or perhaps you have other suggestions?

Add ordered_list and button_list support

Whenever I create an unordered list or ordered list inside my rich text it does not show up on my site. How can I add it? I've tried doing this:

<RichTextRenderer
  content={text}
  schema={{
    nodes: {
      heading: ({ attrs: { level } }) => ({
        component: Text,
        props: { variant: `h${level}` },
      }),
      list_item: () => ({
        component: UL,
      }),
    },
  }}
  resolver={(blok) => {
    return {
      component: StoryblokComponent,
      props: { blok },
    };
  }}
  {...storyblokEditable(blok)}
/>

And then in UL.astro:

---
const { ...props } = Astro.props;
---

<ul {...props}>
  <slot />
</ul>

Still no luck on getting it to show. When I change it to a heading or paragraph it shows up fine.

reproduction: https://stackblitz.com/edit/github-xvrhmy?file=src%2Fpages%2Findex.astro

TODO

  • Add ordered_list and button_list support

Figure element breaks the paragraphs structure (empty <p></p> artifact)

Storyblok's default rich-text rendering renders image captions as a title attribute, so I used your project to define my own image component using figure and figcaption, like so:

ImageWithCaption.astro

---
interface Props {
  src: string;
  alt?: string;
  title?: string;
}

const { src, alt, title } = Astro.props;
---
<p>
  <figure>
    <img src={src} alt={alt} loading="lazy" />
    { title && <figcaption>{title}</figcaption> }
  </figure>
</p>

and

article.astro

    <RichTextRenderer
      content={body}
      schema={{
        nodes: {
          image: ({ attrs }) => ({
            component: ImageWithCaption,
            props: attrs,
          })
        }
      }}
      {...storyblokEditable(blok)} />

Problem is, when the rich text content is rendered, the node that follows an image node doesn't get rendered, instead the text content is output as-is. This is typically a paragraph, so it can be worked around with CSS by giving it as the same appearance as a p tag:

image

If you add an empty paragraph under the image in Storyblok's rich-text editor, the text is rendered as a paragraph using a p tag โ€“ which is what's expected:

image

Not sure if this is a bug in this project or if my code is at fault.

Add textResolver to preprocess text values

Context

There might be some cases which requires controlling the text values in text nodes. Some of the use cases are:

  • translations (register/translate text value via custom admin solution, f.e. adding ICU message syntax support)
  • text sanitisation, encoding/decoding
  • text replacement (f.e. have some replacable placeholders like {date} so the app depending on business logic would inject the correct)

TODO

  • Add textResolver to support preprocessing of the text values in rich text
  • Add DEMO example
  • Update README about the possibility of such functionality
  • Cover with TS types

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.