Skip to main content

Components

Component vs CMS Component

In this project, component refers to a React component — a .component.tsx file in src/app/components/. A CMS Component is a different concept: a reusable embedded field schema defined in the Contensis CMS itself (no sys.id, not an entry). The two are unrelated.

Directory structure​

Every component lives in its own directory under src/app/components/:

src/app/components/
└── card/
├── card.component.tsx # Main component (required)
├── card.mapper.ts # CMS data transformer (required if CMS-backed)
├── card.stories.tsx # Storybook story (recommended)
├── card.styled.ts # Styled components — split out if file gets large (optional)
├── card.types.ts # Types — split out if file gets large (optional)
└── index.ts # Re-export (optional)

Page-level templates follow the same convention but live in src/app/templates/.

Component file pattern​

src/app/components/card/card.component.tsx
import React from 'react';
import { styled } from 'styled-components';

// 1. Props type — always exported, never inlined
export type CardProps = {
title: string;
description?: string;
imageUrl?: string;
onClick?: () => void;
};

// 2. Component
const Card = ({ title, description, imageUrl, onClick }: CardProps) => {
return (
<CardStyled onClick={onClick}>
{imageUrl && <CardImage src={imageUrl} alt={title} />}
<CardContent>
<CardTitle>{title}</CardTitle>
{description && <CardDescription>{description}</CardDescription>}
</CardContent>
</CardStyled>
);
};

// 3. Styled components — defined after the main component
const CardStyled = styled.div`
border-radius: 4px;
overflow: hidden;
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
`;

const CardImage = styled.img`
width: 100%;
height: 200px;
object-fit: cover;
`;

const CardContent = styled.div`
padding: ${({ theme }) => theme.spacing.m};
`;

const CardTitle = styled.h3`
margin: 0 0 ${({ theme }) => theme.spacing.xxs};
`;

const CardDescription = styled.p`
margin: 0;
`;

// 4. Default export
export default Card;

For Styled Components patterns (transient props, theme access, extending base components), see the Code Style guide.

Mapper file pattern​

Mappers transform raw Contensis CMS entries into the typed props your component expects. They live alongside the component and are used by the search system and page templates.

src/app/components/card/card.mapper.ts
import type { Entry } from 'contensis-delivery-api';
import type { CardProps } from './card.component';

// Extend the base Entry type with your content type's fields
type CmsCardEntry = Entry & {
cardTitle: string;
cardDescription?: string;
cardImage?: {
asset?: { sys?: { uri: string } };
};
};

export const cardMapper = (entry: CmsCardEntry): CardProps => ({
title: entry.cardTitle,
description: entry.cardDescription,
imageUrl: entry.cardImage?.asset?.sys?.uri ?? '',
});

Mapper best practices​

  • Handle null and undefined: CMS data may be incomplete — use optional chaining (?.) and nullish coalescing (??)
  • Type both input and output: Type the raw CMS entry and the returned props
  • Keep mappers pure: No side effects — mappers should only transform data, never fetch or dispatch
  • Use Entry &: Extend the Entry base type rather than defining a separate type from scratch

Icon components​

SVG icons are converted to React components and registered in a dictionary:

src/app/components/icon/icons/close.tsx
import React from 'react';

export const Close = (props: React.SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" {...props}>
<title>Close</title>
<path fill="currentColor" d="M2 15.435..." />
</svg>
);
src/app/components/icon/icon.dictionary.ts
import { Close } from './icons/close';
import { Menu } from './icons/menu';
import { Search } from './icons/search';

export const iconDictionary = {
close: Close,
menu: Menu,
search: Search,
};

Composer integration​

Components that appear in the Contensis Composer field must be exported via @loadable/component from src/app/components/index.ts:

src/app/components/index.ts
import loadable from '@loadable/component';

export const Card = loadable(() => import('./card/card.component'));
export const Hero = loadable(() => import('./hero/hero.component'));

See the Composer guide for how these are registered and rendered.

Component checklist​

Before considering a component done:

  • Props type defined and exported (ComponentNameProps)
  • Styled components use theme.* tokens — no hardcoded values
  • Mapper file exists if the component receives CMS data
  • Storybook story created with args and argTypes
  • @loadable export added to src/app/components/index.ts if used in Composer
  • Added to the Composer componentMap if used in a Composer field
  • TypeScript strict mode passes (npm run typecheck)
  • Lint passes (npm run lint)