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

By the end of this tutorial, you will have enhanced your Content Block with advanced features, including:

  • Formatted Text: Learn how to enable and render rich text formatting within your content.
  • Image Integration: Add and display images in your content blocks using Sanity and React.
  • Resolved References: Fetch and render referenced data from Sanity, ensuring all linked content is correctly displayed.
  • Clean Content: Remove unwanted steganographic data added during visual editing, ensuring clean and functional content.
  • Sub Blocks: Integrate reusable Sub Blocks into your Content Block, allowing for modular and scalable content creation.

This tutorial will equip you with the skills to create more dynamic, visually appealing, and easily manageable content 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 (Image 1)

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 (Image 2)

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 (Image 3)

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 (Image 4)

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 (Image 5)

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 (Image 6)

Colored Badges

4. Extending with Sub Blocks

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

Badge Example (Image 7)

Badge Example

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

  • React Component
  • Sanity Schema
  • Content Templates

All these parts are 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 the badges component on our Content Block. Until now, it's just a regular Sanity array field, but we want to select badges visually from pre-created templates. Enable the "templates selector" button by adding the TemplateSelector component on this field:

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

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

Enable Template Selector (Image 8)

Enable Template Selector

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

  1. Render badges in our component

Now the 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 a convenient way of sharing reusable functionality throughout 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 the Templates Selector panel.

Sub Blocks in List (Image 9)

Sub Blocks in List

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.

Page with Sub Blocks (Image 10)

Page with Sub Blocks

Sub Blocks are 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 architecture. See the Sub Blocks cheatsheet to quickly find the required functionality.

Summary

In this tutorial, we have significantly enhanced our Content Block to provide a more versatile and dynamic content management experience. Here's a detailed recap of what we accomplished:

  1. Formatted Text Support:

    • We transformed the description field into a rich text editor, enabling content creators to use formatted text. This includes bold text, italics, and links, offering a more flexible way to present information.
  2. Image Integration:

    • We updated the schema to include images in our portfolio cards. By utilizing the SmartImage component, we ensured that images are rendered correctly, enhancing the visual appeal of our content blocks.
  3. Resolving Referred Values:

    • We addressed the challenge of fetching and rendering referred values from Sanity. By updating our GROQ queries, we ensured that image assets and other referenced data are correctly resolved and displayed in our components.
  4. Cleaning Content from Steganography Additions:

    • We tackled the issue of unwanted steganographic data added by Sanity's visual editing feature. Using the vercelStegaSplit utility, we cleaned these values to ensure our content remains untainted and functions as expected.
  5. Extending with Sub Blocks:

    • We introduced the concept of Sub Blocks, a powerful feature that allows for the creation of reusable modules within our Content Blocks. Sub Blocks come with their own React components, Sanity schemas, and content templates, making them highly versatile.
    • We added a subComponents field to our Content Block schema, enabling us to insert Sub Blocks like badges. This was facilitated by the TemplateSelector, allowing for easy visual selection and insertion of templates.
    • By using the renderSanityComponent function, we dynamically rendered these Sub Blocks within our main component, demonstrating how additional elements can be seamlessly integrated and managed.

Sub Blocks are a game-changer for content management, offering several advantages:

  • Reusability: Sub Blocks encapsulate functionality that can be reused across different Content Blocks, promoting code efficiency and consistency.
  • Visual Editing: Content creators benefit from an intuitive visual interface, making it easy to select and insert Sub Blocks without needing to understand the underlying code.
  • Scalability: As your project grows, you can easily add new Sub Blocks to extend functionality, ensuring your content management system can adapt to evolving requirements.
  • Customization: Developers can create new Sub Blocks tailored to specific needs, providing a flexible and customizable solution for complex content structures.

With these enhancements, CMS-KIT now offers a robust and flexible platform for managing content. Whether you're a developer looking to streamline your workflow or a content creator seeking intuitive editing tools, the improvements we've made ensure that CMS-KIT can meet your needs efficiently. You're now equipped to build and manage sophisticated, content-rich websites with ease.