@sudajs/cli 0.10.4 → 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/dist/index.js +43 -30
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/templates/theme/AGENTS.md +355 -21
- package/templates/theme/src/index.tsx +3 -2
- package/templates/theme/src/locales/en.json +8 -0
- package/templates/theme/src/sections.tsx +70 -2
- package/templates/theme/src/templates.ts +151 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudajs/cli",
|
|
3
|
-
"version": "0.
|
|
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": "
|
|
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 `
|
|
12
|
-
- Starter page slugs preview at site-like root routes in `suda theme dev`: `
|
|
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
|
|
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.
|
|
162
|
-
-
|
|
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`
|
|
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: "
|
|
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: "
|
|
521
|
+
title: { type: "text", label: t("sections.hero.fields.title") },
|
|
187
522
|
},
|
|
188
523
|
blockSlots: {
|
|
189
524
|
actions: {
|
|
190
|
-
label: "
|
|
525
|
+
label: t("sections.hero.blocks.actions"),
|
|
191
526
|
blocks: {
|
|
192
527
|
button: {
|
|
193
|
-
label: "
|
|
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: "
|
|
196
|
-
href: { type: "url", label: "
|
|
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 {
|
|
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
|
|
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
|
+
};
|