Skip to content

extend_created_block

Oleg Proskurin edited this page Jun 19, 2024 · 10 revisions

Extend Created Block

In this tutorial, we will address typical cases in developing Content Blocks.

Expected Outcome

We will learn about Content Blocks development and add some improvements to our component:

  • Formatted text
  • Images
  • Resolve referred values
  • Clean content from steganography additions
  • Extending Content Blocks with Sub Blocks

Estimated Time to Complete

Approximately 50 minutes

Prerequisites

We will start from where we finished the previous Adding a New Content Block tutorial. Ensure you have a "NewBlock" folder with the created Content Block.

Steps to Follow

1. Switch Description to Formatted Text

Let's imagine one day Content Creators ask us to add bold text to the description and insert a couple of links. We realize that we now want to use formatted text for the description.

Formatted Description

We can achieve this in a few simple steps.

Change the field type for description to a special helper type:

df({
  name: 'description',
  type: customRichText.name,
  title: 'Description',
}),

This will apply the formatted text type to the field. Note that in Sanity Studio, you will see an error about the field format. Just reset the value and input some text into the new field editor. You can use this text for testing:

Crafted with skill and care to help our clients grow their business! Our team is dedicated to delivering exceptional results tailored to meet the unique needs of each client. From bespoke website designs to innovative management systems, we ensure every project is handled with the utmost professionalism and attention to detail.

Explore our case studies to see how we’ve transformed businesses with our cutting-edge solutions. Discover why leading companies like Sanity and Shopify trust us to elevate their digital presence.

Next, we need to render this formatted text in our component. We will use the GenericRichText helper component:

const NewBlock: React.FC<PortfolioBlockProps> = ({
  title,
  description,
  cards,
}) => {
  return (
    <section className="bg-white dark:bg-gray-900 antialiased">
      <div className="max-w-screen-xl px-4 py-8 mx-auto lg:px-6 sm:py-16 lg:py-24">
        <div className="max-w-2xl mx-auto text-center">
          <h2 className="text-3xl font-extrabold leading-tight tracking-tight text-gray-900 sm:text-4xl dark:text-white">
            {title}
          </h2>
          {/* Render formatted text with Helper component */}
          <GenericRichText
            value={description}
            components={richTextComponents}
          />
        </div>
        <div className="grid grid-cols-1 mt-12 text-center sm:mt-16 gap-x-20 gap-y-12 sm:grid-cols-2 lg:grid-cols-3">
          {cards.map((card) => (
            <SimpleCardCMS key={getCmsKey(card)} {...card} />
          ))}
        </div>
      </div>
    </section>
  );
};

To render all elements properly, we're passing a richTextComponents with a map of elements rendering functions. Update the classname prop for the <p> element:

const richTextComponents = {
  block: {
    h2: ({ children }: { children: React.ReactElement }) => (
      <h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
        {children}
      </h2>
    ),
    normal: ({ children }: { children: React.ReactElement }) => (
      <p className="mt-4 text-base font-normal text-gray-500 sm:text-xl dark:text-gray-400">
        {children}
      </p>
    ),
  },
};

If you now update the /test page, you will see the formatted text with two paragraphs. However, our links are not visible, so we should add a separate style for them. Let's add additional styles for link markup:

const richTextComponents = {
  block: {
    h2: ({ children }: { children: React.ReactElement }) => (
      <h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
        {children}
      </h2>
    ),
    normal: ({ children }: { children: React.ReactElement }) => (
      <p className="mt-4 text-base font-normal text-gray-500 sm:text-xl dark:text-gray-400">
        {children}
      </p>
    ),
  },
  marks: {
    link: ({
      value,
      children,
    }: {
      children: React.ReactElement;
      value: { href: string };
    }) => (
      <a
        href={value?.href}
        className="text-blue-500 sm:text-xl dark:text-blue-400"
      >
        {children}
      </a>
    ),
  },
};

1.1 Extending Formatting

You might want to specify styles for other elements. For example, add bullet points and numbered lists. Follow the React Portable Text docs for a full list of customizations.

2. Add Image

Another challenge could arise when we are asked to add logo images to portfolio cards. Let's start with schema updates using the imageWithMetadata helper.

export const simpleCard = defineComponentType(({ df }) => ({
  name: 'simpleCard',
  type: 'object',
  title: 'Simple Card',
  fields: [
    df({
      name: 'company',
      type: 'string',
      title: 'Company',
    }),
    df({
      name: 'title',
      type: 'string',
      title: 'Title',
    }),
    df({
      name: 'logo',
      type: imageWithMetadata.name,
      title: 'Company Logo',
    }),
    df({
      name: 'description',
      type: 'text',
      title: 'Description',
    }),
    df({
      name: 'link',
      type: 'url',
      title: 'Link',
    }),
  ],
}));

Now we can add images into card objects. Note that you can specify images' alt and other attributes.

Edit Image

To render the image, we use the helper component SmartImage:

const SimpleCard: React.FC<SimpleCardProps> = ({
  company,
  title,
  logo,
  description,
  link,
}) => {
  return (
    <div className="space-y-4">
      <span
        className={`text-white text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded`}
      >
        {company}
      </span>
      <h3 className="text-2xl font-bold leading-tight text-gray-900 dark:text-white">
        {title}
      </h3>
      <SmartImage imageWithMetadata={logo} className="m-auto w-16" />
      <p className="text-lg font-normal text-gray-500 dark:text-gray-400">
        {description}
      </p>
      <a
        href={link}
        title=""
        className="text-white bg-primary-700 justify-center hover:bg-primary-800 inline-flex items-center focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
        role="button"
      >
        View case study
        <ArrowIcon />
      </a>
    </div>
  );
};

Switch to the Sanity studio and add logo images of your choice. When finished, switch to the page in the browser and check it. We still don't see images. If we check the props in React Dev Tools, we will see that we're getting logo props, but it contains a reference to our image objects. We need to get image information from those objects. That's why we need to resolve references when fetching information from CMS.

Check Dev Tools

I highlighted the things you need to pay attention to.

Open src/sets/tw-base/pages/landing/sa/landingPageQuery.ts and find the query we use for fetching data. See how you use conditional statements to specify a query for certain Content Block Types. Insert this query to resolve image assets:

_type == 'tw-base.newBlock' => {
  ...,
  cards[]{
    ...,
    logo{
      ...,
      'imageAsset': asset->{
          'src': url,
          'width': metadata.dimensions.width,
          'height': metadata.dimensions.height,
          'alt': altText,
        }
    }
  }
},

Note how we use asset-> syntax to resolve referred objects and specify the fields we need. Now logo props contain the object with imageAsset field required for <SmartImage>:

{
  "changed": true,
  "_type": "glob.imageWithMetadata",
  "asset": {
    "_ref": "image-9c864badcd97622d5a9135e3adf80c47756003e2-364x364-png",
    "_type": "reference"
  },
  "imageAsset": {
    "src": "https://cdn.sanity

.io/images/2920fhf7/production/9c864badcd97622d5a9135e3adf80c47756003e2-364x364.png",
    "width": 364,
    "height": 364,
    "alt": "Logo"
  }
}

At the same time, we still have an asset with _ref containing the image asset ID in props.

Finally, your component should look like this on a page:

Block with Images

3. Passing Cleared Value

Let's say we want to specify colors for link badges on cards. In a real project, the recommended way would be to provide a dropdown with limited, approved-by-design named colors matching Tailwind settings. But for this tutorial, adding a simple string value will be enough. Let's use it to keep any CSS-compatible color value and insert it via the style attribute.

Add a badgeColor field to the simpleCard schema in sa-schema.ts:

df({
  name: 'badgeColor',
  type: 'string',
  title: 'Badge Color',
}),

Add the style attribute to the <a> tag in <SimpleCard> in Component.tsx:

<a
  style={badgeColor ? { backgroundColor: badgeColor } : undefined}
  href={link}
  title=""
  className="text-white bg-primary-700 justify-center hover:bg-primary-800 inline-flex items-center focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
  role="button"
>
  View case study
  <ArrowIcon />
</a>

Then open Sanity Studio and add some values to the new fields and update the /test page. You will notice that nothing has changed. But let's inspect our components with Chrome dev tools.

Inspect Style

We're actually passing the value into the style attribute. But what's the strange addition added to our original value? Nothing unexpected—these values are added to any content field by Sanity when the "Visual editing" feature is enabled. This way, when you click on an element inside the Presentation tool, Sanity Studio knows which field should be opened in the editor form.

That's fine when such fields are passed as content—the browser just ignores them. But in some cases, like this one, it can break our logic.

Let's fix this. Open sa-adapters.ts and edit saSimpleCard this way:

import { vercelStegaSplit } from '@vercel/stega';

// ...

export const saSimpleCard: AdapterFn = (cmsProps) => {
  return {
    key: getCmsKey(cmsProps),
    ...cmsProps,
    badgeColor:
      cmsProps.badgeColor && vercelStegaSplit(cmsProps.badgeColor).cleaned,
  };
};

Here we use vercelStegaSplit, which is a convenient tool for working with such steganography values. As we cleaned the passing value, our colors start to apply to badges as expected.

Colored Badges

4. Extending with Sub Blocks

Let's imagine we were asked to add more elements to our description area and make more expressive. We have some elements in our Sub Blocks folder and we have chosen to use a Badge Sub Block for that.

alt text

Sub Blocks are good reusable modules as they packed with everything we need to include them into our Component Block:

  • React Component
  • Sanity Schema
  • Content Templates

All this parts already registered in our project, so we can simply start extending our Content Block with them.

  1. Add a new field to the main component schema:
// sa-schema.ts
import { badges } from '../../SubBlocks/Badges/sa-schema';

// ...

export const newBlock = defineBlockType(({ df }) => ({

// newBlock fields...
df({
   name: 'subComponents',
   type: 'array',
   of: [{ type: badges.name }],
   title: 'Additional components',
 }),

This adds an array field where we can put sub blocks, only badges type for now. But we can extend it later. This will allow us to use badges component on our Content Block. Until now it's just a regular Sanity array field, but we want to select badges visually from precreated templates. Enable "templates selector" button by adding TemplateSelector component on this field

df({
   name: 'subComponents',
   type: 'array',
   of: [{ type: badges.name }],
   title: 'Additional components',
   components: {
     field: TemplateSelector,
   },
 }),

now you can see templates selector button and use it to browse through available badges templates.

alt text

Add an existing badge from a panel to your 'Additional components' field.

  1. Render badges in our component

now we the field subComponents value is passing to our component as a prop. How can we render it? Let's use renderSanityComponent for that. It can render any registered Content Block or Sub Block by their type and field values. Insert it into the Components code where you want to render your additional list of components.

import { renderSanityComponent } from '@focus-reactive/cms-kit-sanity/sanity-next';

// ...

const NewBlock: React.FC<PortfolioBlockProps> = ({
  title,
  description,
  cards,
  subComponents,
}) => {
  return (
    <section className="bg-white dark:bg-gray-900 antialiased">
      <div className="max-w-screen-xl px-4 py-8 mx-auto lg:px-6 sm:py-16 lg:py-24">
        <div className="max-w-2xl mx-auto text-center">
          <h2 className="text-3xl font-extrabold leading-tight tracking-tight text-gray-900 sm:text-4xl dark:text-white">
            {title}
          </h2>
          <GenericRichText
            value={description}
            components={richTextComponents}
          />
        </div>
        // Use it here to render all blocks coming inside `subComponents` array
        <div>{subComponents.map(renderSanityComponent({}))}</div>
        <div className="grid grid-cols-1 mt-12 text-center sm:mt-16 gap-x-20 gap-y-12 sm:grid-cols-2 lg:grid-cols-3">
          {cards.map((card) => (
            <SimpleCardCMS key={getCmsKey(card)} {...card} />
          ))}
        </div>
      </div>
    </section>
  );
};

This way we just added one Sub Block to our Content Block. Sub Blocks are convenient way of sharing reusable functionality through the project. We can use existing Sub Blocks or create new ones. We might want to add more Sub Block types to be available to insert into our Content Block. For that we just need to specify their types in our schema field:

export const newBlock = defineBlockType(({ df }) => ({
  name: 'newBlock',
  type: 'object',
  title: 'New Block',
  fields: [
    df({
      name: 'title',
      type: 'string',
      title: 'Title',
    }),
    df({
      name: 'description',
      type: customRichText.name,
      title: 'Description',
    }),
    df({
      name: 'subComponents',
      type: 'array',
      of: [
        // List here types of Sub Blocks you'd like to use
        { type: badges.name },
        { type: buttons.name },
        { type: logoCloudGrid.name },
      ],
      title: 'Additional components',
      components: {
        field: TemplateSelector,
      },
    }),
    df({
      name: 'cards',
      type: 'array',
      of: [{ type: simpleCard.name }],
      title: 'Cards',
    }),
  ],
  components: { preview: BlockPreview },
  preview: {
    select: {
      customTitle: 'customTitle',
      components: 'components',
      blockOptions: 'blockOptions',
    },
    // @ts-ignore
    prepare({ components, blockOptions, customTitle }) {
      return {
        title: customTitle || 'Page block',
        customTitle,
        components,
        blockOptions,
      };
    },
  },
}));

After that they will be available for selecting via Templates Selector panel

alt text

As you insert templates of these blocks into your component, they will be rendered automatically by renderSanityComponent. You don't need to add any changes to your React component.

alt text

Sub Blocks is a powerful means of creating reusable code chunks in your project. They enable visual components selection for Content Creators, clear development flow for project developers and scalable project architercture. See the Sub Blocks cheatsheet to quickly find required funtionality.

Summary

In this tutorial, we have extended our Content Block to support formatted text, added image support, resolved referred values, and cleaned content from steganography additions. This setup will allow for a more flexible and dynamic content management experience.

Tutorials

Documentation

Related resources

GitHub Repositories

NPM Packages

Read

https://focusreactive.com/cms-kit-focusreactive/ Blog post

Flowbite

https://flowbite.com/blocks/ - Flowbite blocks library https://flowbite.com/pro/ - Flowbite Pro license

Clone this wiki locally