Frontend development

Sample: Next.js script widget

Overview

The Script widget allows you to embed JavaScript code or external script files on your pages. You can also use it to paste embed codes from third-party tools, such as Google Analytics, social media widgets, or other tracking solutions.

You can choose from the following options:

  • Inline
    In this mode the Script widget renders the content of the text area in the widget designer on the place where the widget is dropped in the WYSIWYG editor
  • BodyTop
    These modes register scripts with the script injection system. The ScriptInjector components subsequently retrieve these scripts and position them at the beginning of the <body> tag.
  • BodyBottomThese modes register scripts with the script injection system. The ScriptInjector components subsequently retrieve these scripts and position them at the end of the <body> tag.

PREREQUISITES: You must set up a Sitefinity renderer application and connect it to your Sitefinity CMS application. For more information, see Install Sitefinity in Next.js mode

Required setup

Two critical configurations are needed:

  • Include ScriptInjector components in the page template - Add ScriptInjectorBodyTop and ScriptInjectorBodyBottom components to your page template
  • Pass RequestContext to ScriptInjectors - The ScriptInjector components require the RequestContext to access the layout data

IMPORTANT: This custom template configuration is required for every page where the Script widget is placed with BodyTop or BodyBottomlocation.

Create the widget

The implementation consists of four main components:

  • Script Widget (script.tsx)
    Main widget component that renders inline scripts or returns null for BodyTop/BodyBottom scripts
    In edit mode, displays a preview of the script with location information
    Server-side component that processes the widget configuration
  • ScriptInjector (script-injector.tsx)
    Scans the page layout to find all Script widgets with BodyTop or BodyBottom location
    Renders the scripts at the appropriate positions in the page template
    Disabled in edit mode to prevent script execution during page editing
  • ScriptEntity (script.entity.ts)
    Designer metadata defining the widget properties
    Configures the widget editor interface in Sitefinity
  • ScriptLocation constant (location.ts)
    Select where the script should be placed (Inline, Body top, or Body bottom)

  • Navigate to '../widgets/script/'
  • Create a script.tsx file.
  • In the file, paste the following code and save your changes:
TSX
import React from 'react';
import 'server-only';
import { WidgetContext, htmlAttributes } from '@progress/sitefinity-nextjs-sdk';
import { ScriptEntity } from './script.entity';
import { ScriptLocation } from './script-location';

export function Script(props: WidgetContext<ScriptEntity>) {
  const { model } = props;
  const entity = model.Properties;

  const location = entity.Location || ScriptLocation.Inline;
  const content = entity.Script || '';
  const scriptId = `script-${model.PlaceHolder}-${location}-${content}`;
  const dataAttributes = htmlAttributes(props);

  if (props.requestContext.isEdit) {
    const paragraphs = content && content.length ? content.split(/\r?\n/).slice(0, 3) : [];
    let scriptLocationString = '';
    if (content) {
      switch (location) {
        case ScriptLocation.Inline: scriptLocationString = 'Included where the widget is dropped.'; break;
        case ScriptLocation.BodyBottom: scriptLocationString = 'Included before the closing </body> tag.'; break;
        case ScriptLocation.BodyTop: scriptLocationString = 'Included after the opening <body> tag.'; break;
        default: break;
      }
    }

    return (
      <div {...dataAttributes}>
        {paragraphs.map((line, i) => (
          <React.Fragment key={i}>
            {line}<br />
          </React.Fragment>
        ))}
        {scriptLocationString && (
          <>
            ...<br />
            <i>{scriptLocationString}</i>
          </>
        )}
      </div>
    );
  }

  if (location === ScriptLocation.BodyBottom || location === ScriptLocation.BodyTop) {
    // BodyBottom and BodyTop scripts are handled by ScriptInjector components
    // which read them directly from the layout data
    return null;
  }

  return (
    <div
      className="script-widget"
      {...dataAttributes}
      id={scriptId + '-container'}
      dangerouslySetInnerHTML={{ __html: content }}
    />
  );
}

  • Navigate to '../widgets/script/'
  • Create a script-injector.tsx file.
  • In the file, paste the following code and save your changes:
TSX
import React from 'react';
import { WidgetModel } from '@progress/sitefinity-nextjs-sdk';
import { RequestContext } from '@progress/sitefinity-nextjs-sdk';
import { ScriptLocation, ScriptLocationType } from './script-location';

interface ScriptInjectorProps {
  requestContext: RequestContext;
}

// Helper function to flatten nested widget hierarchy
function flattenWidgets(widgets: WidgetModel[]): WidgetModel[] {
  return widgets.reduce((acc: WidgetModel[], widget: WidgetModel): WidgetModel[] => {
    if (Array.isArray(widget?.Children) && widget.Children.length) {
      return acc.concat(flattenWidgets(widget.Children));
    }

    return acc.concat([widget]);
  }, []);
}

// Helper function to filter and render scripts by location
function renderScriptsByLocation(requestContext: RequestContext, location: ScriptLocationType) {
  // Don't render scripts in edit mode
  if (requestContext.isEdit) {
    return null;
  }

  const allWidgets = flattenWidgets(requestContext.layout.ComponentContext.Components || []);

  const scripts = allWidgets
    .filter(widget => widget.Name === 'Script')
    .filter(widget => {
      const widgetLocation = widget.Properties?.Location || ScriptLocation.Inline;
      return widgetLocation === location;
    });

  if (scripts.length === 0) {
    return null;
  }

  return (
    <>
      {scripts.map((widget) => {
        const scriptId = `script-${widget.PlaceHolder}-${location}-${widget.Id}`;
        const content = widget.Properties?.Script || '';

        return (
          <div
            key={scriptId}
            id={scriptId}
            dangerouslySetInnerHTML={{ __html: content }}
          />
        );
      })}
    </>
  );
}

export function ScriptInjectorBodyTop({ requestContext }: ScriptInjectorProps) {
  return renderScriptsByLocation(requestContext, ScriptLocation.BodyTop);
}

export function ScriptInjectorBodyBottom({ requestContext }: ScriptInjectorProps) {
  return renderScriptsByLocation(requestContext, ScriptLocation.BodyBottom);
}

  • Navigate to '../widgets/script/'
  • Create a script.entity.ts file.
  • In the file, paste the following code and save your changes:
TypeScript
import { Choice, ContentSection, ContentSectionTitles, DataType, DefaultValue, Description, DisplayName, KnownFieldTypes, WidgetEntity } from '@progress/sitefinity-widget-designers-sdk';
import { ScriptLocation } from './script-location';

@WidgetEntity('Script', 'Script')
export class ScriptEntity {
    @ContentSection(ContentSectionTitles.LabelsAndMessages, 0)
    @DisplayName('Script location')
    @Description('Select where the script should be placed on the page')
    @Choice([
        { Title: 'Inline', Name: ScriptLocation.Inline, Value: ScriptLocation.Inline },
        { Title: 'Body top', Name: ScriptLocation.BodyTop, Value: ScriptLocation.BodyTop },
        { Title: 'Body bottom', Name: ScriptLocation.BodyBottom, Value: ScriptLocation.BodyBottom }
    ])
    @DefaultValue(ScriptLocation.Inline)
    Location?: string = ScriptLocation.Inline;

    @ContentSection(ContentSectionTitles.LabelsAndMessages, 1)
    @DisplayName('Script')
    @Description('Put the script with its wrapping tag -> e.g. <script>javascript code</script>')
    @DataType(KnownFieldTypes.TextArea)
    Script?: string;
}

  • Navigate to '../widgets/script/'
  • Create a location.ts file.
  • In the file, paste the following code and save your changes:
TypeScript
export const ScriptLocation = {
    Inline: 'Inline',
    BodyTop: 'BodyTop',
    BodyBottom: 'BodyBottom'
} as const;

export type ScriptLocationType = typeof ScriptLocation[keyof typeof ScriptLocation];

Integrate with page templates

 

Perform the following:

  1. Navigate to your page template file (e.g. src/app/templates/sitefinity-template.tsx)

  2. In the file, paste the following code and save your changes:

    TSX
    import { ScriptInjectorBodyTop, ScriptInjectorBodyBottom } from '../widgets/script/script-injector';
    
    export function SitefinityTemplate({ widgets, requestContext }: {
        widgets: { [key: string]: ReactNode[] };
        requestContext: RequestContext;
    }): JSX.Element {
        return (
          <>
            <ScriptInjectorBodyTop requestContext={requestContext} />
            <header data-sfcontainer="Header">
              {widgets['Header']}
            </header>
            <main data-sfcontainer="Content">
              {widgets['Content']}
            </main>
            <footer data-sfcontainer="Footer">
              {widgets['Footer']}
            </footer>
            <ScriptInjectorBodyBottom requestContext={requestContext} />
          </>
        );

Register the widget

The next step is to register the component implementation and the designer metadata with the Next.js renderer. The Sitefinity Next.js SDK will automatically generate the needed designer metadata based on the defined entity. The registry is used to find the component function reference for the widget from the response of the Page Layout service. It is also used when generating metadata for the widget when it is used in the WYSIWYG page editor – labels, visuals etc.

The widget-registry.ts file file should include the following code:

TypeScript
import { WidgetRegistry, initRegistry, defaultWidgetRegistry } from '@progress/sitefinity-nextjs-sdk';
import { Script } from './widgets/script/script';
import { ScriptEntity } from './widgets/script/script.entity';

const customWidgetRegistry: WidgetRegistry = {
    widgets: {
        'Script': {
            componentType: Script,
            entity: ScriptEntity,
            ssr: true, 
            editorMetadata: {
                Title: 'Script',
                Category: 'Content',
                Section: 'Basic',
                EmptyIconText: 'Set JavaScript',
                EmptyIconAction: 'Edit',
                IconName: 'code'
            }
        }
    }
};

customWidgetRegistry.widgets = {
    ...defaultWidgetRegistry.widgets,
    ...customWidgetRegistry.widgets
};

export const widgetRegistry: WidgetRegistry = initRegistry(customWidgetRegistry);

Result

When you open your Renderer application and open the New editor, you will see the Script widget in the widget selector. When you add the widget on your page and edit it, you can choose where to place the script and you can enter the script in the text area.

Script

Key Implementation Details

  • Server-Side Rendering: The Script widget is a server component that processes during page rendering
  • Edit Mode Preview: In edit mode, shows the first 3 lines of the script with a location hint
  • Layout Scanning: ScriptInjector components scan the entire widget tree to find Script widgets
  • Unique IDs: Each script gets a unique ID based on placeholder, location, and widget ID
  • HTML Injection: Scripts are injected using dangerouslySetInnerHTML for direct HTML rendering
Want to learn more?
Enhance your Sitefinity skills by enrolling in free training sessions. Become Sitefinity certified through Progress Education Community to strengthen your professional credentials.