Skip to content

add_cblock

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

Adding a New Content Block to the Components Set

This tutorial is designed for developers looking to extend their project with new components. It serves as a starting point for learning how to create and develop a Content Block. For a more in-depth look at the development process with CMS-KIT, refer to the follow-up tutorials.

Expected Outcome

By the end of this tutorial, you will have created a new Content Block and made it available for Content Creators.

Estimated Time to Complete

Approximately 40 minutes

Good to Know

Content Block: A Content Block is a modular unit of your project with the following key attributes: UI, React Component, CMS Schema, and Content Example. Content Blocks are available in the CMS for Content Creators, allowing them to place these blocks on a page and edit the content of each specific block entry. In the project code, Content Blocks are represented by React components, with CMS content passed via props. Typically, Content Blocks have a full-width rectangular UI on a page, with logically tied content that is visually separated from other blocks. They function like "lego" modules used to build an entire landing page. For more details, see the Content Blocks documentation.

Components Set: A Components Set is a collection of Content Blocks and related items such as Sub Blocks, Components, Page templates, queries, schemas, and more. Typically, a Components Set comprises visually and technically compatible parts. For more information, see the Components Set documentation.

Prerequisites

Before you begin, ensure that your system is set up and your local development environment is configured. If you haven't done this yet, refer to the "Setting Up the System with CMS-KIT" tutorial.

You must have at least one Components Set installed in your project. For this tutorial, we will assume you have the tw-base Set installed, as all tasks will be based on it. If you started your project from the CMS-KIT-Sanity template, you should already have that Components Set in your src/sets/tw-base folder.

Steps to Follow

1. Set Up Dependencies for the Component Set

A Components Set may have its own dependencies that need to be installed separately from the project dependencies.

Note: You need to complete this step only once, and only if you haven't done it before.

  1. Navigate to your Set folder:
    cd src/sets/tw-base
  2. Install the dependencies:
    pnpm i

2. Add New Dummy Content Block from a Template

Let's start a new Content Block with the help of a generator. Your package.json in the Set root contains a script for that. Launch it with:

pnpm run add:cblock

It will ask you to input the name for your content block and the folder where it will be located. Give it a solid, reasonable name in PascalCase. I will proudly name it "NewBlock" to use in this tutorial.

Add Content Block CLI (Image 1)

Add Content Block CLI

After the script is finished, start the NextJS Dev server from the project root:

pnpm run dev

Open the landing pages list in your Sanity Studio locally at http://localhost:3000/admin/structure/landing. Create a new page where we will work with our new content block. Set the title to "Testing page" and the slug to "test".

Find the content field on the testing page and press the "+ Add Template..." button. A panel with all available content blocks and their templates will appear on the right. Scroll to find the newly created block.

New Block in Template List (Image 2)

New Block in Template List

Troubleshooting:

Due to NextJS caching specifics and how initial data is collected in Sanity Studio, you might not see the added Content Block in the templates list immediately. Don't panic and follow these steps:

  1. Restart the dev server pnpm run dev
  2. Open Sanity Studio from its main page http://localhost:3000/admin/structure
  3. Open the document and the templates panel If it doesn't help, run pnpm run build && pnpm start and repeat steps 2-3.

Add this block to your page by pressing the "Append to content fields" button. The block will appear in the list in your page editor with demo content.

New Block in Page Editor (Image 3)

New Block in Page Editor

Let's dive into the details about what was added to our project by the generator and how we can work with it.

Code Changes (Image 4)

Code Changes

2.1 React Component

A new folder "NewBlock" was added to the ContentBlocks folder. It contains everything related to the new block. Open src/sets/tw-base/ContentBlocks/NewBlock/Component.tsx in your IDE. You will find the NewBlock React component inside. This is where you define how your Content Block will be rendered on pages and in the "Adding Template" panel in Sanity Studio. Feel free to use your favorite React development practices and organize the code as you see fit.

2.2 Adapter Functions

Note that we wrap our NewBlock component with the withCMS HOC and pass the sa function into it. Open the sa-adapters.ts file and see the code of that function. It has the AdapterFn type. The purpose of adapter functions is to convert data fetched from CMS into React component props.

Why can't we pass data to components directly? Actually, we can, but this approach has some advantages:

  1. As components from a Components Set are intended to be compatible with different CMSs, we have to adapt them to the content shapes of these CMSs. Despite utilizing the most similar content models for each supported CMS, we can't avoid some tech details leading to slightly different data formats. To overcome this difference, we split our code into a single reusable React component and adapter functions that depend on the CMS.
  2. We provide some reusable helper components to handle common cases like Images, Links, Formatted text, and so on. To keep components compatible, we convert incoming data into a compatible format when needed.
  3. You might want to do some preparation work before passing data into component props. For example, the adapter function is a good place for cleaning steganography markers with @vercel/stega.

Note that point 1 is only for reusable Components Sets. If you're working on a project with a specific CMS and don't plan to share your components, feel free to keep the Adapter Function minimal and organize the rest of the code wherever it's most convenient for you.

2.3 Content Block Schema

For each Content Block, we have to define its schema—the set of fields of a certain type included in the project's content model. When Content Editors work with this block in the CMS and edit its content, they see it in an editor that shows fields specified in the Content Block schema. Open sa-schema.ts and find newBlock—this is the definition of the block's schema. Note that we use some helpers for creating it:

  • defineBlockType: Defines the schema for the content block level.
  • imageWithMetadata.name: Use this when you need a field for an image. It has some advantages over the standard image field type.
  • customRichText.name: Formatted text helper field type.
  • blockOptions.name: Common Content Block options you might want to have in your block's schema.

We export the schema definition as follows:

export default [newBlock];

This is enough to register this new schema in the project's model. Sometimes you will need to specify additional schemas for your component. Export them as well inside the array:

export default [newBlock, newBlockCard];

2.4 Templates

Templates are predefined content examples for your Content Block. You must have at least one template that will appear in the "Adding Template" panel in the CMS, so Content Creators can select it and add it to a page. Note that a template rendered by a component is the only way Content Creators can recognize and deal with your Content Block. This way, you provide a visual representation of the real appearance, which works better than hundreds of words. We utilize the "Show, don't tell" approach here.

Open the sa-templates.ts file. You will find two important things here: the template definition object of the SanityTemplate type, where you give a name, title, and category to a template, and the newBlockDefaultJSON JSON file that contains the actual data of the initial content. We export templates in a similar way to schemas.

You are encouraged to create multiple templates for each Content Block. Have your Content Block with different theme appearances, layouts, or subcomponents? Create a template for each edge case and show all possible use cases you're providing. Finally, Content Creators can only see the titles and appearances of different blocks and have no idea about the name of the React component rendering it—this is not

within their responsibility scope.

We keep JSON files with templates in a separate templates folder, but you can follow your preferred approach in organizing your code.

Before continuing, it's worth noting that now is a good time to git commit your current progress.

3. Content Block Development

Let's go through the major steps of creating a new content block and get familiar with the main aspects and approaches we consider. I'd prefer to create something simple and bright, so let's pick one of the awesome Flowbite components and combine it into blocks. The Flowbite blocks use a similar concept to our Content Blocks, which makes them fit well for our pages.

Take a look at this block: Flowbite Marketing Portfolio. It has everything we need for our exercise.

Flowbite Marketing Portfolio Block (Image 5)

Flowbite Marketing Portfolio Block

3.1 Add React Components

You can grab the HTML code of this component and use it in our project. But we need to convert it into React, separate reusable parts, and turn content copies into props. As experienced React developers, we can skip that part and jump to the moment where we have already prepared code (of course, with ChatGPT's help, never try to do it manually).

Converted Block Code

Resulting React Code:

// Component.tsx

import React from 'react';

const defaultCards: SimpleCardProps[] = [
  {
    company: 'Alphabet Inc.',
    title: 'Official website',
    description:
      'Flowbite helps you connect with friends, family and communities of people who share your interests.',
    link: '#',
  },
  {
    company: 'Microsoft Corp.',
    title: 'Management system',
    description:
      'Flowbite helps you connect with friends, family and communities of people who share your interests.',
    link: '#',
  },
  {
    company: 'Adobe Inc.',
    title: 'Logo design',
    description:
      'Flowbite helps you connect with friends, family and communities of people who share your interests.',
    link: '#',
  },
];

const defaultProps: PortfolioBlockProps = {
  _type: 'tw.newBlock',
  title: 'Our work',
  description:
    'Crafted with skill and care to help our clients grow their business!',
  cards: defaultCards,
};

const ArrowIcon: React.FC = () => {
  return (
    <svg
      aria-hidden="true"
      className="w-5 h-5 ml-2 -mr-1"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 20 20"
      fill="currentColor"
    >
      <path
        fillRule="evenodd"
        d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
        clipRule="evenodd"
      />
    </svg>
  );
};

type SimpleCardProps = {
  company: string;
  title: string;
  description: string;
  link: string;
};

const SimpleCard: React.FC<SimpleCardProps> = ({
  company,
  title,
  description,
  link,
}) => {
  return (
    <div className="space-y-4">
      <span className="bg-gray-100 text-gray-900 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-gray-300">
        {company}
      </span>
      <h3 className="text-2xl font-bold leading-tight text-gray-900 dark:text-white">
        {title}
      </h3>
      <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>
  );
};

type PortfolioBlockProps = ContentBlockGeneric & {
  title: string;
  description: string;
  cards: SimpleCardProps[];
};

const NewBlock: React.FC<PortfolioBlockProps> = () => {
  const { title, description, cards } = defaultProps;
  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>
          <p className="mt-4 text-base font-normal text-gray-500 sm:text-xl dark:text-gray-400">
            {description}
          </p>
        </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, index) => (
            <SimpleCard key={index} {...card} />
          ))}
        </div>
      </div>
    </section>
  );
};

export default withCMS({ sa })(NewBlock);

After inserting this code into our Component.tsx file, we can open the /test page in the browser to see the result. Don't forget to update your Tailwind config with values that can be found on a tab next to the "HTML" one in the component section on Flowbite page. Put it into your tailwind.config.ts in the project root.

React Component Rendered (Image 6)

React Component Rendered

While this looks great, all content is still hardcoded in the component's file, so we can't edit it in the CMS. Let's fix this in the next step.

3.2 Define the Schema

Now we need to describe our Content Block props in terms of content fields. Let's split it the same way our components are designed: newBlock schema for the main component and simpleCard for the card component. See the schema definition below:

Content Block Schema
// sa-schema.ts

import {
  BlockPreview,
  customRichText,
} from '@focus-reactive/cms-kit-sanity/sanity';
import { defineBlockType, defineComponentType } from '../../sa-config';

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: 'description',
      type: 'text',
      title: 'Description',
    }),
    df({
      name: 'link',
      type: 'url',
      title: 'Link',
    }),
  ],
}));

export const newBlock = defineBlockType(({ df }) => ({
  name: 'newBlock',
  type: 'object',
  title: 'New Block',
  fields: [
    df({
      name: 'title',
      type: 'string',
      title: 'Title',
    }),
    df({
      name: 'description',
      type: 'text',
      title: 'Description',
    }),
    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,
      };
    },
  },
}));

export default [newBlock, simpleCard];

Open the page where we have added this Content Block in Sanity Studio and open that block for editing. You will see errors and warnings about the current content. This is fine—Sanity is notifying us that the existing object in the dataset doesn't match the new schema. We are here to fix this. Remove all unnecessary values using the provided buttons or manually. Now let's fill the block with the content we need. You can grab values just from the defaultProps object in your component code. Don't forget to create three cards and fill in their values.

Filling Content in Sanity Studio (Image 7)

Filling Content in Sanity Studio

Now we have content in the CMS, but our component still shows hardcoded values. Let's change this.

3.3 Pass Content into Content Block Through Adapter Function

Let's switch to data coming from the CMS. We can use props passing to the component.

Component Changes for CMS Data (Image 8)

Component Changes for CMS Data

Update the component as shown above. Note that we don't need to touch the adapter function as it's already do what we need.

Now we can spot an issue in our Content Block. We have an array of cards, and now we use array indexes for keys. Our perfectionism demands that we immediately switch to best practices which encourage to use ID or keys values from the CMS for this. Technically we could solve it like this:

{
  cards.map((card) => <SimpleCard key={card._key} {...card} />);
}

But we can use a convenient helper function getCmsKey(contentObject). It remembers how to extract keys from any CMS.

{
  cards.map((card) => <SimpleCardCMS key={getCmsKey(card)} {...card} />);
}

3.4 Update Template

Now let's update the template with our new content so it will look exactly the same on the panel. Open this content block in Sanity Studio. Open the "Current Value" dialog window.

Grabbing Current Content Value (Image 9)

Grabbing Current Content Value

Copy the JSON with the current block content and paste it into templates/sa-mock-default.json. As you save it, you will see the template in the panel in the CMS, and it's available for adding to a page with that saved content.

In sa-templates.ts, we can adjust the title and other details of the template. Let's change some minor details to make it look even clearer for Content Creators:

const newBlockDefault: SanityTemplate = {
  name: 'New Portfolio Block',
  type: newBlock.name,
  namespace: namespace.name,
  title: 'Portfolio',
  description: 'Company portfolio with cards',
  category: TemplateCategory.pageBlock,
  area: TemplateArea.marketing,
  template: newBlockDefaultJSON,
  height: 600 + 4, // 4 is iframe border
};

4. Test the Created Block

Build the app:

pnpm run build && pnpm start

Open the Sanity Studio and add the new version of the component to the page. You should now see the updated component with dynamic content from the CMS.

Summary

With these steps completed, you should have a fully functional Content Block integrated into your CMS-KIT project. Happy coding!