Simple Content Block Tutorial
This tutorial takes you through the process of building a simple Content Block Addon for Sana Commerce. Although it is notable that not every Addon uses Content Blocks (e.g., most third-party integrations may not require frontend UI), but building a simple Content Block is a good place to start learning the basics of Addon development.
The Content Block Addon we're going to develop will render a single HTML <h1>
heading element, using text configured by the user in Sana Admin.
Step 1: Create the Content Block Model Class
The Model Class defines the data structure for your content block. This data is configured by users within Sana Admin and subsequently used by the webstore to render the content block.
Add a new C# class file named
CustomHeadingContentBlockModel.cs
to your Addon project.Define the class as follows, ensuring it inherits from
ContentBlockModel
and includes the necessary properties (in this case, aText
property):using Sana.Extensions.ContentBlocks; /// <summary> /// Represents the data model for the Custom Heading Content Block. /// This model holds the configuration data entered by the user in Sana Admin. /// </summary> public class CustomHeadingContentBlockModel : ContentBlockModel { /// <summary> /// Gets or sets the text to be displayed in the heading. /// </summary> public string Text { get; set; } }
Step 2: Implement the Extension Class
The Extension Class registers your new content block type with the Sana Commerce framework.
Add a new C# class file named
CustomHeadingContentBlockExtension.cs
to your Addon project.Define the class, inheriting from
ContentBlockExtension<T>
whereT
is your model class (CustomHeadingContentBlockModel
).Decorate the class with the
ContentBlockId
attribute, providing a unique identifier for this content block type. This ID is crucial for Sana Commerce to link the content block with correct frontend implementation of the content block.using Sana.Extensions.ContentBlocks; /// <summary> /// Registers the Custom Heading Content Block with Sana Commerce. /// </summary> [ContentBlockId("CustomHeading")] public class CustomHeadingContentBlockExtension : ContentBlockExtension<CustomHeadingContentBlockModel> { // Further logic or overrides can be added here if needed, // but for this simple example, none are required. }
Step 3: Create the UI Component (React)
The UI Component is responsible for rendering the content block in the webstore using the data provided by the model.
3.1. Setup the frontend structure
Follow these steps to establish the necessary structure and configuration for client-side development within your Addon project.
Create Base Folder: Create an empty folder named
ClientApp
at the root of your Addon project. All client-side source code must reside within this directory.Create Webstore Folder: Inside
ClientApp
, create an empty folder namedwebstore
. This folder will house all client-side code specifically for the webstore. (Note: A siblingadmin
folder could exist for the UI implementation of Sana Admin, but that is outside the scope of this tutorial.)Configure TypeScript/JavaScript: Create a
tsconfig.json
(for TypeScript) orjsconfig.json
(for JavaScript) file within theClientApp/webstore
folder. This file enables IntelliSense and configures the compiler/transpiler, extending the base configuration from the Sana Commerce SDK.tsconfig.json
/jsconfig.json
:{ "extends": "../../../../SDK/Sana.Commerce.WebApp/ClientApp/src/webstore/tsconfig.json", "compilerOptions": { "baseUrl": ".", "paths": { "sana/*": [ "../../../../SDK/Sana.Commerce.WebApp/ClientApp/src/adk/webstore/*" ], "*": [ "./*", "../../../../SDK/Sana.Commerce.WebApp/ClientApp/node_modules/@types/*", "../../../../SDK/Sana.Commerce.WebApp/ClientApp/node_modules/*" ] }, "types": [] }, "include": [ "./**/*", "../../../../SDK/Sana.Commerce.WebApp/ClientApp/src/adk/webstore/typedef/*" ] }
Configure NPM (Optional): If required by your environment or dependencies, create a file named
.npmrc
in theClientApp
folder (or wherever yourpackage.json
resides) with the following content:# .npmrc legacy-peer-deps=true node-options="--openssl-legacy-provider"
Configure Jest (Optional): If you plan to use Jest (version 29.5.0 or higher) for testing, ensure your
package.json
scripts use the--no-compilation-cache
flag for Node.js:"scripts": { "test": "node --no-compilation-cache ./node_modules/jest/bin/jest.js --ci --coverage", "jest": "node --no-compilation-cache ./node_modules/jest/bin/jest.js" }
(Note: You may also need to install
jest
and related dependencies via npm if not already present.)
3.2. Implement the React Component
Create the React component that defines the visual representation of your content block.
Create Component File: Create a file named
CustomHeadingBlock.tsx
(or.jsx
) inside theClientApp/webstore
folder.Implement Component Logic: Write the React component. It receives the content block model data via props and renders the corresponding HTML.
import { memo } from 'react'; import type { ContentBlockProps } from 'sana/types'; import { Row, Col } from 'sana/elements'; // Define the expected structure of the model data received from the backend type CustomHeadingBlockModel = { text: string | null; }; /** * React component responsible for rendering the Custom Heading Content Block. * Displays the 'text' property from the model within an H1 tag. */ const CustomHeadingBlock = ({ model }: ContentBlockProps<CustomHeadingBlockModel>) => { // Basic validation or default value handling can be added here const displayText = model?.text ?? 'Default Heading'; // Example default return ( <Row> <Col md={12}> <h1>{displayText}</h1> </Col> </Row> ); }; // Use memo for performance optimization if the component is pure export default memo(CustomHeadingBlock);
3.3. Create the Client-Side Entry Point
Define the main entry point for your Addon's webstore client-side bundle. This file exports the component(s) mapping them to their registered Content Block IDs.
Create Entry Point File: Create a file named
index.ts
(or.js
) inside theClientApp/webstore
folder.Export Component(s): Define the
addonExports
object. ThecontentBlocks
property should be an object where keys match theContentBlockId
specified in the C# Extension class (CustomHeading
in this case), and values are references to the corresponding React components.import type { AddonExports } from 'sana/types'; import CustomHeadingBlock from './CustomHeadingBlock'; /** * Defines the exports for the Addon's webstore client-side bundle. * Maps Content Block IDs to their respective React components. */ const addonExports: AddonExports = { contentBlocks: { // Key must match the [ContentBlockId("...")] attribute value 'CustomHeading': CustomHeadingBlock, }, }; export default addonExports;
Final Directory Structure
After completing the steps above, your Addon project structure should resemble the following:
𝗦𝗼𝗹𝘂𝘁𝗶𝗼𝗻 '𝗦𝗮𝗻𝗮 𝗖𝗼𝗺𝗺𝗲𝗿𝗰𝗲'/
├── Addons 📂/
│ ├── Sana.Extensions.CustomHeadingContentBlock 📂/
│ │ ├── Dependencies 📁
│ │ ├── Properties 📁
│ │ ├── ClientApp 📂/
│ │ │ ├── webstore 📂/
│ │ │ │ ├── CustomHeadingBlock.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── tsconfig.json
│ │ │ ├── .npmrc
│ │ │ └── package.json
│ │ ├── CustomHeadingContentBlockModel.cs
│ │ ├── CustomHeadingContentBlockExtension.cs
│ │ └── sanamanifest.xml
│ └── .eslintrc.json
├── Benchmarks 📁
├── Framework 📁
├── Modules 📁
├── SDK 📂/
│ └── 🌐 𝗦𝗮𝗻𝗮.𝗖𝗼𝗺𝗺𝗲𝗿𝗰𝗲.𝗪𝗲𝗯𝗔𝗽𝗽 📁
└── Solution Items 📁
Note
The exact structure might vary slightly based on your solution setup and specific naming conventions.
What's next?
Continue with building, testing & debugging your Addon.