Building a Sitefinity Next.js Hero Widget

Date Published  

Categories  Sitefinity

Have you ever landed on a website and been immediately drawn in by that prominent, eye-catching section at the top? That's often a Hero widget doing its job - blending compelling visuals with key messages to instantly capture visitor attention and set the tone. While Sitefinity is a powerful and flexible platform, especially with its Next.js renderer, it gives you the exciting opportunity to craft custom components like a Hero widget, perfectly tailored to your project's unique needs. In this article we are going to do just that! We'll build a custom Hero widget from scratch using the Sitefinity Next.js SDK and React. So, let's roll up our sleeves and explore how to build one!

Hero widget on a page with the styles applied

Hero widget designer view with all the fields created

Building the entity: hero.entity.ts

Our first step is to define the structure of our Hero widget. This means specifying what pieces of information a content editor can manage for this widget. This is all done in the hero.entity.ts file, which acts as the blueprint for our widget's properties within the Sitefinity backend.

1import { WidgetEntity, DataType,
2ContentContainer, KnownFieldTypes,
3Content, KnownContentTypes, MixedContentContext } from '@progress/sitefinity-widget-designers-sdk';
4
5@WidgetEntity('Hero', 'Hero')
6export class HeroEntity {
7 @ContentContainer()
8 @DataType('string')
9 Heading: string | null = null;
10
11 @ContentContainer()
12 @DataType('string')
13 SubHeading: string | null = null;
14
15 @ContentContainer()
16 @DataType(KnownFieldTypes.Html)
17 Content: string | null = null;
18
19 @Content({
20 Type: KnownContentTypes.Images,
21 AllowMultipleItemsSelection: false })
22 public Image: MixedContentContext | null = null;
23
24 @Content({
25 Type: KnownContentTypes.Pages,
26 AllowMultipleItemsSelection: false })
27 public CtaLink: MixedContentContext | null = null;
28
29 @ContentContainer()
30 @DataType('string')
31 CtaText: string | null = null;
32}
33

Let's break down what these decorators and properties mean:

  • @WidgetEntity('Hero', 'Hero'): This decorator sets the widget 's name and the widget's display caption.
    In the widget-registry.ts when registering the widget we can also set the name of the widget in the Name property as part of the editorMetadata. If set it will override the name defined in the @WidgetEntity attribute.
  • Heading: string | null = null;: This property will store the main title of our hero section.
    • @DataType('string') specifies that this field will hold plain text.
  • SubHeading: string | null = null;: Similar to the heading, this is for a secondary line of text, often smaller, that complements the main heading.
  • Content: string | null = null;: This is where the main descriptive text or paragraph for the hero section will go.
    • @DataType(KnownFieldTypes.Html) tells Sitefinity to provide a rich text editor (WYSIWYG) for this field. This allows content editors to format text, add lists, and more.
  • @ContentContainer(): This decorator above HeadingSubHeading, and Content tell Sitefinity to index these properties (by Sitefinity's search).
  • Image: MixedContentContext | null = null;: This property is designed to hold the hero image.
    • @Content({Type: KnownContentTypes.Images, AllowMultipleItemsSelection: false }) configures this property to use Sitefinity's image selector. KnownContentTypes.Images specifies that we're expecting an image, and AllowMultipleItemsSelection: false restricts the editor to selecting only one image. The MixedContentContext property is used to select content, such as images, news, pages.
      This selector can work with different types of providers.
  • CtaLink: MixedContentContext | null = null;: This property will store the link for our call-to-action button.
    • @Content({Type: KnownContentTypes.Pages, AllowMultipleItemsSelection: false }) allows editors to pick a page from the Sitefinity site structure to link to. Again, only a single page selection is allowed.
  • CtaText: string | null = null;: This holds the text for the call-to-action button, like "Learn More" or "Get Started."

With this entity defined, content editors will have a clear and structured way to input all the necessary content for the Hero widget.

Building the React Component: hero.view.tsx 

1import React from 'react';
2import { WidgetContext, htmlAttributes, SanitizerService } from '@progress/sitefinity-nextjs-sdk';
3import { RestClient, RestSdkTypes, ImageItem, PageItem } from '@progress/sitefinity-nextjs-sdk/rest-sdk';
4import { HeroEntity } from './hero.entity';
5import styles from './hero.module.css'; // Assuming you have a CSS module for styles
6
7export async function HeroView(props: WidgetContext<HeroEntity>) {
8
9 const dataAttributes = htmlAttributes(props);
10 const entity = props.model.Properties;
11 const sanitizerService = SanitizerService.getInstance();
12
13 const selectedImageId = entity.Image ? entity.Image.ItemIdsOrdered?.[0] : '';
14
15 const selectedImageSource = entity.Image?.Content?.[0]?.Variations?.[0]?.Source ?? '';
16
17 let imageItem: ImageItem | undefined = undefined;
18
19 const selectedPageId = entity.CtaLink ? entity.CtaLink.ItemIdsOrdered?.[0] : '';
20
21 let pageItem: PageItem | undefined = undefined;
22
23 if (entity.Image && selectedImageId) {
24 imageItem = await RestClient.getItemWithFallback<ImageItem>({
25 type: RestSdkTypes.Image,
26 id: selectedImageId.toString(),
27 provider: selectedImageSource.toString(),
28 culture: props.requestContext.culture
29 });
30 }
31
32 if (entity.CtaLink && entity.CtaText) {
33 pageItem = await RestClient.getItem<PageItem>({
34 type: RestSdkTypes.Pages,
35 id: (selectedPageId ?? '').toString(),
36 culture: props.requestContext.culture
37 });
38 }
39
40 const safeSanitizeHtml = (html: string | null): string => {
41 if (!html) return '';
42 return sanitizerService.sanitizeHtml(html).toString();
43 };
44
45 const heading = safeSanitizeHtml(entity.Heading);
46 const subHeading = safeSanitizeHtml(entity.SubHeading);
47 const content = safeSanitizeHtml(entity.Content);
48 const ctaText = safeSanitizeHtml(entity.CtaText);
49
50 return (
51 <>
52 <div {...dataAttributes} className={styles.heroWrapper}>
53 <div className={styles.heroTextwrapper}>
54 <div className={styles.heroText}>
55 <h1 className={styles['display1']}
56 dangerouslySetInnerHTML={{ __html: heading }} />
57 </div>
58 <div className={styles.heroInfo}>
59 <div className={styles['h3']}
60 dangerouslySetInnerHTML={{ __html: subHeading }} />
61
62 <div className={styles['description']}
63 dangerouslySetInnerHTML={{ __html: content }} />
64
65 {pageItem && ctaText && (
66 <a href={pageItem?.ViewUrl} className={styles['cta-button']}
67 dangerouslySetInnerHTML={{ __html: ctaText }} />
68 )}
69 </div>
70 </div>
71 {imageItem?.Thumbnails?.filter(
72 thumbnail => thumbnail.Title === 'sm').map(
73 (thumbnail) => (
74 <div key={thumbnail.Url} className={styles.heroMediaWrapper}>
75 <div className={styles.heroMedia}>
76 <img src={thumbnail.Url} title={imageItem.AlternativeText || ''}
77 alt={imageItem.AlternativeText || 'Hero image'} />
78 </div>
79 </div>
80 ))}
81 </div>
82 </>
83 );
84}
85

Let's walk through the key parts of this component:

Initial Setup:

    • dataAttributes = htmlAttributes(props): These attributes are crucial. It is required to include certain custom html attributes which allow Sitefinity's page editor to recognize and interact with the widget when you're in edit mode (e.g., for drag-and-drop, editing the widget in the backend).
    • entity = props.model.Properties: This conveniently gives us access to all the properties we defined in HeroEntity (like HeadingImage, etc.) that the content editor has filled in.
    • sanitizerService = SanitizerService.getInstance(): We initialize an instance of Sitefinity's SanitizerService. This is a vital tool for security. It cleans the HTML, removing any potentially harmful scripts or tags to prevent XSS attacks.

Fetching Content Details:

    • We first extract the ID (selectedImageId) and provider source (selectedImageSource) of the chosen image from the entity.Image property.
    • If an image has been selected (entity.Image && selectedImageId), we use RestClient.getItemWithFallback<ImageItem>({...}). This powerful function from the Sitefinity Next.js SDK asynchronously fetches the full details of the selected image item from Sitefinity. This includes its URL, alternative text, dimensions, and available thumbnails. We pass props.requestContext.culture to ensure we get the image appropriate for the current language version of the page.
    • A similar process happens for the Call to Action (CTA) link. If a page has been selected in entity.CtaLink and entity.CtaText (the button text) is provided, we fetch the details of that page (most importantly, its ViewUrl) using RestClient.getItem<PageItem>({...}).

Security is Key: Sanitizing HTML:

    • The safeSanitizeHtml function is a helper we've defined. It takes an HTML string as input (which could come from our HeadingSubHeading, or Content fields) and uses the sanitizerService.sanitizeHtml() method. This method cleans the HTML, removing any potentially harmful scripts or tags, which is a critical step to prevent cross-site scripting (XSS) vulnerabilities.
    • Before rendering, we process entity.Headingentity.SubHeadingentity.Content, and entity.CtaText through this sanitization function.

Rendering the Hero:

    • The return (...) block contains the JSX that defines the HTML structure of our Hero widget.
    • You'll notice dangerouslySetInnerHTML={{ __html: heading }}. The name "dangerouslySetInnerHTML" sounds alarming, but because we have already sanitized the heading (and subHeadingcontentctaText) using our safeSanitizeHtml function, it's now safe to use. This approach allows us to render any legitimate HTML formatting (like bold text from the rich text editor for Content) that the content editor intended. This is the correct and safe way to handle HTML content in Next.js, and it's the direct equivalent of Html.Raw() in MVC.
    • The CTA button (<a> tag) is rendered conditionally: only if pageItem (meaning a page was selected in the widget designer and successfully fetched) and ctaText (the button text) are available. Its href is set to pageItem?.ViewUrl.
    • For the image, we first check if imageItem and its Thumbnails property exist. Sitefinity can automatically generate different sizes (thumbnails) of an image. Here, we're filtering to find a thumbnail specifically titled 'sm' (you can configure thumbnail profiles in Sitefinity). We then map over the found thumbnails (though we expect one 'sm' thumbnail) to render the <img> tag. Using imageItem.AlternativeText for the title and alt attributes is important for accessibility. If you don't have the image thumbnails defined in your system, you could use imageItem.MediaUrl as the source for the image tag.

Register your widget

After creating your hero.entity.ts and hero.view.tsx files, the final step to make your custom Hero widget available in the Sitefinity page editor is to register it. This is typically done in a widget-registry.ts file (or a similarly named file, depending on your project's structure) within your Next.js application.

This registry acts as a directory that tells Sitefinity about all the custom widgets available for your site, how to render them, and what data they expect.

Updating the Widget Registry

You'll need to import your newly created HeroEntity and HeroView. You'll also typically import WidgetMetadata from the Sitefinity SDK if you need more advanced editor configurations, though for a basic title, it's straightforward.

Here's how you would add your Hero widget to the collection:

1import { WidgetRegistry } from '@progress/sitefinity-nextjs-sdk';
2
3// Import your custom widget's entity and view
4import { HeroEntity } from './widgets/hero/hero.entity'; // Adjust path as necessary
5import { HeroView } from './widgets/hero/hero.view'; // Adjust path as necessary
6
7// You might also import WidgetMetadata
8// if you plan to add more complex editor configurations later
9// import { WidgetMetadata } from '@progress/sitefinity-widget-designers-sdk';
10
11export const customWidgetRegistry: WidgetRegistry = {
12 widgets: {
13 // ... any other custom widgets you might have
14 'Hero': { // This key is how Sitefinity will identify your widget internally
15 // Specifies the React component to render the widget
16 componentType: HeroView,
17 // Specifies the entity class defining the widget's properties
18 entity: HeroEntity,
19 // Set to true if your widget is SSR-compatible (which ours is)
20 ssr: true,
21 // Metadata for the Sitefinity page editor
22 editorMetadata: {
23 // The display name of your widget in the editor's toolbox
24 Title: 'Hero'
25 // You can add other properties here
26 // like 'Section', 'EmptyIcon', 'EmptyIconAction'
27 }
28 }
29 // ...
30 }
31};
32
33customWidgetRegistry.widgets = {
34 ...defaultWidgetRegistry.widgets,
35 ...customWidgetRegistry.widgets
36};
37
38export const widgetRegistry:
39WidgetRegistry = initRegistry(customWidgetRegistry);
40


Styling the Hero widget: hero.module.css

For styling React components, especially in a Next.js environment, CSS Modules are a fantastic approach. They locally scope your styles by default, which means the class names you define in one module won't accidentally clash with class names in another. This helps keep your styling organized and maintainable.

In our hero.view.tsx component, you'll notice the line:

1import styles from './hero.module.css';
2

This imports a CSS file named hero.module.css (which you would create in the same directory as your hero.view.tsx file). The styles object then becomes a mapping of the class names you define in your CSS file to unique, generated class names. For example, if you have .heroWrapper in your CSS, you'd use it in your JSX like className={styles.heroWrapper}.

Here's a sample set of styles you can use for hero.module.css to achieve a modern and appealing look for your Hero widget:

1.heroWrapper {
2 display: flex;
3 align-items: center;
4 justify-content: space-between;
5 background: #181646;
6 padding: 64px 5vw;
7 min-height: 480px;
8 gap: 56px;
9 }
10
11 .heroTextwrapper {
12 flex: 1 1 0;
13 max-width: 520px;
14 color: #fff;
15 }
16
17 .heroText {
18 margin-bottom: 24px;
19 }
20
21 .display1 {
22 font-size: 2.5rem;
23 font-weight: 700;
24 line-height: 1.15;
25 margin-bottom: 16px;
26 }
27
28 .heroInfo .h3 {
29 font-size: 1rem;
30 font-weight: 600;
31 letter-spacing: 0.08em;
32 text-transform: uppercase;
33 color: #bfc1ff;
34 margin-bottom: 20px;
35 }
36
37 .heroInfo .description {
38 font-size: 1.1rem;
39 color: #e6e7f9;
40 margin-bottom: 28px;
41 line-height: 1.7;
42 }
43
44 .heroInfo .cta-button {
45 display: inline-block;
46 background: #2e6cff;
47 color: #fff;
48 font-weight: 600;
49 border-radius: 24px;
50 padding: 14px 32px;
51 font-size: 1rem;
52 text-decoration: none;
53 box-shadow: 0 2px 12px rgba(46,108,255,0.18);
54 margin-top: 16px;
55 transition: background 0.2s;
56 }
57 .heroInfo .cta-button:hover {
58 background: #1e4bb8;
59 }
60
61 .heroMediaWrapper {
62 flex: 1 1 0;
63 display: flex;
64 justify-content: flex-end;
65 align-items: center;
66 min-width: 340px;
67 position: relative;
68 }
69
70 .heroMedia {
71 background: #fff;
72 border-radius: 18px;
73 box-shadow: 0 8px 32px rgba(0,0,0,0.12);
74 overflow: visible;
75 position: relative;
76 padding: 0;
77 display: flex;
78 align-items: center;
79 justify-content: center;
80 }
81
82 .heroMedia img {
83 width: 100%;
84 border-radius: 18px;
85 display: block;
86 }
87
88 .heroMediaOverlay {
89 position: absolute;
90 top: 28px;
91 right: -44px;
92 width: 220px;
93 background: #f7f8fc;
94 border-radius: 12px;
95 box-shadow: 0 4px 24px rgba(24,22,70,0.18);
96 z-index: 2;
97 padding: 0;
98 }
99
100 @media (max-width: 900px) {
101 .heroWrapper {
102 flex-direction: column;
103 align-items: flex-start;
104 gap: 36px;
105 padding: 40px 5vw;
106 }
107 .heroMediaWrapper, .heroMedia {
108 width: 100%;
109 min-width: 0;
110 justify-content: center;
111 }
112 .heroMediaOverlay {
113 position: static;
114 margin: 24px auto 0;
115 right: 0;
116 top: 0;
117 }
118 }
119

And that's it!

We've successfully built a custom Hero widget in Sitefinity using the Next.js SDK. We covered defining the entity, creating the React view component, fetching and displaying content like text, images, and page links, the importance of sanitization, registering the widget, and finally, adding some CSS Module styles."

This is just a starting point, of course. You could extend this with more fields, different layouts, or more complex interactions. But hopefully, this gives you a solid foundation for building your own custom Sitefinity Next.js widgets.

Related Posts