-
Notifications
You must be signed in to change notification settings - Fork 2
extend_created_block
In this tutorial, we will address typical cases in developing Content Blocks.
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
Approximately 40 minutes
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.
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.
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>
),
},
};
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.
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.
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.
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:
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.
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.
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.
- Add a new field to the main component schema:
df({
name: 'subcomponents',
type: 'array',
of: [{ type: badges.name }],
title: 'Additional components',
}),
This will allow us to use badges component on our Content Block. 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.
- Render badges in our component
now we getting new field subcomponents
in our component. let's use renderSanityComponent
helper to render them:
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>
<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 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.
[ Home Page ] [ Github repo ] [ Blog post ] [ Flowbite ]
GitHub Repositories
- https://github.com/focusreactive/cms-kit - CMS-KIT Core
- https://github.com/focusreactive/cms-kit-sanity - CMS-KIT-Sanity starting template
NPM Packages
- @focus-reactive/cms-kit CMS-KIT core package
- @focus-reactive/cms-kit-sanity CMS-KIT-Sanity helper package
Read
https://focusreactive.com/cms-kit-focusreactive/ Blog post
Flowbite
https://flowbite.com/blocks/ - Flowbite blocks library https://flowbite.com/pro/ - Flowbite Pro license