@se-studio/skills 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +25 -0
- package/skills/contentful-cms-alt-text-audit/SKILL.md +60 -0
- package/skills/contentful-cms-cms-guidelines/README.md +166 -0
- package/skills/contentful-cms-cms-guidelines/colour-hint-prompt.md +77 -0
- package/skills/contentful-cms-cms-guidelines/evaluation-prompt.md +84 -0
- package/skills/contentful-cms-cms-guidelines/generate-component-guidelines.md +126 -0
- package/skills/contentful-cms-cms-guidelines/generation-prompt.md +231 -0
- package/skills/contentful-cms-cms-guidelines/html-component-authoring.md +401 -0
- package/skills/contentful-cms-cms-guidelines/validation-prompt.md +170 -0
- package/skills/contentful-cms-cms-guidelines/variant-loop.md +189 -0
- package/skills/contentful-cms-cms-guidelines/variant-proposal-prompt.md +131 -0
- package/skills/contentful-cms-core/SKILL.md +793 -0
- package/skills/contentful-cms-generate-all-guidelines/SKILL.md +313 -0
- package/skills/contentful-cms-generate-cms-guidelines/SKILL.md +313 -0
- package/skills/contentful-cms-image-guide/SKILL.md +240 -0
- package/skills/contentful-cms-navigation/SKILL.md +23 -0
- package/skills/contentful-cms-rich-text/SKILL.md +96 -0
- package/skills/contentful-cms-schema-org/SKILL.md +74 -0
- package/skills/contentful-cms-screenshots/SKILL.md +46 -0
- package/skills/contentful-cms-seo-descriptions/SKILL.md +54 -0
- package/skills/contentful-cms-templates/SKILL.md +21 -0
- package/skills/contentful-cms-update-cms-guidelines/SKILL.md +348 -0
- package/skills/performance-audit/SKILL.md +344 -0
- package/skills/se-marketing-sites-cms-routes-and-appshared/SKILL.md +99 -0
- package/skills/se-marketing-sites-create-collection/SKILL.md +295 -0
- package/skills/se-marketing-sites-create-component/SKILL.md +250 -0
- package/skills/se-marketing-sites-create-page/SKILL.md +183 -0
- package/skills/se-marketing-sites-curate-showcase-mocks/SKILL.md +344 -0
- package/skills/se-marketing-sites-handling-media/SKILL.md +195 -0
- package/skills/se-marketing-sites-lib-cms-structure/SKILL.md +83 -0
- package/skills/se-marketing-sites-register-cms-features/SKILL.md +95 -0
- package/skills/se-marketing-sites-styling-system/SKILL.md +122 -0
- package/skills/site-workflows-brand-context-builder/SKILL.md +98 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cms-routes-and-appshared
|
|
3
|
+
description: Guide for the (cms-routes) route group and appShared pattern. Use when setting up or migrating CMS-driven routing, creating new route segments, or refactoring page structure.
|
|
4
|
+
license: Private
|
|
5
|
+
metadata:
|
|
6
|
+
author: se-core-product
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# CMS Routes and appShared Pattern
|
|
11
|
+
|
|
12
|
+
Reference apps: **example-se2026**, **example-brightline**, **example-om1**. (example-om1 uses `resources/`, `team/`, `topics/` instead of `articles/`, `people/`, `tags/`.)
|
|
13
|
+
|
|
14
|
+
## Route Group: (cms-routes)
|
|
15
|
+
|
|
16
|
+
All CMS-driven pages live under `src/app/(cms-routes)/`. The layout enables dynamic params:
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// layout.tsx
|
|
20
|
+
export const dynamicParams = true;
|
|
21
|
+
|
|
22
|
+
export default function CmsRoutesLayout({ children }: { children: React.ReactNode }) {
|
|
23
|
+
return children;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Route Structure
|
|
28
|
+
|
|
29
|
+
| Path | File | Purpose |
|
|
30
|
+
|------|------|---------|
|
|
31
|
+
| `/` | `page.ts` | Home (slug: `index`) |
|
|
32
|
+
| `/{slug}/` | `[level1]/page.ts` | Single-level page |
|
|
33
|
+
| `/{slug1}/{slug2}/...` | `[level1]/[...slugs]/page.ts` | Multi-level pages |
|
|
34
|
+
| `/articles/` | `articles/page.ts` | Articles index (uses ARTICLES_BASE) |
|
|
35
|
+
| `/articles/{type}/` | `articles/[articleType]/page.ts` | Article type index |
|
|
36
|
+
| `/articles/{type}/{tag}/` | `articles/[articleType]/[tag]/page.ts` | Article type + tag index |
|
|
37
|
+
| `/articles/{type}/{tag}/{article}/` | `articles/[articleType]/[tag]/[...slugs]/page.ts` | Individual article |
|
|
38
|
+
| `/tags/`, `/tags/{tag}/` | `tags/` | Tag index (if enabled) |
|
|
39
|
+
| `/people/`, `/people/{person}/` | `people/` | People (if enabled) |
|
|
40
|
+
|
|
41
|
+
Base paths come from `@/lib/constants` (e.g. ARTICLES_BASE = `/learning-hub` or `/articles`).
|
|
42
|
+
|
|
43
|
+
## appShared Modules
|
|
44
|
+
|
|
45
|
+
Route files are thin wrappers. Logic lives in `src/appShared/`:
|
|
46
|
+
|
|
47
|
+
| Module | Exports | Used by |
|
|
48
|
+
|--------|---------|---------|
|
|
49
|
+
| `pageShared.tsx` | `generatePage`, `generatePageMetadata` | `/`, `[level1]`, `[level1]/[...slugs]` |
|
|
50
|
+
| `articleShared.tsx` | `generateArticlePage`, `generateArticleMetadata` | Article detail |
|
|
51
|
+
| `articleTypeShared.tsx` | `generateArticleTypePage`, `generateArticleTypeMetadata` | Article type index |
|
|
52
|
+
| `articleTypesIndexShared.tsx` | `generateArticleTypesIndexPage`, `generateArticleTypesIndexMetadata` | Articles index |
|
|
53
|
+
| `articleTypeTagShared.tsx` | `generateArticleTypeTagPage`, `generateArticleTypeTagMetadata` | Article type + tag index |
|
|
54
|
+
| `customTypeShared.tsx` | `generateCustomTypePage`, `generateCustomTypeMetadata` | Custom types |
|
|
55
|
+
| `personShared.tsx` | `generatePersonPage`, `generatePersonMetadata` | Person page |
|
|
56
|
+
| `tagShared.tsx` | `generateTagPage`, `generateTagMetadata` | Tag index |
|
|
57
|
+
| `tagsIndexShared.tsx` | `generateTagsIndexPage`, `generateTagsIndexMetadata` | Tags index |
|
|
58
|
+
| `teamIndexShared.tsx` | `generateTeamIndexPage`, `generateTeamIndexMetadata` | People index |
|
|
59
|
+
|
|
60
|
+
## Route Page Pattern
|
|
61
|
+
|
|
62
|
+
Each route page:
|
|
63
|
+
|
|
64
|
+
1. Exports `generateStaticParams() { return []; }` for dynamic routes
|
|
65
|
+
2. Defines `extractDetails(props)` to derive slug/path from params
|
|
66
|
+
3. Exports `generateMetadata(props, parent)` calling the shared metadata function
|
|
67
|
+
4. Exports default async function calling the shared page function
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import type { ResolvingMetadata } from 'next';
|
|
71
|
+
import { generatePage, generatePageMetadata } from '@/appShared/pageShared';
|
|
72
|
+
|
|
73
|
+
export function generateStaticParams() {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function extractDetails(props: PageProps<'/[level1]'>) {
|
|
78
|
+
const params = await props.params;
|
|
79
|
+
const { level1 } = params;
|
|
80
|
+
return { slug: level1, path: `/${level1}/` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function generateMetadata(props: PageProps<'/[level1]'>, parent: ResolvingMetadata) {
|
|
84
|
+
const { slug, path } = await extractDetails(props);
|
|
85
|
+
return generatePageMetadata(slug, path, true, parent);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default async function (props: PageProps<'/[level1]'>) {
|
|
89
|
+
const { slug, path } = await extractDetails(props);
|
|
90
|
+
return generatePage(slug, path, true);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For article routes, use `converterContext.urlCalculators.article`, `articleType`, `articleTypeTag` to compute the path. Import `converterContext` from `@/lib/converter-context` when that module exists (so route files don't pull in cms-server); otherwise from `@/lib/cms-server`.
|
|
95
|
+
|
|
96
|
+
## See Also
|
|
97
|
+
|
|
98
|
+
- **create-page** – Creating new pages and routes
|
|
99
|
+
- **lib-cms-structure** – lib directory layout
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-cms-collection
|
|
3
|
+
description: Guide for creating new CMS-driven collections (component containers) in the SE Core Product monorepo. Use this skill when asked to create a new collection or "grid" component.
|
|
4
|
+
license: Private
|
|
5
|
+
metadata:
|
|
6
|
+
author: se-core-product
|
|
7
|
+
version: "1.1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Creating CMS Collections
|
|
11
|
+
|
|
12
|
+
Collections are components that contain other components (usually referred to as "Cards" in this context). They follow the same **four-layer architecture** as standard components but use specialized helpers.
|
|
13
|
+
|
|
14
|
+
## Architecture Overview
|
|
15
|
+
|
|
16
|
+
Like components, collections **MUST be Server Components** by default. Complex interactions (carousels, filters) must be isolated.
|
|
17
|
+
|
|
18
|
+
1. **Layer 1: Core Collection**
|
|
19
|
+
* Named export: `Core[CollectionName]`
|
|
20
|
+
* Iterates over `contents` array
|
|
21
|
+
* Renders children (Cards)
|
|
22
|
+
* Handles layout (grid, list) - **Server-side only**
|
|
23
|
+
* If a Carousel or Filter is needed, wraps children in a Client Component.
|
|
24
|
+
|
|
25
|
+
2. **Layer 2: Wrapper Collection**
|
|
26
|
+
* Named export: `[CollectionName]`
|
|
27
|
+
* Wraps Core with `<Section>`
|
|
28
|
+
* Handles outer spacing & background
|
|
29
|
+
|
|
30
|
+
3. **Layer 3: Renderer**
|
|
31
|
+
* Default export: `[CollectionName]`
|
|
32
|
+
* Implements `CollectionRenderer`
|
|
33
|
+
* Wraps with `UnusedChecker`
|
|
34
|
+
|
|
35
|
+
4. **Layer 4: Registration**
|
|
36
|
+
* Named export: `[CollectionName]Registration`
|
|
37
|
+
* Uses `defineCollection` (NOT `defineComponent`)
|
|
38
|
+
* Defines `usedFields` for the collection itself
|
|
39
|
+
* Defines `cardUsedFields` for the children components
|
|
40
|
+
|
|
41
|
+
## Client Component Strategy
|
|
42
|
+
|
|
43
|
+
**Rule: Isolate Client Logic (Carousels, Animations, Filters).**
|
|
44
|
+
|
|
45
|
+
Never make the whole collection a Client Component.
|
|
46
|
+
|
|
47
|
+
* **BAD**: `'use client'` on `MyCollection.tsx`.
|
|
48
|
+
* **GOOD**: Create `MyCollectionClient.tsx` (e.g., `<CarouselClient>`) that accepts children.
|
|
49
|
+
|
|
50
|
+
### Pattern: The Client Wrapper
|
|
51
|
+
|
|
52
|
+
For a carousel:
|
|
53
|
+
1. **Server Component** (`CoreMyCollection`): Renders the loop of cards.
|
|
54
|
+
2. **Client Component** (`CarouselClient`): Imports the carousel library (swiper, embla), manages state.
|
|
55
|
+
3. **Usage**: Pass the server-rendered cards as `children` to the client wrapper.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// CoreMyCollection.tsx (Server)
|
|
59
|
+
<CarouselClient>
|
|
60
|
+
{contents.map(item => <Card content={item} />)}
|
|
61
|
+
</CarouselClient>
|
|
62
|
+
|
|
63
|
+
// CarouselClient.tsx (Client)
|
|
64
|
+
'use client';
|
|
65
|
+
export const CarouselClient = ({ children }) => {
|
|
66
|
+
return <Slider>{children}</Slider>; // Client logic here
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This ensures the card content is SEO-ready and server-rendered, while only the slider mechanism is client-side.
|
|
71
|
+
|
|
72
|
+
## Quick Start Template
|
|
73
|
+
|
|
74
|
+
Use this template for new collections. Replace `MyCollection` with your collection name.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import type { IContentContext } from '@se-studio/core-data-types';
|
|
78
|
+
import { isComponent } from '@se-studio/core-data-types';
|
|
79
|
+
import {
|
|
80
|
+
type CmsRendererConfig,
|
|
81
|
+
type CollectionRenderer,
|
|
82
|
+
cn,
|
|
83
|
+
defineCollection,
|
|
84
|
+
getPreviewFieldProps,
|
|
85
|
+
RtfOrString,
|
|
86
|
+
UnusedChecker,
|
|
87
|
+
} from '@se-studio/core-ui';
|
|
88
|
+
import { SectionLinks } from '@/elements/SectionLinks';
|
|
89
|
+
import { Section } from '@/framework/Section';
|
|
90
|
+
import type { ICollection, IComponent, IContent } from '@/lib/cms';
|
|
91
|
+
import { getSizingInformation } from '@/lib/SizingInformation';
|
|
92
|
+
// import MyCollectionClient from './MyCollectionClient'; // If needed
|
|
93
|
+
|
|
94
|
+
// 1. Define fields for the Collection itself
|
|
95
|
+
const FIELD_KEYS = [
|
|
96
|
+
'id',
|
|
97
|
+
'index',
|
|
98
|
+
'heading',
|
|
99
|
+
'body',
|
|
100
|
+
'contents', // The children
|
|
101
|
+
'links',
|
|
102
|
+
'anchor',
|
|
103
|
+
'cmsLabel',
|
|
104
|
+
] as const;
|
|
105
|
+
|
|
106
|
+
type Fields = Pick<ICollection, (typeof FIELD_KEYS)[number]>;
|
|
107
|
+
const USED_FIELDS = new Set([...FIELD_KEYS, 'collectionType']);
|
|
108
|
+
|
|
109
|
+
// 2. Define fields used by the Child Cards
|
|
110
|
+
// (If the children are generic components, this might just be 'id' and 'componentType')
|
|
111
|
+
const CARD_FIELD_KEYS = [
|
|
112
|
+
'id',
|
|
113
|
+
'heading',
|
|
114
|
+
'body',
|
|
115
|
+
'visual',
|
|
116
|
+
'links',
|
|
117
|
+
'cmsLabel',
|
|
118
|
+
] as const;
|
|
119
|
+
const CARD_USED_FIELDS = new Set([...CARD_FIELD_KEYS, 'componentType']);
|
|
120
|
+
|
|
121
|
+
// Helper to render individual cards (use ChildElement for card titles – items inside a collection)
|
|
122
|
+
const CardWrapper: React.FC<{
|
|
123
|
+
content: IComponent;
|
|
124
|
+
index: number;
|
|
125
|
+
contentContext: IContentContext;
|
|
126
|
+
rendererConfig: CmsRendererConfig;
|
|
127
|
+
}> = ({ content, index, contentContext, rendererConfig }) => {
|
|
128
|
+
const { ChildElement, sizingInformation } = getSizingInformation(1);
|
|
129
|
+
return (
|
|
130
|
+
<div className="p-6 bg-white rounded-lg shadow-sm">
|
|
131
|
+
{content.heading && (
|
|
132
|
+
<ChildElement className={cn(sizingInformation.h3, 'mb-2')}>
|
|
133
|
+
{content.heading}
|
|
134
|
+
</ChildElement>
|
|
135
|
+
)}
|
|
136
|
+
{/* ... */}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// 3. Layer 1: Core Collection (Server Component)
|
|
142
|
+
const CoreMyCollection: React.FC<
|
|
143
|
+
Fields & {
|
|
144
|
+
contentContext: IContentContext;
|
|
145
|
+
rendererConfig: CmsRendererConfig;
|
|
146
|
+
positionClassName?: string;
|
|
147
|
+
}
|
|
148
|
+
> = ({
|
|
149
|
+
id,
|
|
150
|
+
index,
|
|
151
|
+
heading,
|
|
152
|
+
body,
|
|
153
|
+
contents,
|
|
154
|
+
links,
|
|
155
|
+
cmsLabel,
|
|
156
|
+
contentContext,
|
|
157
|
+
rendererConfig,
|
|
158
|
+
positionClassName,
|
|
159
|
+
}) => {
|
|
160
|
+
const { Element, sizingInformation } = getSizingInformation(index);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className={cn('space-y-8', positionClassName)}>
|
|
164
|
+
<div className="text-center space-y-4">
|
|
165
|
+
{heading && (
|
|
166
|
+
<Element
|
|
167
|
+
{...getPreviewFieldProps(rendererConfig, id, 'heading')}
|
|
168
|
+
className={cn(sizingInformation.h2)}
|
|
169
|
+
>
|
|
170
|
+
{heading}
|
|
171
|
+
</Element>
|
|
172
|
+
)}
|
|
173
|
+
{body && (
|
|
174
|
+
<RtfOrString
|
|
175
|
+
{...getPreviewFieldProps(rendererConfig, id, 'body')}
|
|
176
|
+
content={body}
|
|
177
|
+
className="rtf-standard"
|
|
178
|
+
rendererConfig={rendererConfig}
|
|
179
|
+
contentContext={contentContext}
|
|
180
|
+
/>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{contents && contents.length > 0 && (
|
|
185
|
+
<ul className="grid grid-cols-1 laptop:grid-cols-3 gap-6">
|
|
186
|
+
{contents.filter(isComponent).map((item, i) => (
|
|
187
|
+
<li key={item.id}>
|
|
188
|
+
<CardWrapper
|
|
189
|
+
content={item}
|
|
190
|
+
index={i}
|
|
191
|
+
contentContext={contentContext}
|
|
192
|
+
rendererConfig={rendererConfig}
|
|
193
|
+
/>
|
|
194
|
+
</li>
|
|
195
|
+
))}
|
|
196
|
+
</ul>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{links && (
|
|
200
|
+
<SectionLinks
|
|
201
|
+
links={links}
|
|
202
|
+
rendererConfig={rendererConfig}
|
|
203
|
+
trackingLocation={cmsLabel}
|
|
204
|
+
analyticsContext={contentContext.analyticsContext}
|
|
205
|
+
className="justify-center"
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// 4. Layer 2: Wrapper Collection
|
|
213
|
+
const MyCollection: React.FC<
|
|
214
|
+
Fields & {
|
|
215
|
+
information: IContent;
|
|
216
|
+
contentContext: IContentContext;
|
|
217
|
+
rendererConfig: CmsRendererConfig;
|
|
218
|
+
}
|
|
219
|
+
> = ({ information, ...rest }) => {
|
|
220
|
+
return (
|
|
221
|
+
<Section
|
|
222
|
+
componentName={information.collectionType}
|
|
223
|
+
information={information}
|
|
224
|
+
previewHelpers={rest.rendererConfig?.previewHelpers}
|
|
225
|
+
>
|
|
226
|
+
<CoreMyCollection
|
|
227
|
+
{...rest}
|
|
228
|
+
positionClassName="col-span-full"
|
|
229
|
+
/>
|
|
230
|
+
</Section>
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// 5. Layer 3: Renderer
|
|
235
|
+
const MyCollectionRenderer: CollectionRenderer<ICollection> = ({
|
|
236
|
+
information,
|
|
237
|
+
contentContext,
|
|
238
|
+
rendererConfig,
|
|
239
|
+
}) => {
|
|
240
|
+
const { collectionType, ...rest } = information;
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<UnusedChecker
|
|
244
|
+
componentName={collectionType}
|
|
245
|
+
allFields={rest}
|
|
246
|
+
usedFields={USED_FIELDS}
|
|
247
|
+
>
|
|
248
|
+
<MyCollection
|
|
249
|
+
information={information}
|
|
250
|
+
contentContext={contentContext}
|
|
251
|
+
rendererConfig={rendererConfig}
|
|
252
|
+
{...(rest as Fields)}
|
|
253
|
+
/>
|
|
254
|
+
</UnusedChecker>
|
|
255
|
+
);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export default MyCollectionRenderer;
|
|
259
|
+
|
|
260
|
+
// 6. Layer 4: Registration
|
|
261
|
+
export const MyCollectionRegistration = defineCollection({
|
|
262
|
+
name: 'My Collection', // Must match CMS collectionType
|
|
263
|
+
renderer: MyCollectionRenderer,
|
|
264
|
+
usedFields: USED_FIELDS,
|
|
265
|
+
cardUsedFields: CARD_USED_FIELDS,
|
|
266
|
+
mock: {
|
|
267
|
+
heading: 'Sample Collection',
|
|
268
|
+
body: 'Collection description text',
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Checklist for Collections
|
|
274
|
+
|
|
275
|
+
1. **Define Two Sets of Fields**:
|
|
276
|
+
* `USED_FIELDS` for the collection container (heading, body, background).
|
|
277
|
+
* `CARD_USED_FIELDS` for the items inside `contents`.
|
|
278
|
+
2. **Use `defineCollection`**:
|
|
279
|
+
* Instead of `defineComponent`.
|
|
280
|
+
* Pass both `usedFields` and `cardUsedFields`.
|
|
281
|
+
3. **Heading hierarchy**:
|
|
282
|
+
* Use **Element** from `getSizingInformation(index)` for the collection's section heading (and a class from `sizingInformation`). Use **ChildElement** from `getSizingInformation(1)` for item/card titles. Render preHeading/postHeading as **<p>** with a typography class, never as heading tags.
|
|
283
|
+
4. **Preview helpers**:
|
|
284
|
+
* Use `getPreviewFieldProps(rendererConfig, id, fieldId)` from `@se-studio/core-ui` for CMS preview (do not import from cms-server – avoids circular dependency). For responsive visuals use `getPreviewResponsiveVisualFieldProps(rendererConfig, id, 'visual', 'mobileVisual')`.
|
|
285
|
+
* For collections that fetch data: use `rendererConfig?.fetchHelpers?.getRelatedArticles?.(information, contentContext, count)` instead of importing from cms-server.
|
|
286
|
+
5. **Handle `contents`**:
|
|
287
|
+
* Check `if (contents && contents.length > 0)`.
|
|
288
|
+
* Filter with `contents.filter(isComponent)`.
|
|
289
|
+
* Map over the result to render items.
|
|
290
|
+
6. **Isolate Client Logic**:
|
|
291
|
+
* **ENSURE SERVER COMPONENT** for the main file.
|
|
292
|
+
* If a carousel/slider/filter is needed, move that logic to `[Name]Client.tsx`.
|
|
293
|
+
* Pass server-rendered cards as `children` to the client wrapper.
|
|
294
|
+
7. **Register**:
|
|
295
|
+
* Add to `collectionRegistrationsList` in `src/lib/registrations.ts`. The `name` is defined only in your `defineCollection(...)` call.
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-cms-component
|
|
3
|
+
description: Guide for creating new CMS-driven components in the SE Core Product monorepo. Use this skill when asked to create a new component, ensuring adherence to the 4-layer architecture.
|
|
4
|
+
license: Private
|
|
5
|
+
metadata:
|
|
6
|
+
author: se-core-product
|
|
7
|
+
version: "1.1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Creating CMS Components
|
|
11
|
+
|
|
12
|
+
This guide explains how to build new CMS-driven components following the established architecture patterns in this codebase.
|
|
13
|
+
|
|
14
|
+
## Architecture Overview
|
|
15
|
+
|
|
16
|
+
All components follow a **four-layer architecture** and must remain **Server Components** by default.
|
|
17
|
+
|
|
18
|
+
1. **Layer 1: Core Component (Pure Presentation)**
|
|
19
|
+
* Named export: `Core[ComponentName]`
|
|
20
|
+
* CMS-agnostic rendering logic
|
|
21
|
+
* Accepts `positionClassName` prop
|
|
22
|
+
* **MUST be a Server Component** (no `'use client'`)
|
|
23
|
+
* If interactivity is needed, import a Client Component here.
|
|
24
|
+
|
|
25
|
+
2. **Layer 2: Wrapper Component (Section Integration)**
|
|
26
|
+
* Named export: `[ComponentName]`
|
|
27
|
+
* Wraps Core with `<Section>`
|
|
28
|
+
* Handles spacing & background
|
|
29
|
+
|
|
30
|
+
3. **Layer 3: Renderer (CMS Integration)**
|
|
31
|
+
* Default export: `[ComponentName]` (short name)
|
|
32
|
+
* Implements `ComponentRenderer`
|
|
33
|
+
* Wraps with `UnusedChecker` for CMS validation
|
|
34
|
+
|
|
35
|
+
4. **Layer 4: Registration (Showcase & CMS)**
|
|
36
|
+
* Named export: `[ComponentName]Registration`
|
|
37
|
+
* Uses `defineComponent` helper
|
|
38
|
+
* Provides metadata & mock data
|
|
39
|
+
|
|
40
|
+
## Client Component Strategy
|
|
41
|
+
|
|
42
|
+
**Rule: Isolate Client Logic.**
|
|
43
|
+
|
|
44
|
+
Never make the entire component a Client Component just for a small interactive part.
|
|
45
|
+
|
|
46
|
+
* **BAD**: Adding `'use client'` to `MyComponent.tsx` (Layers 1-3).
|
|
47
|
+
* **GOOD**: Creating `MyComponentClient.tsx` (or `*Animator.tsx`) with `'use client'` and importing it into `CoreMyComponent`.
|
|
48
|
+
|
|
49
|
+
### Why?
|
|
50
|
+
* **Performance**: Keeps the majority of the HTML generation on the server.
|
|
51
|
+
* **SEO**: Ensures content is fully rendered for crawlers.
|
|
52
|
+
* **Bundle Size**: Only sends necessary JS to the browser.
|
|
53
|
+
|
|
54
|
+
### Pattern: The Client Wrapper
|
|
55
|
+
|
|
56
|
+
If you need state (e.g., filters, toggles) or effects (e.g., animations), create a wrapper:
|
|
57
|
+
|
|
58
|
+
1. Create `MyComponentClient.tsx` (or `MyComponentAnimator.tsx`).
|
|
59
|
+
2. Add `'use client'` at the top.
|
|
60
|
+
3. Accept `children` or raw data props.
|
|
61
|
+
4. Import and use it in `CoreMyComponent`.
|
|
62
|
+
|
|
63
|
+
## Quick Start Template
|
|
64
|
+
|
|
65
|
+
Use this template for new components. Replace `MyNewComponent` with your component name.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import type { IContentContext } from '@se-studio/core-data-types';
|
|
69
|
+
import {
|
|
70
|
+
type CmsRendererConfig,
|
|
71
|
+
type ComponentRenderer,
|
|
72
|
+
convertText,
|
|
73
|
+
cn,
|
|
74
|
+
defineComponent,
|
|
75
|
+
getPreviewFieldProps,
|
|
76
|
+
RtfOrString,
|
|
77
|
+
UnusedChecker,
|
|
78
|
+
} from '@se-studio/core-ui';
|
|
79
|
+
import { SectionLinks } from '@/elements/SectionLinks';
|
|
80
|
+
import { Section } from '@/framework/Section';
|
|
81
|
+
import type { IComponent, IContent } from '@/lib/cms';
|
|
82
|
+
import { getSizingInformation } from '@/lib/SizingInformation';
|
|
83
|
+
// If interactivity is needed:
|
|
84
|
+
// import MyNewComponentClient from './MyNewComponentClient';
|
|
85
|
+
|
|
86
|
+
// 1. Define which CMS fields this component uses
|
|
87
|
+
const FIELD_KEYS = [
|
|
88
|
+
'id',
|
|
89
|
+
'index',
|
|
90
|
+
'heading',
|
|
91
|
+
'body',
|
|
92
|
+
'links',
|
|
93
|
+
'anchor',
|
|
94
|
+
'cmsLabel',
|
|
95
|
+
] as const;
|
|
96
|
+
|
|
97
|
+
type Fields = Pick<IComponent, (typeof FIELD_KEYS)[number]>;
|
|
98
|
+
|
|
99
|
+
const USED_FIELDS = new Set([...FIELD_KEYS, 'componentType']);
|
|
100
|
+
|
|
101
|
+
// 2. Layer 1: Core Component (Pure presentation, Server Component)
|
|
102
|
+
export const CoreMyNewComponent: React.FC<
|
|
103
|
+
Fields & {
|
|
104
|
+
contentContext?: IContentContext;
|
|
105
|
+
rendererConfig?: CmsRendererConfig;
|
|
106
|
+
embedded?: boolean;
|
|
107
|
+
positionClassName?: string;
|
|
108
|
+
}
|
|
109
|
+
> = ({
|
|
110
|
+
id,
|
|
111
|
+
index,
|
|
112
|
+
heading,
|
|
113
|
+
body,
|
|
114
|
+
links,
|
|
115
|
+
cmsLabel,
|
|
116
|
+
contentContext,
|
|
117
|
+
rendererConfig,
|
|
118
|
+
positionClassName,
|
|
119
|
+
}) => {
|
|
120
|
+
const { Element, sizingInformation } = getSizingInformation(index);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className={cn('space-y-6', positionClassName)}>
|
|
124
|
+
{heading && (
|
|
125
|
+
<Element
|
|
126
|
+
{...getPreviewFieldProps(rendererConfig, id, 'heading')}
|
|
127
|
+
className={cn(sizingInformation.h1)}
|
|
128
|
+
>
|
|
129
|
+
{convertText(heading)}
|
|
130
|
+
</Element>
|
|
131
|
+
)}
|
|
132
|
+
{body && (
|
|
133
|
+
<RtfOrString
|
|
134
|
+
{...getPreviewFieldProps(rendererConfig, id, 'body')}
|
|
135
|
+
content={body}
|
|
136
|
+
className={cn('rtf-standard')}
|
|
137
|
+
rendererConfig={rendererConfig}
|
|
138
|
+
contentContext={contentContext}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
{/* Example of isolated client logic usage: */}
|
|
142
|
+
{/* <MyNewComponentClient>
|
|
143
|
+
<InteractivePart />
|
|
144
|
+
</MyNewComponentClient>
|
|
145
|
+
*/}
|
|
146
|
+
{links && (
|
|
147
|
+
<SectionLinks
|
|
148
|
+
links={links}
|
|
149
|
+
rendererConfig={rendererConfig}
|
|
150
|
+
trackingLocation={cmsLabel}
|
|
151
|
+
analyticsContext={contentContext?.analyticsContext}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// 3. Layer 2: Wrapper Component (Section integration)
|
|
159
|
+
const MyNewComponent: React.FC<
|
|
160
|
+
Fields &
|
|
161
|
+
Pick<IComponent, 'componentType'> & {
|
|
162
|
+
contentContext: IContentContext;
|
|
163
|
+
information: IContent;
|
|
164
|
+
rendererConfig: CmsRendererConfig;
|
|
165
|
+
}
|
|
166
|
+
> = ({ information, componentType, ...rest }) => {
|
|
167
|
+
return (
|
|
168
|
+
<Section
|
|
169
|
+
componentName={componentType}
|
|
170
|
+
information={information}
|
|
171
|
+
previewHelpers={rest.rendererConfig?.previewHelpers}
|
|
172
|
+
>
|
|
173
|
+
<CoreMyNewComponent
|
|
174
|
+
{...rest}
|
|
175
|
+
positionClassName="col-span-full laptop:col-start-2 laptop:col-span-10"
|
|
176
|
+
/>
|
|
177
|
+
</Section>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// 4. Layer 3: Renderer Export (CMS integration)
|
|
182
|
+
const MyNew: ComponentRenderer<IComponent> = ({
|
|
183
|
+
information,
|
|
184
|
+
contentContext,
|
|
185
|
+
rendererConfig,
|
|
186
|
+
}) => {
|
|
187
|
+
const { componentType, ...rest } = information;
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<UnusedChecker componentName={componentType} allFields={rest} usedFields={USED_FIELDS}>
|
|
191
|
+
<MyNewComponent
|
|
192
|
+
rendererConfig={rendererConfig}
|
|
193
|
+
contentContext={contentContext}
|
|
194
|
+
information={information}
|
|
195
|
+
componentType={componentType}
|
|
196
|
+
{...rest}
|
|
197
|
+
/>
|
|
198
|
+
</UnusedChecker>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export default MyNew;
|
|
203
|
+
|
|
204
|
+
// 5. Layer 4: Registration
|
|
205
|
+
export const MyNewRegistration = defineComponent({
|
|
206
|
+
name: 'My New Component', // Must match CMS componentType
|
|
207
|
+
renderer: MyNew,
|
|
208
|
+
usedFields: USED_FIELDS,
|
|
209
|
+
mock: {
|
|
210
|
+
heading: 'Sample Heading',
|
|
211
|
+
body: 'This is sample body text for the showcase.',
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Implementation Checklist
|
|
217
|
+
|
|
218
|
+
1. **Create file**: `src/project/components/MyNewComponent.tsx`
|
|
219
|
+
2. **Define Fields**:
|
|
220
|
+
* Set `FIELD_KEYS` with all used fields.
|
|
221
|
+
* Create `Fields` type.
|
|
222
|
+
* Create `USED_FIELDS` set (don't forget `'componentType'`).
|
|
223
|
+
* Include standard fields: `id`, `index`, `anchor`, `cmsLabel`.
|
|
224
|
+
3. **Build Core Component**:
|
|
225
|
+
* **ENSURE SERVER COMPONENT**: Do not use `'use client'`.
|
|
226
|
+
* Export named `Core[Name]`.
|
|
227
|
+
* Accept `positionClassName`.
|
|
228
|
+
* Use `getSizingInformation(index)` for semantic headings.
|
|
229
|
+
* Check field existence (`field &&`).
|
|
230
|
+
* Use `convertText()` for strings.
|
|
231
|
+
* Use `<RtfOrString>` for rich text.
|
|
232
|
+
* Use `getPreviewFieldProps(rendererConfig, id, fieldId)` from `@se-studio/core-ui` for CMS preview (do not import from cms-server – avoids circular dependency). For responsive visuals use `getPreviewResponsiveVisualFieldProps(rendererConfig, id, 'visual', 'mobileVisual')`.
|
|
233
|
+
4. **Handle Interactivity (If Needed)**:
|
|
234
|
+
* Create separate `[Name]Client.tsx` or `[Name]Animator.tsx`.
|
|
235
|
+
* Add `'use client'` to that file only.
|
|
236
|
+
* Import and use in Core component.
|
|
237
|
+
5. **Build Wrapper Component**:
|
|
238
|
+
* Wrap Core with `<Section>`.
|
|
239
|
+
* Pass `componentName={componentType}` and `information`.
|
|
240
|
+
* Set layout (e.g., `positionClassName="col-span-full"`).
|
|
241
|
+
6. **Build Renderer**:
|
|
242
|
+
* Default export (short name).
|
|
243
|
+
* Wrap with `<UnusedChecker>`.
|
|
244
|
+
7. **Define Registration**:
|
|
245
|
+
* Export named `[Name]Registration`.
|
|
246
|
+
* Use `defineComponent`.
|
|
247
|
+
* **CRITICAL**: `name` must match CMS `componentType` exactly.
|
|
248
|
+
* Provide mock data for showcase.
|
|
249
|
+
8. **Register Component**:
|
|
250
|
+
* Add to `componentRegistrationsList` in `src/lib/registrations.ts`. The `name` is defined only in your `defineComponent(...)` call; do not duplicate it as a Record key.
|