@sudajs/cli 0.10.3 → 0.11.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sudajs/cli",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "suda": "./bin/suda.js"
@@ -34,7 +34,7 @@
34
34
  "react": "^19.2.7",
35
35
  "react-dom": "^19.2.7",
36
36
  "zod": "^3.24.1",
37
- "@sudajs/theme-engine": "2.6.1"
37
+ "@sudajs/theme-engine": "3.0.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@tailwindcss/postcss": "^4.3.0",
@@ -49,8 +49,8 @@
49
49
  "typescript": "^5.7.2",
50
50
  "vite": "^7.3.3",
51
51
  "vitest": "^3.2.4",
52
- "@suda/build-config": "0.0.0",
53
52
  "@suda/eslint-config": "0.0.0",
53
+ "@suda/build-config": "0.0.0",
54
54
  "@suda/tsconfig": "0.0.0"
55
55
  },
56
56
  "scripts": {
@@ -4,17 +4,344 @@ This directory is a standalone Vite + React + Tailwind Suda theme generated by `
4
4
 
5
5
  Your job in a theme project is to create a complete, publishable theme with theme-specific components, realistic starter pages, scoped styling, assets, and AI metadata. Do not build pages by assembling generic engine components; those are reserved for end users who need supplemental editing blocks after the theme is installed.
6
6
 
7
+ ## Theme-building workflow
8
+
9
+ When you add or change a section, complete the whole theme authoring loop in
10
+ one pass:
11
+
12
+ 1. Define the props type. Use serializable values only. Use `Slot` only for
13
+ props that are backed by `blockSlots`.
14
+ 2. Export a `SudaComponentConfig<Props>` with `label`, `ai.instructions`,
15
+ `fields`, optional `blockSlots`, `defaultProps`, and `render`.
16
+ 3. Keep `fields` and `defaultProps` aligned. Every normal field should have a
17
+ sensible value in `defaultProps`; do not put block-slot props in
18
+ `defaultProps`. Do not put `id` in component `defaultProps`; `id` belongs
19
+ only to concrete top-level page/layout content items.
20
+ 4. Add every editor-facing label key to `src/locales/en.json` when you add the
21
+ component, field, option, or local block.
22
+ 5. Register the component in `src/config.ts` under `pageConfig.components`.
23
+ Register only shared chrome components such as `Header`, `Footer`, and
24
+ `PageOutlet` under `layoutConfig.components`.
25
+ 6. Use the component in starter pages and CMS templates with public component
26
+ keys from `pageConfig.components`.
27
+ 7. Wrap authored page data with `defineSudaPageData(pageConfig, data)` whenever
28
+ practical so TypeScript checks top-level component types and block-slot
29
+ nested `type` values.
30
+ 8. Run `pnpm typecheck`, `pnpm lint`, `pnpm build`, and `pnpm validate` before
31
+ handoff.
32
+
33
+ Configure ordinary fields like this:
34
+
35
+ ```ts
36
+ type HeroProps = {
37
+ eyebrow?: string;
38
+ title?: string;
39
+ body?: string;
40
+ align?: "left" | "center";
41
+ featured?: boolean;
42
+ };
43
+
44
+ export const Hero: SudaComponentConfig<HeroProps> = {
45
+ label: t("sections.hero.label"),
46
+ ai: {
47
+ instructions:
48
+ "Landing page hero section. Configure concise copy and a layout choice.",
49
+ },
50
+ fields: {
51
+ eyebrow: { type: "text", label: t("sections.hero.fields.eyebrow") },
52
+ title: { type: "text", label: t("sections.hero.fields.title") },
53
+ body: { type: "textarea", label: t("sections.hero.fields.body") },
54
+ align: {
55
+ type: "select",
56
+ label: t("sections.hero.fields.align"),
57
+ options: [
58
+ { label: t("common.options.left"), value: "left" },
59
+ { label: t("common.options.center"), value: "center" },
60
+ ],
61
+ },
62
+ featured: { type: "checkbox", label: t("sections.hero.fields.featured") },
63
+ },
64
+ defaultProps: {
65
+ eyebrow: "New",
66
+ title: "Welcome",
67
+ body: "Describe the main offer in one or two sentences.",
68
+ align: "left",
69
+ featured: true,
70
+ },
71
+ render: ({ eyebrow, title, body, align, featured }) => (
72
+ <section data-align={align} data-featured={featured ? "true" : "false"}>
73
+ {eyebrow ? <p>{eyebrow}</p> : null}
74
+ <h1>{title}</h1>
75
+ {body ? <p>{body}</p> : null}
76
+ </section>
77
+ ),
78
+ };
79
+ ```
80
+
81
+ Use normal fields for scalar section settings such as text, textarea, number,
82
+ range, checkbox, select/radio options, media, URL, icon, color, object, and
83
+ array values. Put those field values in the host component's `defaultProps`.
84
+ Use `blockSlots` only when the section owns nested local blocks that editors
85
+ and AI should add, remove, or reorder independently.
86
+
87
+ Field source of truth:
88
+
89
+ - Use TypeScript, not a hand-written runtime schema. Type props first, then let
90
+ `SudaComponentConfig<Props>`, `SudaFields<Props>`, `SudaField`, and
91
+ `defineSudaPageData(pageConfig, data)` catch mismatches.
92
+ - There is no theme-authored zod schema for fields. Do not invent one in the
93
+ theme. Run `pnpm typecheck` and `pnpm validate` instead.
94
+ - Every field entry needs a `type`. Add a localized `label` for every
95
+ editor-facing field.
96
+ - Put a matching serializable value in `defaultProps` for every normal field.
97
+ Optional props can still have defaults; defaults make editor insertion and AI
98
+ generation reliable.
99
+ - Use `description`, `placeholder`, and `visibleIf` only when they help the
100
+ editor or AI choose the right value.
101
+ - Suda extended field types are `url`, `icon`, `font`, `menu`, `color`,
102
+ `range`, `spacing`, `media`, `image`, `video`, and `posts`. These are valid
103
+ in `fields` and are normalized by `@sudajs/theme-engine`.
104
+
105
+ Use these field patterns:
106
+
107
+ ```ts
108
+ fields: {
109
+ // Plain copy
110
+ title: { type: "text", label: t("common.fields.title") },
111
+ body: { type: "textarea", label: t("common.fields.description") },
112
+ richBody: { type: "richtext", label: t("common.fields.content") },
113
+
114
+ // Numbers and booleans
115
+ columns: { type: "number", label: t("common.fields.columns") },
116
+ featured: { type: "checkbox", label: t("common.fields.featured") },
117
+ opacity: {
118
+ type: "range",
119
+ label: t("common.fields.opacity"),
120
+ min: 0,
121
+ max: 100,
122
+ step: 5,
123
+ unit: "%",
124
+ },
125
+
126
+ // Choices: always provide options with localized labels and serializable values.
127
+ align: {
128
+ type: "radio",
129
+ label: t("common.fields.align"),
130
+ options: [
131
+ { label: t("common.options.left"), value: "left" },
132
+ { label: t("common.options.center"), value: "center" },
133
+ ],
134
+ },
135
+ tone: {
136
+ type: "select",
137
+ label: t("common.fields.tone"),
138
+ options: [
139
+ { label: t("common.options.light"), value: "light" },
140
+ { label: t("common.options.dark"), value: "dark" },
141
+ ],
142
+ },
143
+
144
+ // Suda extended fields
145
+ href: { type: "url", label: t("common.fields.link") },
146
+ image: { type: "image", label: t("common.fields.image") },
147
+ video: { type: "video", label: t("common.fields.video") },
148
+ download: { type: "media", kind: "file", label: t("common.fields.file") },
149
+ icon: { type: "icon", label: t("common.fields.icon") },
150
+ accentColor: { type: "color", label: t("common.fields.color") },
151
+ font: { type: "font", label: t("common.fields.font") },
152
+ links: { type: "menu", label: t("common.fields.menu") },
153
+ spacing: { type: "spacing", label: t("common.fields.spacing") },
154
+
155
+ // Dynamic post lists. Use this only on page sections that intentionally
156
+ // render post resources, not on fixed CMS main sections.
157
+ postList: { type: "posts", label: t("common.fields.posts") },
158
+
159
+ // Objects and arrays need nested typed fields.
160
+ badge: {
161
+ type: "object",
162
+ label: t("common.fields.badge"),
163
+ objectFields: {
164
+ label: { type: "text", label: t("common.fields.label") },
165
+ tone: { type: "text", label: t("common.fields.tone") },
166
+ },
167
+ },
168
+ cards: {
169
+ type: "array",
170
+ label: t("common.fields.cards"),
171
+ getItemSummary: (item) => item.title || "Card",
172
+ defaultItemProps: { title: "Card title", body: "Card body" },
173
+ arrayFields: {
174
+ title: { type: "text", label: t("common.fields.title") },
175
+ body: { type: "textarea", label: t("common.fields.description") },
176
+ href: { type: "url", label: t("common.fields.link") },
177
+ },
178
+ },
179
+ }
180
+ ```
181
+
182
+ Match those fields with defaults:
183
+
184
+ ```ts
185
+ defaultProps: {
186
+ title: "Welcome",
187
+ body: "Describe the offer.",
188
+ richBody: "<p>Long-form content.</p>",
189
+ columns: 3,
190
+ featured: true,
191
+ opacity: 80,
192
+ align: "left",
193
+ tone: "light",
194
+ href: "/contact",
195
+ image: themeAsset("assets/hero.jpg"),
196
+ video: "",
197
+ download: "",
198
+ icon: "sparkles",
199
+ accentColor: "#2563eb",
200
+ font: "system",
201
+ links: [{ label: "Home", url: "/" }],
202
+ spacing: "md",
203
+ postList: { strategy: "featured", limit: 3 },
204
+ badge: { label: "New", tone: "primary" },
205
+ cards: [{ title: "Fast setup", body: "Launch quickly.", href: "/features" }],
206
+ }
207
+ ```
208
+
209
+ Starter page and CMS template data must use this shape:
210
+
211
+ ```ts
212
+ defineSudaPageData(pageConfig, {
213
+ root: { props: {} },
214
+ content: [
215
+ {
216
+ type: "Hero",
217
+ props: {
218
+ id: "Hero-1",
219
+ title: "Welcome",
220
+ actions: [
221
+ {
222
+ type: "button",
223
+ props: { label: "Contact us", href: "/contact" },
224
+ },
225
+ ],
226
+ },
227
+ },
228
+ ],
229
+ });
230
+ ```
231
+
232
+ Top-level `content[].type` must be a key registered in `pageConfig.components`.
233
+ Top-level `content[].props.id` is required in starter pages, CMS templates, and
234
+ layout data. Do not put `id` in component `defaultProps`, local block
235
+ `defaultProps`, `defaultBlocks`, or block-slot nested items.
236
+ Nested block-slot items must use short local block kind names such as
237
+ `"button"`, `"copy"`, or `"cards"`. Never write
238
+ `__suda_local_block__/...` in source templates, starter pages, CMS templates,
239
+ or AI examples.
240
+
7
241
  ## Theme contract
8
242
 
9
243
  - Keep `renderMode: "ssr"` in `src/manifest.ts`.
10
244
  - Keep source entries at `src/index.tsx`, `src/runtime.client.ts`, and `src/styles.css`.
11
- - Export a complete `ThemeModule` from `src/index.tsx`: `manifest`, `pageConfig`, `layoutConfig`, `defaultLayout`, and `starterPages`.
12
- - Starter page slugs preview at site-like root routes in `suda theme dev`: `home` is `/home`, `contact-us` is `/contact-us`. Do not use `/pages/...` as a preview route prefix.
245
+ - Export a complete `ThemeModule` from `src/index.tsx`: `manifest`, `pageConfig`, `layoutConfig`, `defaultLayout`, `starterPages`, and `cmsTemplates`.
246
+ - Starter page slugs preview at site-like root routes in `suda theme dev`: `index` is `/index`, `contact-us` is `/contact-us`. Do not use `/pages/...` as a preview route prefix.
13
247
  - Keep `pageConfig` focused on page content sections. Keep `layoutConfig` focused on shared site chrome such as root, header, page outlet, and footer.
14
248
  - Do not edit generated files under `dist/`; run `pnpm build` to regenerate them.
15
249
  - React, React DOM, Puck, and `@sudajs/theme-engine` are host-provided peers. Do not bundle private copies into the theme runtime.
16
250
  - Keep persisted props JSON-serializable. Do not store functions, React nodes, class instances, database ids for media, or environment-specific absolute filesystem paths.
17
251
 
252
+ ## CMS and starter templates
253
+
254
+ Suda CMS is a built-in fixed post system. Themes must provide exactly these
255
+ four CMS templates through `cmsTemplates`:
256
+
257
+ - `posts`: post list template for `/posts`
258
+ - `post`: post detail template for `/posts/:slug`
259
+ - `tags`: tag/category list template for `/tags`
260
+ - `tag`: posts filtered by one tag for `/tags/:slug`
261
+
262
+ Each `cmsTemplates` value must be an object with `{ title: string; data:
263
+ PageData }`. Do not point a key directly at raw `PageData`, and do not use
264
+ intersection types such as `ThemeModule & { cmsTemplates:
265
+ Record<string, PageData> }` to bypass the current contract.
266
+
267
+ ```ts
268
+ export const cmsTemplates: ThemeCmsTemplates = {
269
+ posts: { title: "Posts", data: postIndexTemplate },
270
+ post: { title: "Post", data: postDetailTemplate },
271
+ tags: { title: "Tags", data: tagIndexTemplate },
272
+ tag: { title: "Tag", data: tagDetailTemplate },
273
+ };
274
+ ```
275
+
276
+ Do not create Shopify-style template variants such as `post.default`,
277
+ `post.modern`, or `posts.magazine`. Do not add arbitrary collection/content
278
+ types or manual data-source binding contracts. Keep CMS-only sections limited
279
+ to the CMS templates where they belong.
280
+
281
+ Create CMS template main sections like this:
282
+
283
+ - `MainPosts`: place only on `posts` and optionally `tag` templates with
284
+ `placement: { cmsTemplates: ["posts", "tag"] }`; render `cms.posts`
285
+ directly and use `cms.pagination` for pagination controls. Use
286
+ `cms.currentTag` when a tag filter is active.
287
+ - `MainPost`: place only on the `post` template; render the current article
288
+ when `cms.type === "post"`.
289
+ - `MainTags`: place only on the `tags` template; render tag/category cards
290
+ when `cms.type === "tags"`.
291
+ - `MainTagPosts` can be a separate tag-page list section, or `MainPosts` can
292
+ handle both `posts` and `tag` contexts. Choose the clearer API for the theme.
293
+
294
+ In CMS template main sections, call `getCmsContent(puck?.metadata)` and render
295
+ the matching CMS data. Do not add `type: "posts"` fields to those sections.
296
+
297
+ For pagination controls, use `cms.pagination.page`,
298
+ `cms.pagination.totalPages`, `cms.pagination.hasPreviousPage`, and
299
+ `cms.pagination.hasNextPage` to decide what to show. Do not hand-build
300
+ pagination query strings. Import `cmsPaginationUrl` from
301
+ `@sudajs/theme-engine/runtime` and call
302
+ `cmsPaginationUrl(cms.pagination, targetPage)` for numbered links so existing
303
+ filter/search parameters stay in the URL. Use `cms.pagination.previousUrl` and
304
+ `cms.pagination.nextUrl` for previous/next buttons when present.
305
+
306
+ Themes must also provide at least one reusable ordinary-page post section, for
307
+ example `FeaturedPosts`, `LatestPosts`, or `TopicPosts`. Use it for homepage
308
+ insights, featured articles, case studies, news teasers, or tag-filtered post
309
+ groups on ordinary pages.
310
+
311
+ Define that section with exactly one top-level `type: "posts"` field and a
312
+ default query on the same prop:
313
+
314
+ ```tsx
315
+ fields: {
316
+ postList: { type: "posts" },
317
+ },
318
+ defaultProps: {
319
+ postList: { strategy: "featured", limit: 3 },
320
+ },
321
+ ```
322
+
323
+ Supported strategies are `latest`, `featured`, `by_tag`, and `manual`. In the
324
+ section render function, call `getPostResource(puck?.metadata, id)` to get the
325
+ posts. Never import Prisma, call platform APIs, or query the database from
326
+ theme code.
327
+
328
+ Starter pages must include one home/index page with `isHome: true`. Recommended
329
+ starter pages include `index`, `about-us`, `contact-us`, `services`, and `team`.
330
+ Those recommended pages are not hard requirements, but a publishable theme
331
+ should usually include several realistic pages so AI site generation has a rich
332
+ reference set.
333
+
334
+ Treat repetition as a design and business judgment, not a fixed rule. Let the
335
+ site strategy decide where pages should share patterns and where they should
336
+ diverge. Reusing strong sections across starter pages is often right: CTA,
337
+ contact, booking, newsletter, download, and other conversion sections may appear
338
+ repeatedly when they help visitors move forward. Avoid only mechanical
339
+ copy-paste that makes different pages feel interchangeable. When useful, give
340
+ each page its own purpose, angle, proof points, media, or ordering, while
341
+ keeping repeated business-driving sections where they serve the user journey.
342
+ Richer starter pages make the theme easier for AI to adapt without making users
343
+ learn CMS/template mechanics.
344
+
18
345
  ## Editor i18n and locales
19
346
 
20
347
  Theme editor labels are localized by the host editor. Use the template's
@@ -156,14 +483,20 @@ Good component instructions explain:
156
483
 
157
484
  ## Block slot authoring rules
158
485
 
159
- Use `blockSlots` when a section owns controlled local content that benefits from independent editor selection and configuration, such as hero actions, pricing cards, feature rows, stats, timeline items, or contact methods. Keep ordinary section props in `fields`.
486
+ Use `blockSlots` when a section owns controlled local content such as hero
487
+ actions, pricing cards, feature rows, stats, timeline items, or contact
488
+ methods. Keep ordinary section props in `fields`.
160
489
 
161
- - Type the slot prop as Puck `Slot` and declare `blockSlots` with the same prop key. This is the Suda block-slot API; do not create a native `fields.<key>.type = "slot"` yourself.
162
- - Define local block kinds under `blockSlots.<slotName>.blocks`. Local blocks support normal Suda fields, `label`, `defaultProps`, `render`, `metadata`, `inline`, `permissions`, and optional `ai`.
490
+ - Type the slot prop as Puck `Slot` and declare `blockSlots` with the same prop key.
491
+ - Do not create a native `fields.<key>.type = "slot"` in page components.
492
+ - Define local block kinds under `blockSlots.<slotName>.blocks`.
493
+ - Give every local block `label`, useful fields, `defaultProps`, `render`, and
494
+ optional `ai.instructions` when the block needs generation guidance.
163
495
  - Keep local blocks specific to the owning section. They are not reusable global page components.
164
- - Do not add nested `blockSlots` or `cms` to local blocks. Block slots are one level deep in the theme authoring API.
496
+ - Do not add nested `blockSlots` to local blocks. Block slots are one level deep in the theme authoring API.
165
497
  - Do not put the slot prop in the host component's `defaultProps`; use `defaultBlocks` on the slot instead.
166
498
  - Do not hand-write `id` in local block `defaultProps`, `defaultBlocks[].props`, or starter page nested block props. The editor/runtime owns block ids.
499
+ - In authored page data, starter pages, CMS templates, and AI examples, nested block-slot items must use the short local block kind as `type`, exactly matching a key in `blockSlots.<slotName>.blocks`. Never write internal local block types such as `__suda_local_block__/Hero/actions/button`; `@sudajs/theme-engine` generates those only for Puck internals.
167
500
 
168
501
  Example:
169
502
 
@@ -171,29 +504,35 @@ Example:
171
504
  import type { Slot } from "@puckeditor/core";
172
505
  import type { SudaComponentConfig } from "@sudajs/theme-engine";
173
506
 
507
+ import { t } from "./i18n.js";
508
+
174
509
  type HeroProps = {
175
510
  title?: string;
176
511
  actions?: Slot;
177
512
  };
178
513
 
179
514
  export const Hero: SudaComponentConfig<HeroProps> = {
180
- label: "Hero",
515
+ label: t("sections.hero.label"),
181
516
  ai: {
182
517
  instructions:
183
518
  "Primary page introduction for landing pages. Use once near the top with one or two local action blocks.",
184
519
  },
185
520
  fields: {
186
- title: { type: "text", label: "Title" },
521
+ title: { type: "text", label: t("sections.hero.fields.title") },
187
522
  },
188
523
  blockSlots: {
189
524
  actions: {
190
- label: "Actions",
525
+ label: t("sections.hero.blocks.actions"),
191
526
  blocks: {
192
527
  button: {
193
- label: "Button",
528
+ label: t("sections.hero.blocks.button"),
529
+ ai: {
530
+ instructions:
531
+ "Primary or secondary hero action. Use one or two buttons with clear labels and valid links.",
532
+ },
194
533
  fields: {
195
- label: { type: "text", label: "Label" },
196
- href: { type: "url", label: "Link" },
534
+ label: { type: "text", label: t("sections.hero.blocks.buttonLabel") },
535
+ href: { type: "url", label: t("sections.hero.blocks.buttonHref") },
197
536
  },
198
537
  defaultProps: {
199
538
  label: "Get started",
@@ -217,7 +556,7 @@ export const Hero: SudaComponentConfig<HeroProps> = {
217
556
  };
218
557
  ```
219
558
 
220
- Starter page data should use the public host component type and a standard array on the block-slot prop. Use local block `kind` values as nested `type` values.
559
+ Starter page data should use the public host component type and a standard array on the block-slot prop. Use local block `kind` values as nested `type` values; for the example above, `actions[].type` can only be `"button"`.
221
560
 
222
561
  ```ts
223
562
  {
@@ -244,17 +583,12 @@ Starter page data should use the public host component type and a standard array
244
583
  - Every starter page is Puck `Data` with `root: { props: {} }` and `content: [...]`.
245
584
  - Each top-level component instance in starter page `content` must include a stable `props.id`.
246
585
  - Use only components registered in `pageConfig.components` for top-level page content.
586
+ - For any `blockSlots` prop, use only short local block kinds as nested `type` values, never `__suda_local_block__/...` internal types.
587
+ - Do not put local block `id` values in starter pages. Only top-level page components need `props.id`.
588
+ - Where practical, wrap authored data with `defineSudaPageData(pageConfig, data)` so TypeScript checks top-level component keys and block-slot nested `type` values.
247
589
  - Match starter page content to the theme's intended audience and category. The home page should show the theme's best composition, not just every component in order.
248
590
  - Use `themeAsset("assets/...")` for bundled starter media.
249
591
 
250
- ## CMS-aware themes
251
-
252
- - If the theme supports posts or tags, declare `content.views` on the exported `ThemeModule`.
253
- - Add one starter page with matching `cmsView` for every declared CMS view.
254
- - Declare component-level `cms` capabilities only on components that can render that CMS source.
255
- - Use `cmsBinding` in starter page props when a component should receive CMS data at render time.
256
- - Keep static fallback props for CMS components so the page still renders meaningfully when CMS payloads are absent.
257
-
258
592
  ## Commands
259
593
 
260
594
  ```bash
@@ -2,7 +2,7 @@ import type { ThemeManifest, ThemeModule } from "@sudajs/theme-engine";
2
2
 
3
3
  import { defaultLayout, layoutConfig, pageConfig } from "./config.js";
4
4
  import { sourceManifest } from "./manifest.js";
5
- import { starterPages } from "./templates.js";
5
+ import { cmsTemplates, starterPages } from "./templates.js";
6
6
 
7
7
  // Single source of truth: theme version comes from package.json#version,
8
8
  // injected by Vite as a compile-time constant so the published bundle stays
@@ -15,7 +15,8 @@ const theme: ThemeModule = {
15
15
  layoutConfig,
16
16
  defaultLayout,
17
17
  starterPages,
18
+ cmsTemplates,
18
19
  };
19
20
 
20
21
  export default theme;
21
- export { pageConfig, layoutConfig, defaultLayout, starterPages };
22
+ export { pageConfig, layoutConfig, defaultLayout, starterPages, cmsTemplates };
@@ -48,6 +48,14 @@
48
48
  "featureDescription": "Description"
49
49
  }
50
50
  },
51
+ "featuredPosts": {
52
+ "label": "Featured posts",
53
+ "fields": {
54
+ "title": "Title",
55
+ "description": "Description",
56
+ "postList": "Post list"
57
+ }
58
+ },
51
59
  "testimonial": {
52
60
  "label": "Testimonial",
53
61
  "fields": {
@@ -1,5 +1,11 @@
1
1
  import type { SudaComponentConfig, SudaLucideIconName } from "@sudajs/theme-engine";
2
- import { resolveAsset, SudaLucideIcon, type ThemeRenderMetadata } from "@sudajs/theme-engine/runtime";
2
+ import {
3
+ getPostResource,
4
+ resolveAsset,
5
+ SudaLucideIcon,
6
+ type ThemePostResourceQuery,
7
+ type ThemeRenderMetadata,
8
+ } from "@sudajs/theme-engine/runtime";
3
9
 
4
10
  import { t } from "./i18n.js";
5
11
  import { themeAsset } from "./theme-asset.js";
@@ -32,6 +38,12 @@ type CallToActionProps = {
32
38
  buttonLabel?: string;
33
39
  buttonHref?: string;
34
40
  };
41
+ type FeaturedPostsProps = {
42
+ id?: string;
43
+ title?: string;
44
+ description?: string;
45
+ postList?: ThemePostResourceQuery;
46
+ } & PuckExtras;
35
47
 
36
48
  export const Hero: SudaComponentConfig<HeroProps> = {
37
49
  label: t("sections.hero.label"),
@@ -209,4 +221,60 @@ export const CallToAction: SudaComponentConfig<CallToActionProps> = {
209
221
  ),
210
222
  };
211
223
 
212
- export const SECTION_COMPONENTS = { Hero, FeatureGrid, Testimonial, CallToAction };
224
+ export const FeaturedPosts: SudaComponentConfig<FeaturedPostsProps> = {
225
+ label: t("sections.featuredPosts.label"),
226
+ ai: {
227
+ instructions:
228
+ "Reusable post resource section for ordinary pages such as home, services, cases, or insights teasers. " +
229
+ "Use it when a page should show dynamic CMS posts such as latest articles, featured posts, or posts from a specific tag. " +
230
+ "Set the posts field prop; do not generate static article cards.",
231
+ },
232
+ fields: {
233
+ title: { type: "text", label: t("sections.featuredPosts.fields.title") },
234
+ description: { type: "textarea", label: t("sections.featuredPosts.fields.description") },
235
+ postList: { type: "posts", label: t("sections.featuredPosts.fields.postList") },
236
+ },
237
+ defaultProps: {
238
+ title: "Featured insights",
239
+ description: "Highlight recent or strategically important posts from the CMS.",
240
+ postList: { strategy: "featured", limit: 3 },
241
+ },
242
+ render: ({ id, title, description, puck }) => {
243
+ const resource = getPostResource(puck?.metadata, id);
244
+ const posts = resource.posts;
245
+ return (
246
+ <section className="__SUDA_THEME_KEY__-section">
247
+ <h2>{title}</h2>
248
+ <p>{description}</p>
249
+ <div className="__SUDA_THEME_KEY__-grid" style={{ "--__SUDA_THEME_KEY__-columns": 3 }}>
250
+ {posts.length > 0 ? (
251
+ posts.map((post) => (
252
+ <article className="__SUDA_THEME_KEY__-card" key={post.id}>
253
+ <p className="__SUDA_THEME_KEY__-eyebrow">
254
+ {post.tags[0]?.name ?? "Post"}
255
+ </p>
256
+ <h3>{post.title}</h3>
257
+ <p>{post.excerpt}</p>
258
+ <a href={post.url}>{post.title}</a>
259
+ </article>
260
+ ))
261
+ ) : (
262
+ <article className="__SUDA_THEME_KEY__-card">
263
+ <p className="__SUDA_THEME_KEY__-eyebrow">CMS</p>
264
+ <h3>No posts yet</h3>
265
+ <p>Create and publish posts in SudaCloud to populate this section.</p>
266
+ </article>
267
+ )}
268
+ </div>
269
+ </section>
270
+ );
271
+ },
272
+ };
273
+
274
+ export const SECTION_COMPONENTS = {
275
+ Hero,
276
+ FeatureGrid,
277
+ FeaturedPosts,
278
+ Testimonial,
279
+ CallToAction,
280
+ };