-
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.
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.
Approximately 50 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.
Formatted Description (Image 1)
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.
Edit Image (Image 2)
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)
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)
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)
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)
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)
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.
- 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)
Add an existing badge from a panel to your 'Additional components' field.
- 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)
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)
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.
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:
-
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.
-
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.
- We updated the schema to include images in our portfolio cards. By utilizing the
-
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.
-
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.
- We tackled the issue of unwanted steganographic data added by Sanity's visual editing feature. Using the
-
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 theTemplateSelector
, 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.
[ 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