@sudajs/cli 0.18.9 → 0.18.11
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.d.ts +8 -2
- package/dist/index.js +259 -80
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/templates/theme/AGENTS.md +183 -1182
- package/templates/theme/docs/agent-guides/component-authoring.md +270 -0
- package/templates/theme/docs/agent-guides/design-and-runtime.md +170 -0
- package/templates/theme/docs/agent-guides/editor-compatibility.md +231 -0
- package/templates/theme/docs/agent-guides/templates-and-locales.md +206 -0
|
@@ -1,1197 +1,198 @@
|
|
|
1
1
|
# Suda Theme Agent Guide
|
|
2
2
|
|
|
3
|
-
This
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
type HeroProps = {
|
|
100
|
-
eyebrow?: string;
|
|
101
|
-
title?: string;
|
|
102
|
-
body?: string;
|
|
103
|
-
align?: "left" | "center";
|
|
104
|
-
featured?: boolean;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
export const Hero: SudaComponentConfig<HeroProps> = {
|
|
108
|
-
label: t("sections.hero.label"),
|
|
109
|
-
ai: {
|
|
110
|
-
instructions:
|
|
111
|
-
"Landing page hero section. Configure concise copy and a layout choice.",
|
|
112
|
-
},
|
|
113
|
-
fields: {
|
|
114
|
-
eyebrow: { type: "text", label: t("sections.hero.fields.eyebrow") },
|
|
115
|
-
title: { type: "text", label: t("sections.hero.fields.title") },
|
|
116
|
-
body: { type: "textarea", label: t("sections.hero.fields.body") },
|
|
117
|
-
align: {
|
|
118
|
-
type: "select",
|
|
119
|
-
label: t("sections.hero.fields.align"),
|
|
120
|
-
options: [
|
|
121
|
-
{ label: t("common.options.left"), value: "left" },
|
|
122
|
-
{ label: t("common.options.center"), value: "center" },
|
|
123
|
-
],
|
|
124
|
-
},
|
|
125
|
-
featured: { type: "checkbox", label: t("sections.hero.fields.featured") },
|
|
126
|
-
},
|
|
127
|
-
defaultProps: {
|
|
128
|
-
eyebrow: "New",
|
|
129
|
-
title: "Welcome",
|
|
130
|
-
body: "Describe the main offer in one or two sentences.",
|
|
131
|
-
align: "left",
|
|
132
|
-
featured: true,
|
|
133
|
-
},
|
|
134
|
-
render: ({ eyebrow, title, body, align, featured }) => (
|
|
135
|
-
<section data-align={align} data-featured={featured ? "true" : "false"}>
|
|
136
|
-
{eyebrow ? <p>{eyebrow}</p> : null}
|
|
137
|
-
<h1>{title}</h1>
|
|
138
|
-
{body ? <p>{body}</p> : null}
|
|
139
|
-
</section>
|
|
140
|
-
),
|
|
141
|
-
};
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Use normal fields for scalar section settings such as text, textarea, number,
|
|
145
|
-
range, checkbox, select/radio options, media, URL, icon, color, object, and
|
|
146
|
-
array values. Put those field values in the host component's `defaultProps`.
|
|
147
|
-
Use `blockSlots` only when the section owns nested local blocks that editors
|
|
148
|
-
and AI should add, remove, or reorder independently.
|
|
149
|
-
|
|
150
|
-
Field source of truth:
|
|
151
|
-
|
|
152
|
-
- Use TypeScript, not a hand-written runtime schema. Type props first, then let
|
|
153
|
-
`SudaComponentConfig<Props>`, `SudaFields<Props>`, `SudaField`, and
|
|
154
|
-
`defineSudaPageData(pageConfig, data)` catch mismatches.
|
|
155
|
-
- There is no theme-authored zod schema for fields. Do not invent one in the
|
|
156
|
-
theme. Run `pnpm typecheck` and `pnpm validate` instead.
|
|
157
|
-
- Every field entry needs a `type`. Add a localized `label` for every
|
|
158
|
-
editor-facing field.
|
|
159
|
-
- Put a matching serializable value in `defaultProps` for every normal field.
|
|
160
|
-
Optional props can still have defaults; defaults make editor insertion and AI
|
|
161
|
-
generation reliable.
|
|
162
|
-
- Use `description`, `placeholder`, and `visibleIf` only when they help the
|
|
163
|
-
editor or AI choose the right value.
|
|
164
|
-
- Suda extended field types are `url`, `icon`, `color`,
|
|
165
|
-
`range`, `spacing`, `media`, `image`, `video`, and `posts`. These are valid
|
|
166
|
-
in `fields` and are normalized by `@sudajs/theme-engine`.
|
|
167
|
-
|
|
168
|
-
## Navigation and footer menus
|
|
169
|
-
|
|
170
|
-
Navigation and footer menus must be typed arrays. Do not use the legacy menu
|
|
171
|
-
field type, multiline text areas, or newline-delimited strings to model menus.
|
|
172
|
-
Every link destination must use a `url` field type, never a plain `text` field.
|
|
173
|
-
The `fields`, TypeScript props, and `defaultProps` data structures must match
|
|
174
|
-
exactly.
|
|
175
|
-
|
|
176
|
-
Navigation supports at most two levels:
|
|
177
|
-
|
|
178
|
-
- `navItems[]`
|
|
179
|
-
- `navItems[].submenu[]`
|
|
180
|
-
|
|
181
|
-
Submenu items may contain only text and link fields. Do not add another nested
|
|
182
|
-
submenu field inside `navItems[].submenu[]`; the field shape must make third
|
|
183
|
-
levels impossible. An empty `submenu` array means the item is an ordinary
|
|
184
|
-
top-level link.
|
|
185
|
-
|
|
186
|
-
The simplest Navigation field shape is:
|
|
187
|
-
|
|
188
|
-
```ts
|
|
189
|
-
type NavSubItem = {
|
|
190
|
-
label: string;
|
|
191
|
-
href: string;
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
type NavItem = {
|
|
195
|
-
label: string;
|
|
196
|
-
href: string;
|
|
197
|
-
submenu: NavSubItem[];
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const navItemsField = {
|
|
201
|
-
type: "array",
|
|
202
|
-
label: t("common.fields.navigationItems"),
|
|
203
|
-
getItemSummary: (item: NavItem) => item.label || "Navigation item",
|
|
204
|
-
defaultItemProps: {
|
|
205
|
-
label: "Home",
|
|
206
|
-
href: "/",
|
|
207
|
-
submenu: [],
|
|
208
|
-
},
|
|
209
|
-
arrayFields: {
|
|
210
|
-
label: {
|
|
211
|
-
type: "text",
|
|
212
|
-
label: t("common.fields.label"),
|
|
213
|
-
},
|
|
214
|
-
href: {
|
|
215
|
-
type: "url",
|
|
216
|
-
label: t("common.fields.link"),
|
|
217
|
-
},
|
|
218
|
-
submenu: {
|
|
219
|
-
type: "array",
|
|
220
|
-
label: t("common.fields.submenu"),
|
|
221
|
-
getItemSummary: (item: NavSubItem) => item.label || "Submenu item",
|
|
222
|
-
arrayFields: {
|
|
223
|
-
label: {
|
|
224
|
-
type: "text",
|
|
225
|
-
label: t("common.fields.label"),
|
|
226
|
-
},
|
|
227
|
-
href: {
|
|
228
|
-
type: "url",
|
|
229
|
-
label: t("common.fields.link"),
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const defaultNavItems: NavItem[] = [
|
|
237
|
-
{
|
|
238
|
-
label: "Home",
|
|
239
|
-
href: "/",
|
|
240
|
-
submenu: [],
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
label: "Company",
|
|
244
|
-
href: "/company",
|
|
245
|
-
submenu: [
|
|
246
|
-
{
|
|
247
|
-
label: "About",
|
|
248
|
-
href: "/about",
|
|
249
|
-
},
|
|
250
|
-
],
|
|
251
|
-
},
|
|
252
|
-
];
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
Footer grouped menus use two array levels:
|
|
256
|
-
|
|
257
|
-
- `columns[]`
|
|
258
|
-
- `columns[].links[]`
|
|
259
|
-
|
|
260
|
-
Use this prop shape and mirror it exactly in the field config and defaults:
|
|
261
|
-
|
|
262
|
-
```ts
|
|
263
|
-
type FooterColumn = {
|
|
264
|
-
title: string;
|
|
265
|
-
links: {
|
|
266
|
-
text: string;
|
|
267
|
-
url: string;
|
|
268
|
-
}[];
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
const footerColumnsField = {
|
|
272
|
-
type: "array",
|
|
273
|
-
label: t("common.fields.columns"),
|
|
274
|
-
getItemSummary: (item: FooterColumn) => item.title || "Footer column",
|
|
275
|
-
defaultItemProps: {
|
|
276
|
-
title: "Company",
|
|
277
|
-
links: [{ text: "About", url: "/about" }],
|
|
278
|
-
},
|
|
279
|
-
arrayFields: {
|
|
280
|
-
title: {
|
|
281
|
-
type: "text",
|
|
282
|
-
label: t("common.fields.title"),
|
|
283
|
-
},
|
|
284
|
-
links: {
|
|
285
|
-
type: "array",
|
|
286
|
-
label: t("common.fields.links"),
|
|
287
|
-
getItemSummary: (item: FooterColumn["links"][number]) => item.text || "Footer link",
|
|
288
|
-
defaultItemProps: {
|
|
289
|
-
text: "About",
|
|
290
|
-
url: "/about",
|
|
291
|
-
},
|
|
292
|
-
arrayFields: {
|
|
293
|
-
text: {
|
|
294
|
-
type: "text",
|
|
295
|
-
label: t("common.fields.label"),
|
|
296
|
-
},
|
|
297
|
-
url: {
|
|
298
|
-
type: "url",
|
|
299
|
-
label: t("common.fields.link"),
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
};
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
Use these field patterns:
|
|
308
|
-
|
|
309
|
-
```ts
|
|
310
|
-
fields: {
|
|
311
|
-
// Plain copy
|
|
312
|
-
title: { type: "text", label: t("common.fields.title") },
|
|
313
|
-
body: { type: "textarea", label: t("common.fields.description") },
|
|
314
|
-
richBody: { type: "richtext", label: t("common.fields.content") },
|
|
315
|
-
|
|
316
|
-
// Numbers and booleans
|
|
317
|
-
columns: { type: "number", label: t("common.fields.columns") },
|
|
318
|
-
featured: { type: "checkbox", label: t("common.fields.featured") },
|
|
319
|
-
opacity: {
|
|
320
|
-
type: "range",
|
|
321
|
-
label: t("common.fields.opacity"),
|
|
322
|
-
min: 0,
|
|
323
|
-
max: 100,
|
|
324
|
-
step: 5,
|
|
325
|
-
unit: "%",
|
|
326
|
-
},
|
|
327
|
-
|
|
328
|
-
// Choices: always provide options with localized labels and serializable values.
|
|
329
|
-
align: {
|
|
330
|
-
type: "radio",
|
|
331
|
-
label: t("common.fields.align"),
|
|
332
|
-
options: [
|
|
333
|
-
{ label: t("common.options.left"), value: "left" },
|
|
334
|
-
{ label: t("common.options.center"), value: "center" },
|
|
335
|
-
],
|
|
336
|
-
},
|
|
337
|
-
tone: {
|
|
338
|
-
type: "select",
|
|
339
|
-
label: t("common.fields.tone"),
|
|
340
|
-
options: [
|
|
341
|
-
{ label: t("common.options.light"), value: "light" },
|
|
342
|
-
{ label: t("common.options.dark"), value: "dark" },
|
|
343
|
-
],
|
|
344
|
-
},
|
|
345
|
-
|
|
346
|
-
// Suda extended fields
|
|
347
|
-
href: { type: "url", label: t("common.fields.link") },
|
|
348
|
-
image: { type: "image", label: t("common.fields.image") },
|
|
349
|
-
video: { type: "video", label: t("common.fields.video") },
|
|
350
|
-
download: { type: "media", kind: "file", label: t("common.fields.file") },
|
|
351
|
-
icon: { type: "icon", label: t("common.fields.icon") },
|
|
352
|
-
accentColor: { type: "color", label: t("common.fields.color") },
|
|
353
|
-
spacing: { type: "spacing", label: t("common.fields.spacing") },
|
|
354
|
-
|
|
355
|
-
// Dynamic post lists. Use this only on page sections that intentionally
|
|
356
|
-
// render post resources, not on fixed CMS main sections.
|
|
357
|
-
postList: { type: "posts", label: t("common.fields.posts") },
|
|
358
|
-
|
|
359
|
-
// Objects and arrays need nested typed fields.
|
|
360
|
-
badge: {
|
|
361
|
-
type: "object",
|
|
362
|
-
label: t("common.fields.badge"),
|
|
363
|
-
objectFields: {
|
|
364
|
-
label: { type: "text", label: t("common.fields.label") },
|
|
365
|
-
tone: { type: "text", label: t("common.fields.tone") },
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
cards: {
|
|
369
|
-
type: "array",
|
|
370
|
-
label: t("common.fields.cards"),
|
|
371
|
-
getItemSummary: (item) => item.title || "Card",
|
|
372
|
-
defaultItemProps: { title: "Card title", body: "Card body" },
|
|
373
|
-
arrayFields: {
|
|
374
|
-
title: { type: "text", label: t("common.fields.title") },
|
|
375
|
-
body: { type: "textarea", label: t("common.fields.description") },
|
|
376
|
-
href: { type: "url", label: t("common.fields.link") },
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
}
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
Match those fields with defaults:
|
|
383
|
-
|
|
384
|
-
```ts
|
|
385
|
-
defaultProps: {
|
|
386
|
-
title: "Welcome",
|
|
387
|
-
body: "Describe the offer.",
|
|
388
|
-
richBody: "<p>Long-form content.</p>",
|
|
389
|
-
columns: 3,
|
|
390
|
-
featured: true,
|
|
391
|
-
opacity: 80,
|
|
392
|
-
align: "left",
|
|
393
|
-
tone: "light",
|
|
394
|
-
href: "/contact",
|
|
395
|
-
image: themeAsset("assets/hero.jpg"),
|
|
396
|
-
video: "",
|
|
397
|
-
download: "",
|
|
398
|
-
icon: "sparkles",
|
|
399
|
-
accentColor: "#2563eb",
|
|
400
|
-
spacing: "md",
|
|
401
|
-
postList: { strategy: "featured", limit: 3 },
|
|
402
|
-
badge: { label: "New", tone: "primary" },
|
|
403
|
-
cards: [{ title: "Fast setup", body: "Launch quickly.", href: "/features" }],
|
|
404
|
-
}
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
Starter page and CMS template data must use this shape:
|
|
408
|
-
|
|
409
|
-
```ts
|
|
410
|
-
defineSudaPageData(pageConfig, {
|
|
411
|
-
root: { props: {} },
|
|
412
|
-
content: [
|
|
413
|
-
{
|
|
414
|
-
type: "Hero",
|
|
415
|
-
props: {
|
|
416
|
-
id: "Hero-1",
|
|
417
|
-
title: "Welcome",
|
|
418
|
-
actions: [
|
|
419
|
-
{
|
|
420
|
-
type: "button",
|
|
421
|
-
props: { label: "Contact us", href: "/contact" },
|
|
422
|
-
},
|
|
423
|
-
],
|
|
424
|
-
},
|
|
425
|
-
},
|
|
426
|
-
],
|
|
427
|
-
});
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
Top-level `content[].type` must be a key registered in `pageConfig.components`.
|
|
431
|
-
Top-level `content[].props.id` is required in starter pages, CMS templates, and
|
|
432
|
-
layout data. Do not put `id` in component `defaultProps`, local block
|
|
433
|
-
`defaultProps`, `defaultBlocks`, or block-slot nested items.
|
|
434
|
-
Nested block-slot items must use short local block kind names such as
|
|
435
|
-
`"button"`, `"copy"`, or `"cards"`. Never write
|
|
436
|
-
`__suda_local_block__/...` in source templates, starter pages, CMS templates,
|
|
437
|
-
or AI examples.
|
|
438
|
-
|
|
439
|
-
## Theme contract
|
|
440
|
-
|
|
441
|
-
- Keep `renderMode: "ssr"` in `src/manifest.ts`.
|
|
442
|
-
- Keep source entries at `src/index.tsx` and `src/styles.css`. The CLI owns and generates `dist/runtime.client.js`; do not add `src/runtime.client.ts(x)`.
|
|
443
|
-
- If the theme needs browser-only setup, export `clientHooks` from `src/client.ts`. Do not export theme configuration from that file.
|
|
444
|
-
- Export a complete `ThemeModule` from `src/index.tsx`: `manifest`, `pageConfig`, `layoutConfig`, `defaultLayout`, `starterPages`, and `cmsTemplates`.
|
|
445
|
-
- 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.
|
|
446
|
-
- Keep `pageConfig` focused on page content sections. Keep `layoutConfig` focused on shared site chrome such as root, header, page outlet, and footer.
|
|
447
|
-
- Do not edit generated files under `dist/`; run `pnpm build` to regenerate them.
|
|
448
|
-
- React, React DOM, Puck, and `@sudajs/theme-engine` are host-provided peers. Do not bundle private copies into the theme runtime.
|
|
449
|
-
- Keep persisted props JSON-serializable. Do not store functions, React nodes, class instances, database ids for media, or environment-specific absolute filesystem paths.
|
|
450
|
-
|
|
451
|
-
### Public site language list
|
|
452
|
-
|
|
453
|
-
Read public-site language switch targets with `getSiteLocale(puck?.metadata)`
|
|
454
|
-
from `@sudajs/theme-engine/runtime`. Render the host-provided `label` and link
|
|
455
|
-
to the host-provided `href`; do not derive a language name or construct a locale
|
|
456
|
-
URL in theme code.
|
|
457
|
-
|
|
458
|
-
```tsx
|
|
459
|
-
const siteLocale = getSiteLocale(puck?.metadata);
|
|
460
|
-
|
|
461
|
-
{
|
|
462
|
-
siteLocale?.locales.map((item) => (
|
|
463
|
-
<a key={item.locale} href={item.href} lang={item.locale}>
|
|
464
|
-
{item.label}
|
|
465
|
-
</a>
|
|
466
|
-
));
|
|
467
|
-
}
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
## Puck editor CSS isolation
|
|
471
|
-
|
|
472
|
-
Puck renders the editable preview in a same-origin iframe. The outer
|
|
473
|
-
`#puck-canvas-root` and `#preview-frame` may be only one viewport tall; the
|
|
474
|
-
full page must scroll inside the iframe. Theme CSS must not collapse or disable
|
|
475
|
-
that iframe document.
|
|
476
|
-
|
|
477
|
-
- In editor mode, make the theme root content-height driven. A root marker such
|
|
478
|
-
as `[data-suda-editor="true"]` should use `height: auto`,
|
|
479
|
-
`min-height: 100vh`, and `overflow: visible` unless the theme has a stronger
|
|
480
|
-
reason not to.
|
|
481
|
-
- If the theme root is rendered inside Puck's `#frame-root`, ensure
|
|
482
|
-
`#frame-root` can grow with content in editor mode. Do not leave it stuck at
|
|
483
|
-
a collapsed height when it contains the theme root.
|
|
484
|
-
- Root `[data-puck-dropzone]` wrappers must be able to grow with page content.
|
|
485
|
-
Avoid forcing `height: 100%` on the root DropZone when that turns the page
|
|
486
|
-
into a single viewport-height box.
|
|
487
|
-
- Keep public-site resets from leaking into Puck behavior. Be very careful with
|
|
488
|
-
global `html`, `body`, `iframe`, `.hidden`, `img`, `button`, `input`, and
|
|
489
|
-
Tailwind preflight rules; they can hide editor wrappers, change intrinsic
|
|
490
|
-
media sizing, or break iframe scrolling.
|
|
491
|
-
- When importing third-party CSS, add editor-scoped overrides under the theme
|
|
492
|
-
root marker instead of changing public runtime behavior globally.
|
|
493
|
-
- Verify the editor canvas by scrolling through every starter page. Components
|
|
494
|
-
must be visible and reachable without selecting them from the outline first.
|
|
495
|
-
|
|
496
|
-
## CMS and starter templates
|
|
497
|
-
|
|
498
|
-
Suda CMS is a built-in fixed post system. Themes must provide exactly these
|
|
499
|
-
four CMS templates through `cmsTemplates`:
|
|
500
|
-
|
|
501
|
-
- `posts`: post list template for `/posts`
|
|
502
|
-
- `post`: post detail template for `/posts/:slug`
|
|
503
|
-
- `tags`: tag/category list template for `/tags`
|
|
504
|
-
- `tag`: posts filtered by one tag for `/tags/:slug`
|
|
505
|
-
|
|
506
|
-
Each `cmsTemplates` value must be an object with `{ title: string; data:
|
|
507
|
-
PageData }`. Do not point a key directly at raw `PageData`, and do not use
|
|
508
|
-
intersection types such as `ThemeModule & { cmsTemplates:
|
|
509
|
-
Record<string, PageData> }` to bypass the current contract.
|
|
510
|
-
|
|
511
|
-
```ts
|
|
512
|
-
export const cmsTemplates: ThemeCmsTemplates = {
|
|
513
|
-
posts: { title: "Posts", data: postIndexTemplate },
|
|
514
|
-
post: { title: "Post", data: postDetailTemplate },
|
|
515
|
-
tags: { title: "Tags", data: tagIndexTemplate },
|
|
516
|
-
tag: { title: "Tag", data: tagDetailTemplate },
|
|
517
|
-
};
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
Do not create Shopify-style template variants such as `post.default`,
|
|
521
|
-
`post.modern`, or `posts.magazine`. Do not add arbitrary collection/content
|
|
522
|
-
types or manual data-source binding contracts. Keep CMS-only sections limited
|
|
523
|
-
to the CMS templates where they belong.
|
|
524
|
-
|
|
525
|
-
Create CMS template main sections like this:
|
|
526
|
-
|
|
527
|
-
- `MainPosts`: place only on `posts` and optionally `tag` templates with
|
|
528
|
-
`placement: { cmsTemplates: ["posts", "tag"] }`; render `cms.posts`
|
|
529
|
-
directly and use `cms.pagination` for pagination controls. Use
|
|
530
|
-
`cms.currentTag` when a tag filter is active.
|
|
531
|
-
- `MainPost`: place only on the `post` template; render the current article
|
|
532
|
-
when `cms.type === "post"`.
|
|
533
|
-
- `MainTags`: place only on the `tags` template; render tag/category cards
|
|
534
|
-
when `cms.type === "tags"`.
|
|
535
|
-
- `MainTagPosts` can be a separate tag-page list section, or `MainPosts` can
|
|
536
|
-
handle both `posts` and `tag` contexts. Choose the clearer API for the theme.
|
|
537
|
-
|
|
538
|
-
In CMS template main sections, call `getCmsContent(puck?.metadata)` and render
|
|
539
|
-
the matching CMS data. Do not add `type: "posts"` fields to those sections.
|
|
540
|
-
|
|
541
|
-
For pagination controls, use `cms.pagination.page`,
|
|
542
|
-
`cms.pagination.totalPages`, `cms.pagination.hasPreviousPage`, and
|
|
543
|
-
`cms.pagination.hasNextPage` to decide what to show. Do not hand-build
|
|
544
|
-
pagination query strings. Import `cmsPaginationUrl` from
|
|
545
|
-
`@sudajs/theme-engine/runtime` and call
|
|
546
|
-
`cmsPaginationUrl(cms.pagination, targetPage)` for numbered links so existing
|
|
547
|
-
filter/search parameters stay in the URL. Use `cms.pagination.previousUrl` and
|
|
548
|
-
`cms.pagination.nextUrl` for previous/next buttons when present.
|
|
549
|
-
|
|
550
|
-
Themes must also provide at least one reusable ordinary-page post section, for
|
|
551
|
-
example `FeaturedPosts`, `LatestPosts`, or `TopicPosts`. Use it for homepage
|
|
552
|
-
insights, featured articles, case studies, news teasers, or tag-filtered post
|
|
553
|
-
groups on ordinary pages.
|
|
554
|
-
|
|
555
|
-
Define that section with exactly one top-level `type: "posts"` field and a
|
|
556
|
-
default query on the same prop:
|
|
557
|
-
|
|
558
|
-
```tsx
|
|
559
|
-
fields: {
|
|
560
|
-
postList: { type: "posts" },
|
|
561
|
-
},
|
|
562
|
-
defaultProps: {
|
|
563
|
-
postList: { strategy: "featured", limit: 3 },
|
|
564
|
-
},
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
Supported strategies are `latest`, `featured`, `by_tag`, and `manual`. Manual
|
|
568
|
-
queries store only `items` and must not include `limit`. Runtime post resources
|
|
569
|
-
are keyed as `${props.id}.${fieldKey}` with both parts URL-encoded by the
|
|
570
|
-
platform. In the section render function, call
|
|
571
|
-
`getPostResource(puck?.metadata, { componentId: props.id, fieldKey: "postList" })`
|
|
572
|
-
with the exact posts field key. Never import Prisma, call platform APIs, or
|
|
573
|
-
query the database from theme code.
|
|
574
|
-
|
|
575
|
-
When rendering posts returned by `getPostResource`, build links from the
|
|
576
|
-
platform-provided `post.url` first. If it is missing, derive the URL only from
|
|
577
|
-
`post.slug` as `/posts/${encodeURIComponent(post.slug)}`; if neither exists,
|
|
578
|
-
use `#`. Do not guess alternate fields such as `href`, `path`, `permalink`,
|
|
579
|
-
`postUrl`, or `canonicalUrl`.
|
|
580
|
-
|
|
581
|
-
Do not add `resourceQuery: { type: "posts" }` or
|
|
582
|
-
`resource_query: { type: "posts" }`. Do not assume the field must be named
|
|
583
|
-
`query`. A section may have only one top-level posts field; do not nest posts
|
|
584
|
-
fields inside `object`, `array`, native slots, or `blockSlots`.
|
|
585
|
-
|
|
586
|
-
Starter pages must include one home/index page with `isHome: true`. Recommended
|
|
587
|
-
starter pages include `index`, `about-us`, `contact-us`, `services`, and `team`.
|
|
588
|
-
Those recommended pages are not hard requirements, but a publishable theme
|
|
589
|
-
should usually include several realistic pages so AI site generation has a rich
|
|
590
|
-
reference set.
|
|
591
|
-
|
|
592
|
-
Treat repetition as a design and business judgment, not a fixed rule. Let the
|
|
593
|
-
site strategy decide where pages should share patterns and where they should
|
|
594
|
-
diverge. Reusing strong sections across starter pages is often right: CTA,
|
|
595
|
-
contact, booking, newsletter, download, and other conversion sections may appear
|
|
596
|
-
repeatedly when they help visitors move forward. Avoid only mechanical
|
|
597
|
-
copy-paste that makes different pages feel interchangeable. When useful, give
|
|
598
|
-
each page its own purpose, angle, proof points, media, or ordering, while
|
|
599
|
-
keeping repeated business-driving sections where they serve the user journey.
|
|
600
|
-
Richer starter pages make the theme easier for AI to adapt without making users
|
|
601
|
-
learn CMS/template mechanics.
|
|
602
|
-
|
|
603
|
-
## Editor i18n and locales
|
|
604
|
-
|
|
605
|
-
Theme editor labels are localized by the host editor. Use the template's
|
|
606
|
-
`src/i18n.ts` helper for every component label, field label, option label, and
|
|
607
|
-
other editor-facing configuration string.
|
|
608
|
-
|
|
609
|
-
- Import `t` from `./i18n.js` in files that define `pageConfig`, `layoutConfig`,
|
|
610
|
-
categories, fields, options, or local blocks.
|
|
611
|
-
- Write labels as stable dot-path keys, for example
|
|
612
|
-
`label: t("sections.hero.fields.title")`, not hardcoded strings.
|
|
613
|
-
- Add every key used with `t(...)` to `src/locales/en.json`. This file is the
|
|
614
|
-
required reference locale for theme validation.
|
|
615
|
-
- Keep locale values as nested JSON objects whose leaves are strings. Arrays,
|
|
616
|
-
numbers, booleans, and null are invalid locale values.
|
|
617
|
-
- When adding another locale such as `src/locales/zh-CN.json`, keep its leaf key
|
|
618
|
-
set exactly aligned with `src/locales/en.json`; missing or extra keys fail
|
|
619
|
-
`suda theme validate`, `suda theme check`, `pnpm build`, and publish.
|
|
620
|
-
- The CLI copies `src/locales/*.json` into `dist/locales/` during build. Files
|
|
621
|
-
named `<locale>.json` are editor messages; files named
|
|
622
|
-
`<locale>.content.json` are starter preview content messages. Do not
|
|
623
|
-
edit `dist/locales/` directly.
|
|
624
|
-
- If an installed legacy theme is missing locale files, the editor falls back to
|
|
625
|
-
showing translation keys instead of crashing. New themes must still pass
|
|
626
|
-
locale validation before publishing.
|
|
627
|
-
- Keep editor `t(...)` out of actual page content, starter page copy, default
|
|
628
|
-
prop values, and public-site text. Starter preview copy may instead use the
|
|
629
|
-
separate `t` exported by `src/preview-i18n.ts`; direct strings remain valid
|
|
630
|
-
and editor-saved values are always plain strings.
|
|
631
|
-
|
|
632
|
-
## Starter preview content locales
|
|
633
|
-
|
|
634
|
-
Use `src/preview-i18n.ts` only inside starter page templates for user-visible
|
|
635
|
-
content such as page titles, headings, descriptions, list copy, and button
|
|
636
|
-
labels. Keep URLs, slugs, IDs, component types, and asset paths as direct
|
|
637
|
-
strings. Do not use preview translations in component labels, fields, default
|
|
638
|
-
props, layout configuration, or CMS templates.
|
|
639
|
-
|
|
640
|
-
- `src/locales/en.content.json` is the required default content locale whenever
|
|
641
|
-
`manifest.previewLocales` is declared.
|
|
642
|
-
- Add every supported locale to `manifest.previewLocales`, including `en`, and
|
|
643
|
-
create the matching `src/locales/<locale>.content.json` file.
|
|
644
|
-
- Every content locale file must have exactly the same string leaf keys as
|
|
645
|
-
`en.content.json`; undeclared, missing, extra, or partial locale files fail
|
|
646
|
-
validation and publishing.
|
|
647
|
-
|
|
648
|
-
Example:
|
|
649
|
-
|
|
650
|
-
```tsx
|
|
651
|
-
import { t } from "./i18n.js";
|
|
652
|
-
|
|
653
|
-
export const Hero: SudaComponentConfig<HeroProps> = {
|
|
654
|
-
label: t("sections.hero.label"),
|
|
655
|
-
fields: {
|
|
656
|
-
title: { type: "text", label: t("sections.hero.fields.title") },
|
|
657
|
-
tone: {
|
|
658
|
-
type: "radio",
|
|
659
|
-
label: t("sections.hero.fields.tone"),
|
|
660
|
-
options: [
|
|
661
|
-
{ label: t("sections.hero.fields.toneWarm"), value: "warm" },
|
|
662
|
-
{ label: t("sections.hero.fields.toneSharp"), value: "sharp" },
|
|
663
|
-
],
|
|
664
|
-
},
|
|
665
|
-
},
|
|
666
|
-
};
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
## Component strategy
|
|
670
|
-
|
|
671
|
-
- Create theme-specific components and sections first. Every page component should express this theme's brand, industry, visual rhythm, and content model.
|
|
672
|
-
- Do not register `createEngineComponents()`, `createBaseBlocks()`, or `createContainers()` in `pageConfig.components` when creating a theme. Suda automatically adds the platform base content blocks (`Text`, `Heading`, `RichText`, `Markdown`, `Image`, `Button`, and `Spacer`) to every page config. Use the single `Markdown` block for standalone long-form pages such as privacy policies, terms, legal notices, documentation, or guides that should not appear in CMS posts. Generic blocks supplement theme-specific sections; they are not the foundation of starter templates.
|
|
673
|
-
- Prefer purpose-built sections such as hero, services, product highlights, feature comparisons, gallery, process, pricing, testimonial, FAQ, article list, contact, and CTA when the theme needs those patterns.
|
|
674
|
-
- Keep section APIs semantic. Expose props that describe content and editor decisions, not implementation details.
|
|
675
|
-
- Keep `fields`, `defaultProps`, and `render` in sync. Every editor field should have a sensible default, and render functions should tolerate omitted optional values.
|
|
676
|
-
- Put editor controls in props when they change rendering, for example `showLogo`, `mediaPosition`, `tone`, or `columns`; pair dependent controls with `visibleIf` rather than hiding logic only in JSX.
|
|
677
|
-
|
|
678
|
-
## Layout and slot rules
|
|
679
|
-
|
|
680
|
-
- Page-level sections and components must not declare native Puck `slot` fields. Do not write `fields: { content: { type: "slot" } }` in page components.
|
|
681
|
-
- Native Puck slots are reserved for Suda-provided layout/container primitives and theme layout components. The normal template pattern is the one in `src/layout.tsx`: `rootConfig` renders `children`, `PageOutlet` renders `getPageSlot(puck?.metadata)`, and `Header` / `Footer` surround that outlet in `defaultLayout`.
|
|
682
|
-
- Do not add arbitrary nested page composition through native slots. It makes AI-generated pages unpredictable and hard to validate.
|
|
683
|
-
- If a page section needs controlled nested content, use Suda `blockSlots`, not a hand-written Puck slot field. `blockSlots` lets the theme define exactly which local block kinds are allowed inside that section.
|
|
684
|
-
- Do not use legacy DropZone or `zones` patterns.
|
|
685
|
-
|
|
686
|
-
## Puck Editor Compatibility Contract
|
|
687
|
-
|
|
688
|
-
Every public page and layout component must render consistently in the public
|
|
689
|
-
site, the Puck Editor iframe, and editor preview mode. Build this compatibility
|
|
690
|
-
in when creating the theme; do not wait for an editor-only positioning bug.
|
|
691
|
-
|
|
692
|
-
### Puck mutates the real component root
|
|
693
|
-
|
|
694
|
-
In edit mode Puck may attach selection and drag attributes and an inline
|
|
695
|
-
position directly to the first real DOM element returned by a component:
|
|
696
|
-
|
|
697
|
-
```ts
|
|
698
|
-
el.setAttribute("data-puck-component", id);
|
|
699
|
-
el.setAttribute("data-puck-dnd", id);
|
|
700
|
-
el.style.position = "relative";
|
|
701
|
-
```
|
|
702
|
-
|
|
703
|
-
- Never assume `[data-puck-component]` is an external wrapper. It may be the
|
|
704
|
-
component's own `<nav>`, `<header>`, `<section>`, or `<footer>`.
|
|
705
|
-
- `:has(.component-root)` does not match an element that is itself
|
|
706
|
-
`.component-root`. When both DOM shapes are possible, cover both explicitly:
|
|
707
|
-
|
|
708
|
-
```css
|
|
709
|
-
.component-root[data-puck-component],
|
|
710
|
-
[data-puck-component]:has(.component-root) {
|
|
711
|
-
/* compatibility override */
|
|
712
|
-
}
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
- Puck's inline `position: relative` beats ordinary theme CSS. A component
|
|
716
|
-
whose real root must remain `sticky`, `fixed`, or `absolute` in the editor
|
|
717
|
-
needs a specific editor-scoped rule with `!important`.
|
|
718
|
-
- Never apply `position: relative !important` to every editor component. That
|
|
719
|
-
destroys navigation, overlays, and floating controls.
|
|
720
|
-
|
|
721
|
-
### Stable component roots
|
|
722
|
-
|
|
723
|
-
- Every public page and layout component should return one stable, semantic,
|
|
724
|
-
real DOM root. Prefer `<header>` or `<nav>` for navigation, `<footer>` for the
|
|
725
|
-
footer, and `<section>` or an explicit container for ordinary sections.
|
|
726
|
-
- Do not use a Fragment as the root of a component that controls positioning,
|
|
727
|
-
margin, size, stacking, background, or editor selection. Do not depend on
|
|
728
|
-
Puck to wrap a Fragment.
|
|
729
|
-
- Do not use `display: contents` on a root that needs margin, padding,
|
|
730
|
-
positioning, sticky/fixed behavior, `z-index`, background, or a selection
|
|
731
|
-
outline.
|
|
732
|
-
- `PageOutlet` must render a real DOM element when it needs negative margin,
|
|
733
|
-
overlap, positioning, or sizing.
|
|
734
|
-
|
|
735
|
-
### Explicit editor scope
|
|
736
|
-
|
|
737
|
-
The layout root must derive editor state from
|
|
738
|
-
`puck?.metadata?.isEditor === true` and expose a stable marker:
|
|
739
|
-
|
|
740
|
-
```tsx
|
|
741
|
-
<div className="theme-root" data-suda-editor={isEditor ? "true" : undefined}>
|
|
742
|
-
{children}
|
|
743
|
-
</div>
|
|
744
|
-
```
|
|
745
|
-
|
|
746
|
-
Keep every editor-only compatibility rule below
|
|
747
|
-
`.theme-root[data-suda-editor="true"]`. Do not globally override `.navbar`,
|
|
748
|
-
`.section`, or `[data-puck-component]`, change the public-site layout to repair
|
|
749
|
-
the editor, use vague selectors that affect unrelated components, or edit
|
|
750
|
-
third-party minified CSS. Put compatibility overrides in the theme's scoped
|
|
751
|
-
`src/styles.css`.
|
|
752
|
-
|
|
753
|
-
When the component itself may be the Puck root or may sit inside one, write and
|
|
754
|
-
test both selectors. Keep later rules from overriding the correction:
|
|
755
|
-
|
|
756
|
-
```css
|
|
757
|
-
.theme-root[data-suda-editor="true"] .theme-navigation[data-puck-component],
|
|
758
|
-
.theme-root[data-suda-editor="true"]
|
|
759
|
-
[data-puck-component]:has(.theme-navigation)
|
|
760
|
-
.theme-navigation {
|
|
761
|
-
position: sticky !important;
|
|
762
|
-
top: 0;
|
|
763
|
-
z-index: 30;
|
|
764
|
-
}
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
### Sticky and fixed positioning
|
|
768
|
-
|
|
769
|
-
Before implementing Header, announcement bar, floating CTA, or any
|
|
770
|
-
sticky/fixed component, verify all of the following:
|
|
771
|
-
|
|
772
|
-
- whether Puck writes `position: relative` on the actual positioned root;
|
|
773
|
-
- which element really scrolls in the site and in the editor iframe;
|
|
774
|
-
- that the sticky element is inside the intended scroll container;
|
|
775
|
-
- whether any ancestor has `overflow: hidden`, `auto`, `scroll`, or `clip`, or
|
|
776
|
-
has `transform`, `filter`, `perspective`, or `contain`;
|
|
777
|
-
- that the sticky parent is taller than the sticky element where required;
|
|
778
|
-
- that `top` and `z-index` are explicit; and
|
|
779
|
-
- that iframe scrolling and the outer editor canvas do not use incompatible
|
|
780
|
-
coordinate systems.
|
|
781
|
-
|
|
782
|
-
For a layout Header, prefer putting sticky behavior on the Header's real root.
|
|
783
|
-
If that root receives `data-puck-component`, override Puck's inline position on
|
|
784
|
-
that same node. Do not force the Header root to `relative !important` and then
|
|
785
|
-
expect a parent sticky rule to work. Site and editor may use different
|
|
786
|
-
positioning strategies only when their visual result remains equivalent.
|
|
787
|
-
|
|
788
|
-
### Transparent navigation is state plus layout
|
|
789
|
-
|
|
790
|
-
Transparent navigation must coordinate all of these concerns, not merely set
|
|
791
|
-
`background: transparent`:
|
|
792
|
-
|
|
793
|
-
- transparent background and removed shadow;
|
|
794
|
-
- logo, menu text, and button color changes;
|
|
795
|
-
- Hero or `PageOutlet` overlap beneath the Header;
|
|
796
|
-
- restoration of the solid state after the scroll threshold;
|
|
797
|
-
- Puck's root-node positioning mutation in the editor; and
|
|
798
|
-
- identical SSR-first-frame and hydrated state.
|
|
799
|
-
|
|
800
|
-
Never render a white logo/text state above an accidentally white navigation
|
|
801
|
-
background. Prefer React Context for a page's transparent-navigation request,
|
|
802
|
-
an SSR-readable DOM marker for the first-frame fallback, and a real
|
|
803
|
-
`PageOutlet` element with negative margin in the editor. The public site may use
|
|
804
|
-
a spacer or equivalent layout strategy. Keep shared measurements in CSS
|
|
805
|
-
variables, for example `--theme-navigation-height: 4.6rem`.
|
|
806
|
-
|
|
807
|
-
### Editor scroll detection
|
|
808
|
-
|
|
809
|
-
Do not rely only on `window.scrollY` or a `window` scroll listener. The active
|
|
810
|
-
scroll source may be `document.scrollingElement`, the iframe document, or an
|
|
811
|
-
inner `overflow: auto/scroll` container, and `scroll` does not bubble.
|
|
812
|
-
|
|
813
|
-
- Find the nearest scrollable ancestor for each navigation instance and read
|
|
814
|
-
that element's own `scrollTop`.
|
|
815
|
-
- Listen on the document with
|
|
816
|
-
`document.addEventListener("scroll", handler, { capture: true, passive: true })`
|
|
817
|
-
and keep a window listener for public-site compatibility.
|
|
818
|
-
- Do not assume editor and site share the same scrolling element.
|
|
819
|
-
|
|
820
|
-
### React and editor runtime lifecycle
|
|
821
|
-
|
|
822
|
-
Runtime code must tolerate editor remounts, live prop changes, HMR, frequent
|
|
823
|
-
`MutationObserver` callbacks, React Strict Mode effect replay, and loading after
|
|
824
|
-
`DOMContentLoaded` has already fired.
|
|
825
|
-
|
|
826
|
-
- Make event binding and initialization idempotent.
|
|
827
|
-
- Observe components inserted later and relevant attribute changes.
|
|
828
|
-
- Do not use a permanent `window.__xxxBound` flag that prevents required HMR
|
|
829
|
-
reinitialization.
|
|
830
|
-
- Clean up component effects, observers, and listeners.
|
|
831
|
-
- Keep SSR markers and client state synchronized instead of letting them fight.
|
|
832
|
-
|
|
833
|
-
### Puck interaction styles
|
|
834
|
-
|
|
835
|
-
Assume edit mode may apply rules equivalent to:
|
|
836
|
-
|
|
837
|
-
```css
|
|
838
|
-
[data-puck-component] * {
|
|
839
|
-
pointer-events: none;
|
|
840
|
-
user-select: none;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
[data-puck-component] {
|
|
844
|
-
pointer-events: auto !important;
|
|
845
|
-
}
|
|
846
|
-
```
|
|
847
|
-
|
|
848
|
-
An unclickable link, dropdown, carousel, or button in edit mode is not by
|
|
849
|
-
itself a theme interaction bug. Distinguish edit mode from preview mode. Never
|
|
850
|
-
globally restore descendant pointer events because that breaks selection and
|
|
851
|
-
dragging; test visitor interactions in editor preview and the public site.
|
|
852
|
-
|
|
853
|
-
### Layout ownership
|
|
854
|
-
|
|
855
|
-
- Keep only `Header`, `PageOutlet`, `Footer`, and necessary global chrome such
|
|
856
|
-
as an announcement bar in `layoutConfig`.
|
|
857
|
-
- Render Header and Footer exactly once per page from the layout. Do not repeat
|
|
858
|
-
them in starter pages.
|
|
859
|
-
- If Footer or Header moves from page sections into the layout, remove duplicate
|
|
860
|
-
entries from `starterPages`, `cmsTemplates`, `pageConfig.categories`, and AI
|
|
861
|
-
examples.
|
|
862
|
-
- Give Header, `PageOutlet`, and Footer stable `props.id` values, with
|
|
863
|
-
`PageOutlet` between Header and Footer.
|
|
864
|
-
- Read platform white-label, ICP, and other footer metadata from runtime
|
|
865
|
-
metadata; do not persist it as duplicated static component data.
|
|
866
|
-
|
|
867
|
-
### Vendor CSS
|
|
868
|
-
|
|
869
|
-
Do not modify original third-party minified CSS unless the task explicitly
|
|
870
|
-
requires a maintained vendor patch. Use precise selectors in the theme's own
|
|
871
|
-
scoped `src/styles.css`. If a vendor file truly must change, document the
|
|
872
|
-
reproducible problem and why an override cannot solve it.
|
|
873
|
-
|
|
874
|
-
### New-theme completion checklist
|
|
875
|
-
|
|
876
|
-
When creating a theme, complete all of these in the initial implementation:
|
|
877
|
-
|
|
878
|
-
- editor root marker and stable real roots;
|
|
879
|
-
- layout-owned Header / `PageOutlet` / Footer with stable ids;
|
|
880
|
-
- Header sticky compatibility in the editor;
|
|
881
|
-
- transparent navigation in editor and site modes;
|
|
882
|
-
- scrollable-ancestor detection and an SSR state marker;
|
|
883
|
-
- idempotent runtime setup and cleanup;
|
|
884
|
-
- real-element `PageOutlet` overlap behavior;
|
|
885
|
-
- responsive viewport behavior; and
|
|
886
|
-
- editor preview versus public-site comparison.
|
|
887
|
-
|
|
888
|
-
### Required regression matrix
|
|
889
|
-
|
|
890
|
-
Whenever Header, transparent navigation, sticky/fixed positioning,
|
|
891
|
-
`PageOutlet`, Footer, carousel, dropdown/mobile menu, or a `100vh` Hero changes,
|
|
892
|
-
verify:
|
|
893
|
-
|
|
894
|
-
- public-site SSR first frame and post-hydration state;
|
|
895
|
-
- editor desktop, tablet, and mobile viewports;
|
|
896
|
-
- editor edit mode and preview mode;
|
|
897
|
-
- initial top state and state after crossing the scroll threshold;
|
|
898
|
-
- state after HMR and live prop updates;
|
|
899
|
-
- exactly one Header and one Footer; and
|
|
900
|
-
- intact Puck selection and drag behavior.
|
|
901
|
-
|
|
902
|
-
### Debugging order
|
|
903
|
-
|
|
904
|
-
When editor and site differ, inspect in this order before changing CSS:
|
|
905
|
-
|
|
906
|
-
1. The component's actual root node.
|
|
907
|
-
2. The element that received `data-puck-component`.
|
|
908
|
-
3. Inline styles written on that element.
|
|
909
|
-
4. Computed `position`, `background`, `z-index`, and ancestor `overflow`.
|
|
910
|
-
5. The real scrolling element and its `scrollTop`.
|
|
911
|
-
6. Iframe versus outer-canvas coordinate systems.
|
|
912
|
-
7. Whether the theme runtime initialized.
|
|
913
|
-
8. Transparent-state Context and DOM markers.
|
|
914
|
-
9. Stylesheet load and cascade order.
|
|
915
|
-
|
|
916
|
-
Do not guess selectors repeatedly before confirming the DOM and computed style.
|
|
917
|
-
|
|
918
|
-
## Design system rules
|
|
919
|
-
|
|
920
|
-
Build every theme from one theme-level design system. Do not let each section,
|
|
921
|
-
CMS template, block slot, or starter page invent its own colors, type scale,
|
|
922
|
-
radius, shadows, or spacing.
|
|
923
|
-
|
|
924
|
-
- Define the editable theme system once in `src/manifest.ts` as
|
|
925
|
-
`sourceManifest.designSystem`. Include `version`, `defaultPresetId`, and
|
|
926
|
-
named `presets` with complete token sets.
|
|
927
|
-
- Use stable token names from the engine contract. At minimum, provide the
|
|
928
|
-
color tokens the theme renders (`background`, `foreground`, `primary`,
|
|
929
|
-
`primaryForeground`, `secondary`, `secondaryForeground`, `accent`,
|
|
930
|
-
`accentForeground`, `muted`, `mutedForeground`) and the radius tokens the CSS
|
|
931
|
-
consumes (`card`, `button`, `input`). Do not invent platform-facing token
|
|
932
|
-
keys without checking the generated types first.
|
|
933
|
-
- Expose the design system at the layout root only, for example:
|
|
934
|
-
```ts
|
|
935
|
-
designSystem: designSystemField(t("common.fields.designSystem"), sourceManifest.designSystem);
|
|
936
|
-
```
|
|
937
|
-
Use `createThemeDesignDefault(sourceManifest.designSystem)` in
|
|
938
|
-
`ROOT_DEFAULTS`.
|
|
939
|
-
- In the root render, resolve the value once with
|
|
940
|
-
`resolveThemeDesignTokens(sourceManifest.designSystem, props.designSystem)`
|
|
941
|
-
and apply `createThemeDesignCssVariables(tokens)` to the theme root element.
|
|
942
|
-
Components should consume CSS variables; they should not resolve design
|
|
943
|
-
presets themselves.
|
|
944
|
-
- Pure Tailwind themes should map Suda tokens only through `@theme inline`, then
|
|
945
|
-
consume utilities such as `bg-primary`, `text-primary-foreground`, and
|
|
946
|
-
`rounded-card`. The `inline` keyword is required so generated utilities read
|
|
947
|
-
`--suda-*` on the rendered theme root instead of resolving a `--color-*`
|
|
948
|
-
alias on `:root`.
|
|
949
|
-
- Hand-written CSS classes must consume `--suda-*` directly. Do not use
|
|
950
|
-
`var(--color-primary)`, `var(--color-background)`, `var(--radius-card)`, or
|
|
951
|
-
another Suda-backed alias declared by `@theme inline`; those compiler aliases
|
|
952
|
-
live on `:root` while Suda injects editable tokens on the descendant theme
|
|
953
|
-
root in production.
|
|
954
|
-
- When hand-written CSS needs a derived value, declare a theme-prefixed custom
|
|
955
|
-
property on the theme root and consume that property, for example:
|
|
956
|
-
```css
|
|
957
|
-
.my-theme-root {
|
|
958
|
-
--my-theme-primary-hover: color-mix(in srgb, var(--suda-color-primary) 84%, black);
|
|
959
|
-
}
|
|
960
|
-
.my-theme-button:hover {
|
|
961
|
-
background: var(--my-theme-primary-hover);
|
|
962
|
-
}
|
|
963
|
-
```
|
|
964
|
-
`suda theme check`, build, and publish reject hand-written consumption of
|
|
965
|
-
Suda-backed `@theme inline` aliases.
|
|
966
|
-
- Keep typography and spacing scales centralized in `src/styles.css`. Define
|
|
967
|
-
reusable classes or variables for containers, section padding, headings,
|
|
968
|
-
eyebrow text, body copy, cards, buttons, inputs, and media frames. Reuse those
|
|
969
|
-
classes instead of writing new one-off Tailwind values in every section.
|
|
970
|
-
- Section props may expose semantic choices such as `tone`, `variant`,
|
|
971
|
-
`columns`, `mediaPosition`, `showImage`, or `spacing`. Map those choices to
|
|
972
|
-
the existing design tokens and shared CSS classes.
|
|
973
|
-
- Do not add raw `color`, typography, `spacing`, `radius`, or `shadow` fields to a
|
|
974
|
-
section just because the CSS has a value. Expose a design token field only
|
|
975
|
-
when the user should intentionally customize that value per component
|
|
976
|
-
instance. Otherwise, keep the value in the theme CSS.
|
|
977
|
-
- Use `color` and `spacing` field types only for real editor-facing
|
|
978
|
-
design controls. Use `select`/`radio` for named variants that are already
|
|
979
|
-
part of the theme design system.
|
|
980
|
-
- Local blocks inside `blockSlots` inherit the host section's design system.
|
|
981
|
-
Local block fields should edit content or semantic variants, not define their
|
|
982
|
-
own palette, type scale, radius, shadows, or spacing.
|
|
983
|
-
- CMS main sections (`MainPosts`, `MainPost`, `MainTags`, `MainTagPosts`) and
|
|
984
|
-
ordinary post-resource sections must use the same containers, heading scale,
|
|
985
|
-
card style, media ratio, pagination style, and empty/loading states as the
|
|
986
|
-
rest of the theme.
|
|
987
|
-
- Starter pages must demonstrate the same design system across different page
|
|
988
|
-
types. Vary content, order, media, and semantic variants; do not hardcode
|
|
989
|
-
unrelated colors, spacing, rounded corners, or shadows in starter page data to
|
|
990
|
-
make pages look different.
|
|
991
|
-
- When adding a new section, first choose the existing container, heading,
|
|
992
|
-
button, card, form, and media patterns it should reuse. Add a new CSS utility
|
|
993
|
-
or token only when multiple sections will use it or the theme needs a new
|
|
994
|
-
named pattern.
|
|
995
|
-
- Keep preview screenshots, default props, starter pages, CMS templates, and
|
|
996
|
-
AI examples aligned with the same token presets. The default preset should
|
|
997
|
-
look publishable without manual editor tweaks.
|
|
998
|
-
|
|
999
|
-
## Styling and assets
|
|
1000
|
-
|
|
1001
|
-
- Tailwind v4 starts in `src/styles.css` with `@import "tailwindcss";` and `@source "./**/*.{ts,tsx}";`.
|
|
1002
|
-
- Theme-local assets live in top-level `assets/`; the CLI copies them to `dist/assets/` during build.
|
|
1003
|
-
- Import `themeAsset` from `./theme-asset.js` and use `themeAsset("assets/...")` for every theme-bundled asset in default props, starter pages, CMS templates, and AI examples.
|
|
1004
|
-
- Do not hand-write `themes/<themeKey>/<version>/...` paths and do not import theme-local images from React code. Full external URLs are allowed, for example CDN URLs such as jsDelivr.
|
|
1005
|
-
- Use `resolveAsset(puck?.metadata, value)` from `@sudajs/theme-engine/runtime` before rendering user-selected media fields.
|
|
1006
|
-
- Scope theme CSS with the generated theme key classes. Avoid global resets that could affect the host editor or other themes.
|
|
1007
|
-
- Preview screenshots are required at `assets/preview/desktop.png`, `assets/preview/tablet.png`, and `assets/preview/mobile.png`; run `suda theme capture` to generate them.
|
|
1008
|
-
- Customize `vite.config.ts`, `postcss.config.mjs`, and `src/styles.css` only when the theme needs it.
|
|
1009
|
-
|
|
1010
|
-
## Field authoring rules
|
|
1011
|
-
|
|
1012
|
-
Use the most specific field type available. A generic `text` field may pass typecheck, but it is wrong when a semantic Suda field exists.
|
|
1013
|
-
|
|
1014
|
-
- Always use `url` for links and routes, including anchors, `mailto:` links, and external URLs. Do not use `text` for href-like props.
|
|
1015
|
-
- Use `image` for images, `video` for videos, and `media` only when the same prop intentionally accepts mixed media.
|
|
1016
|
-
- Use `color` for color values, `icon` for icon names, `range` for bounded numbers, and `spacing` for spacing tokens.
|
|
1017
|
-
- Use `select` for typography choices and `array`/`object` fields for navigation lists.
|
|
1018
|
-
- Use `select` or `radio` for fixed choices. Keep option values stable, short, and serializable.
|
|
1019
|
-
- Use `array` for repeatable items and `object` for grouped settings. Keep nested field names aligned with the prop shape and `defaultProps`.
|
|
1020
|
-
- Use `textarea` for multi-sentence copy. Use `text` only for short labels, headings, slugs, names, or plain strings that are not URLs, media, colors, icons, typography choices, or navigation lists.
|
|
1021
|
-
- Use `t(...)` for every editor-facing `label` in components, fields, nested
|
|
1022
|
-
`arrayFields` / `object.fields`, local blocks, and option lists. Add matching
|
|
1023
|
-
keys to `src/locales/en.json` at the same time.
|
|
1024
|
-
- For card grids, feature lists, pricing tables, logo walls, gallery grids, and any multi-column component, expose a `columns` prop with a `range` or `select` field and a sensible default.
|
|
1025
|
-
- Use `visibleIf: ({ props }, { fields }) => boolean` for synchronous field visibility that depends on sibling props or resolved fields. Keep it pure and fast.
|
|
1026
|
-
- Use Puck `resolveFields` only for heavier dynamic field changes that cannot be expressed with `visibleIf`.
|
|
1027
|
-
- Add field-level `ai.instructions`, `ai.required`, `ai.exclude`, `ai.stream`, or `ai.bind` when a prop needs generation guidance beyond its label and type. Use `ai.stream: false` for atomic values such as URLs that should not stream partially.
|
|
1028
|
-
|
|
1029
|
-
## Contact form contract
|
|
1030
|
-
|
|
1031
|
-
Themes must render the host-provided contact form from `metadata.contactForm`
|
|
1032
|
-
when they offer a contact section. The platform owns field metadata,
|
|
1033
|
-
validation, submissions, notifications, webhooks, permissions, and limits; the
|
|
1034
|
-
theme owns the public UI only.
|
|
1035
|
-
|
|
1036
|
-
- Read the form through `getContactForm(puck?.metadata)` from
|
|
1037
|
-
`@sudajs/theme-engine/runtime`.
|
|
1038
|
-
- `suda theme dev` passes a default contact form through `metadata.contactForm`
|
|
1039
|
-
so contact sections can be developed and previewed locally without a project.
|
|
1040
|
-
- If there is no valid, enabled form configuration, render nothing for the form
|
|
1041
|
-
area; do not show a public "contact form unavailable" fallback panel.
|
|
1042
|
-
- Style all five supported field types: `text`, `textarea`, `checkbox`,
|
|
1043
|
-
`radio`, and `select`.
|
|
1044
|
-
- Style labels, inputs, submit button, success state, validation errors, and
|
|
1045
|
-
responsive layout in the theme's visual language.
|
|
1046
|
-
- Only render placeholders supplied by the form setting. Do not invent default
|
|
1047
|
-
placeholder text for any field type, including the empty option in `select`.
|
|
1048
|
-
- Submit to `contactForm.endpoint` with the field values; never hardcode
|
|
1049
|
-
notification channels, webhook URLs, ICP records, or white-label branding in
|
|
1050
|
-
the contact section.
|
|
1051
|
-
- Include hidden platform fields, including the honeypot field
|
|
1052
|
-
`name={contactForm.honeypotField}`, as bare `<input type="hidden" />`
|
|
1053
|
-
elements. Do not wrap any hidden field in an element, label, layout row, grid
|
|
1054
|
-
item, or visual field component; future hidden tokens must follow the same
|
|
1055
|
-
rule so they cannot disturb the theme's form layout.
|
|
1056
|
-
- Keep contact form layout accessible: every field has a label, keyboard focus
|
|
1057
|
-
is visible, checkbox/radio options are grouped clearly, and error messages
|
|
1058
|
-
are readable on mobile.
|
|
1059
|
-
|
|
1060
|
-
## AI metadata
|
|
1061
|
-
|
|
1062
|
-
Every page component and layout component must declare useful `ai.instructions`. Missing component-level instructions fail `suda theme check` and block publish.
|
|
1063
|
-
|
|
1064
|
-
Good component instructions explain:
|
|
1065
|
-
|
|
1066
|
-
- Purpose: what content or business role the component serves.
|
|
1067
|
-
- Use when / avoid when: how AI should choose this component instead of a similar one.
|
|
1068
|
-
- Placement: where it normally belongs in a page.
|
|
1069
|
-
- Frequency: whether it should appear once, multiple times, or only near another section.
|
|
1070
|
-
- Composition: required neighboring content or local blocks, when relevant.
|
|
1071
|
-
|
|
1072
|
-
- For icons, expose props with `{ type: "icon" }`, type values as `SudaIconName`, and render them with `SudaIcon` from `@sudajs/theme-engine/icons`. Store canonical Lucide names such as `"rocket"` or `"mouse-pointer-click"` (not `"lucide-rocket"`), or namespaced Simple Icons brand names such as `"simple-icons:github"`. Do not keep theme-local icon maps, emoji/icon switch statements, or custom SVG icon registries unless the theme truly needs a bespoke graphic.
|
|
1073
|
-
- Add optional field-level `ai.instructions`, `ai.required`, or `ai.exclude` when a prop needs generation guidance beyond its label and type.
|
|
1074
|
-
- Use `ai.exclude: true` only for structural or editor-only components, and still provide instructions explaining why AI must not generate them. `PageOutlet` is the standard example.
|
|
1075
|
-
|
|
1076
|
-
## Block slot authoring rules
|
|
1077
|
-
|
|
1078
|
-
Use `blockSlots` when a section owns controlled local content such as hero
|
|
1079
|
-
actions, pricing cards, feature rows, stats, timeline items, or contact
|
|
1080
|
-
methods. Keep ordinary section props in `fields`.
|
|
1081
|
-
|
|
1082
|
-
- Type the slot prop as Puck `Slot` and declare `blockSlots` with the same prop key.
|
|
1083
|
-
- Do not create a native `fields.<key>.type = "slot"` in page components.
|
|
1084
|
-
- Define local block kinds under `blockSlots.<slotName>.blocks`.
|
|
1085
|
-
- Give every local block `label`, useful fields, `defaultProps`, `render`, and
|
|
1086
|
-
optional `ai.instructions` when the block needs generation guidance.
|
|
1087
|
-
- Keep local blocks specific to the owning section. They are not reusable global page components.
|
|
1088
|
-
- Do not add nested `blockSlots` to local blocks. Block slots are one level deep in the theme authoring API.
|
|
1089
|
-
- Do not put the slot prop in the host component's `defaultProps`; use `defaultBlocks` on the slot instead.
|
|
1090
|
-
- Do not hand-write `id` in local block `defaultProps`, `defaultBlocks[].props`, or starter page nested block props. The editor/runtime owns block ids.
|
|
1091
|
-
- 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.
|
|
1092
|
-
|
|
1093
|
-
Example:
|
|
3
|
+
This repository is a standalone Vite + React + Tailwind Suda theme generated by
|
|
4
|
+
`suda theme init`. Build a complete, publishable theme with theme-specific
|
|
5
|
+
sections, realistic templates, scoped styling, assets, and AI metadata.
|
|
6
|
+
|
|
7
|
+
## Required reading
|
|
8
|
+
|
|
9
|
+
Read this file before every task. Then read the matching focused guide before
|
|
10
|
+
editing that area. These guides are part of the contract, not optional
|
|
11
|
+
background:
|
|
12
|
+
|
|
13
|
+
| Task | Required guide |
|
|
14
|
+
| ------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
|
|
15
|
+
| Sections, fields, defaults, menus, icons, AI metadata, or block slots | `docs/agent-guides/component-authoring.md` |
|
|
16
|
+
| CMS, posts, starter pages, page data, or locales | `docs/agent-guides/templates-and-locales.md` |
|
|
17
|
+
| Header, Footer, PageOutlet, sticky/fixed UI, transparent navigation, carousel, editor CSS, or client behavior | `docs/agent-guides/editor-compatibility.md` |
|
|
18
|
+
| Theme contract, layout, design tokens, CSS, assets, or contact forms | `docs/agent-guides/design-and-runtime.md` |
|
|
19
|
+
|
|
20
|
+
Read every guide that applies when a change crosses concerns. Follow the
|
|
21
|
+
generated types and installed package APIs when they are more specific than an
|
|
22
|
+
example. Do not guess contracts from memory.
|
|
23
|
+
|
|
24
|
+
## Non-negotiable rules
|
|
25
|
+
|
|
26
|
+
- Keep `renderMode: "ssr"` and export a complete `ThemeModule` from
|
|
27
|
+
`src/index.tsx`: `manifest`, `pageConfig`, `layoutConfig`, `defaultLayout`,
|
|
28
|
+
`starterPages`, and `cmsTemplates`.
|
|
29
|
+
- Keep page sections in `pageConfig` and shared chrome in `layoutConfig`.
|
|
30
|
+
Header and Footer render exactly once from the layout, with `PageOutlet`
|
|
31
|
+
between them.
|
|
32
|
+
- Create theme-specific sections. Do not register `createEngineComponents()`,
|
|
33
|
+
`createBaseBlocks()`, or `createContainers()` as the theme's page sections.
|
|
34
|
+
Suda adds its generic content blocks separately.
|
|
35
|
+
- Keep persisted props JSON-serializable. Never store functions, React nodes,
|
|
36
|
+
class instances, media database ids, or absolute local filesystem paths.
|
|
37
|
+
- All normal field and prop defaults belong only in component `defaultProps`.
|
|
38
|
+
Never invent display defaults inside render code with `||`, `??`, a ternary,
|
|
39
|
+
a default function parameter, or a destructuring default.
|
|
40
|
+
- Every editor-facing label must use `t(...)` and exist in
|
|
41
|
+
`src/locales/en.json`. Public content and `defaultProps` must not use the
|
|
42
|
+
editor translation helper.
|
|
43
|
+
- Every page and layout component must declare useful `ai.instructions`.
|
|
44
|
+
- Top-level page data types must be public keys from `pageConfig.components`,
|
|
45
|
+
and every top-level item must have a stable `props.id`.
|
|
46
|
+
- Block-slot items use short local kinds such as `"button"`; never author
|
|
47
|
+
`__suda_local_block__/...` types or local-block ids.
|
|
48
|
+
- Theme assets use `themeAsset("assets/...")` in authored data and
|
|
49
|
+
`resolveAsset(...)` when rendering editable media. Never hand-write published
|
|
50
|
+
theme asset URLs.
|
|
51
|
+
- Keep theme CSS scoped. Do not use global resets or vendor edits that can
|
|
52
|
+
damage the Puck editor.
|
|
53
|
+
- Do not edit `dist/`; regenerate it through the build.
|
|
54
|
+
|
|
55
|
+
## Project structure
|
|
56
|
+
|
|
57
|
+
Keep every page section in its own `.tsx` file. Do not collect multiple section
|
|
58
|
+
configs and render implementations in a single `sections.tsx`, `config.tsx`, or
|
|
59
|
+
other large module. Use kebab-case names and a small `src/sections/index.ts` to
|
|
60
|
+
export configs or assemble the registry.
|
|
61
|
+
|
|
62
|
+
```txt
|
|
63
|
+
src/
|
|
64
|
+
sections/
|
|
65
|
+
hero.tsx
|
|
66
|
+
booking.tsx
|
|
67
|
+
values.tsx
|
|
68
|
+
faq-directory.tsx
|
|
69
|
+
review-marquee.tsx
|
|
70
|
+
index.ts
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Keep section-specific prop types and tightly coupled helpers with their section.
|
|
74
|
+
Extract shared code only when multiple sections genuinely reuse it.
|
|
75
|
+
|
|
76
|
+
## Component fundamentals
|
|
77
|
+
|
|
78
|
+
- Name public component keys with concise PascalCase visitor-facing patterns,
|
|
79
|
+
such as `HeroImage`, `FeatureCards`, or `TestimonialsCarousel`. Avoid theme
|
|
80
|
+
names and filler suffixes such as `Section`, `Component`, `New`, or `Custom`.
|
|
81
|
+
- Prefer semantic variant names. Use numbered variants only when the variants
|
|
82
|
+
truly share a purpose and no concise name describes the difference.
|
|
83
|
+
- Keep `fields`, TypeScript props, `defaultProps`, and `render` aligned. Every
|
|
84
|
+
normal field needs a sensible serializable default.
|
|
85
|
+
- Use the most specific field: `url`, `image`, `video`, `media`, `icon`,
|
|
86
|
+
`color`, `range`, `spacing`, `posts`, `select`, `radio`, `array`, or `object`.
|
|
87
|
+
Do not model structured or semantic values as generic text.
|
|
88
|
+
- Use `visibleIf` for synchronous sibling-dependent visibility. Use
|
|
89
|
+
`resolveFields` only when `visibleIf` cannot express the behavior.
|
|
90
|
+
- Expose a `columns` prop for multi-column grids and lists.
|
|
91
|
+
- Use ordinary fields for scalar and structured props. Use `blockSlots` only
|
|
92
|
+
for controlled nested items editors and AI may add, remove, or reorder.
|
|
93
|
+
- Native Puck slots are reserved for layout/container primitives. Page sections
|
|
94
|
+
must not declare native `slot` fields, legacy DropZones, or `zones`.
|
|
95
|
+
|
|
96
|
+
Default ownership is strict:
|
|
1094
97
|
|
|
1095
98
|
```tsx
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
type
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
};
|
|
1105
|
-
|
|
1106
|
-
export const Hero: SudaComponentConfig<HeroProps> = {
|
|
1107
|
-
label: t("sections.hero.label"),
|
|
1108
|
-
ai: {
|
|
1109
|
-
instructions:
|
|
1110
|
-
"Primary page introduction for landing pages. Use once near the top with one or two local action blocks.",
|
|
1111
|
-
},
|
|
1112
|
-
fields: {
|
|
1113
|
-
title: { type: "text", label: t("sections.hero.fields.title") },
|
|
1114
|
-
},
|
|
1115
|
-
blockSlots: {
|
|
1116
|
-
actions: {
|
|
1117
|
-
label: t("sections.hero.blocks.actions"),
|
|
1118
|
-
blocks: {
|
|
1119
|
-
button: {
|
|
1120
|
-
label: t("sections.hero.blocks.button"),
|
|
1121
|
-
ai: {
|
|
1122
|
-
instructions:
|
|
1123
|
-
"Primary or secondary hero action. Use one or two buttons with clear labels and valid links.",
|
|
1124
|
-
},
|
|
1125
|
-
fields: {
|
|
1126
|
-
label: { type: "text", label: t("sections.hero.blocks.buttonLabel") },
|
|
1127
|
-
href: { type: "url", label: t("sections.hero.blocks.buttonHref") },
|
|
1128
|
-
},
|
|
1129
|
-
defaultProps: {
|
|
1130
|
-
label: "Get started",
|
|
1131
|
-
href: "/contact",
|
|
1132
|
-
},
|
|
1133
|
-
render: ({ label, href }) => <a href={href}>{label}</a>,
|
|
1134
|
-
},
|
|
1135
|
-
},
|
|
1136
|
-
defaultBlocks: [{ kind: "button" }],
|
|
1137
|
-
},
|
|
1138
|
-
},
|
|
1139
|
-
defaultProps: {
|
|
1140
|
-
title: "Build with SudaCloud",
|
|
1141
|
-
},
|
|
1142
|
-
render: ({ title, actions: Actions }) => (
|
|
1143
|
-
<section>
|
|
1144
|
-
<h1>{title}</h1>
|
|
1145
|
-
<Actions />
|
|
1146
|
-
</section>
|
|
1147
|
-
),
|
|
99
|
+
// Wrong: render invents content.
|
|
100
|
+
<div>{val || "defaultValue"}</div>;
|
|
101
|
+
|
|
102
|
+
// Correct: defaultProps owns the default.
|
|
103
|
+
export const Example: SudaComponentConfig<ExampleProps> = {
|
|
104
|
+
fields: { val: { type: "text", label: t("sections.example.fields.val") } },
|
|
105
|
+
defaultProps: { val: "defaultValue" },
|
|
106
|
+
render: ({ val }) => <div>{val}</div>,
|
|
1148
107
|
};
|
|
1149
108
|
```
|
|
1150
109
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
-
|
|
1175
|
-
|
|
1176
|
-
-
|
|
1177
|
-
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
110
|
+
Conditional rendering is valid when absence intentionally removes optional UI;
|
|
111
|
+
it must not substitute placeholder copy, labels, links, icons, or menu items.
|
|
112
|
+
|
|
113
|
+
## Icons
|
|
114
|
+
|
|
115
|
+
- Fixed icons are acceptable for interaction and UI-state semantics such as
|
|
116
|
+
menu, close, previous/next, expand/collapse, and loading controls.
|
|
117
|
+
- Content icons such as feature, service, contact-method, and social-brand
|
|
118
|
+
icons are props. Expose `{ type: "icon" }`, type values as `SudaIconName`,
|
|
119
|
+
set initial selections in `defaultProps`, and render them with `SudaIcon` from
|
|
120
|
+
`@sudajs/theme-engine/icons`. Do not hardcode content icons in `render`.
|
|
121
|
+
- Use canonical Lucide names such as `"rocket"` and namespaced Simple Icons
|
|
122
|
+
names such as `"simple-icons:github"`. The theme engine supports both; do not
|
|
123
|
+
add another icon library unless neither can represent a required icon.
|
|
124
|
+
|
|
125
|
+
## Page data fundamentals
|
|
126
|
+
|
|
127
|
+
- Wrap hand-authored page data with `defineSudaPageData(pageConfig, data)` when
|
|
128
|
+
practical so TypeScript checks public component keys and block-slot kinds.
|
|
129
|
+
- Starter pages live in `src/templates.ts`, use `root: { props: {} }`, contain
|
|
130
|
+
realistic content, and include one home page with `isHome: true`.
|
|
131
|
+
- Provide exactly the four CMS templates `posts`, `post`, `tags`, and `tag` as
|
|
132
|
+
`{ title, data }` entries. Do not invent CMS template variants.
|
|
133
|
+
- CMS-only main sections read `getCmsContent(puck?.metadata)`. Ordinary page
|
|
134
|
+
post sections use one top-level `posts` field and `getPostResource(...)`.
|
|
135
|
+
- Starter page slugs preview at root-like routes such as `/index` and
|
|
136
|
+
`/contact-us`; never use `/pages/...` as a preview prefix.
|
|
137
|
+
|
|
138
|
+
## Editor and runtime fundamentals
|
|
139
|
+
|
|
140
|
+
- Components return one stable semantic real DOM root when positioning,
|
|
141
|
+
selection, spacing, or background matters. Do not rely on a Fragment or
|
|
142
|
+
`display: contents` for such roots.
|
|
143
|
+
- Derive editor state from `puck?.metadata?.isEditor === true`, expose a stable
|
|
144
|
+
theme-root marker, and scope editor fixes below it.
|
|
145
|
+
- Puck may place `data-puck-component` and inline `position: relative` on the
|
|
146
|
+
component's actual root. Account for that before implementing sticky, fixed,
|
|
147
|
+
absolute, overlap, or transparent navigation behavior.
|
|
148
|
+
- Do not restore pointer events globally in edit mode. Test interactions in
|
|
149
|
+
editor preview and on the public site.
|
|
150
|
+
- Browser setup belongs in an optional `clientHooks` export from
|
|
151
|
+
`src/client.ts`. Do not create `src/runtime.client.ts(x)`; the CLI owns the
|
|
152
|
+
generated runtime.
|
|
153
|
+
|
|
154
|
+
## Design and assets
|
|
155
|
+
|
|
156
|
+
- Define one editable design system in `sourceManifest.designSystem`, expose it
|
|
157
|
+
at the layout root, resolve it once there, and render components through the
|
|
158
|
+
resulting `--suda-*` CSS variables.
|
|
159
|
+
- Tailwind token mappings use `@theme inline`. Hand-written CSS consumes
|
|
160
|
+
`--suda-*` variables directly, not `--color-*` or `--radius-*` aliases.
|
|
161
|
+
- Keep typography, spacing, containers, buttons, cards, forms, and media frames
|
|
162
|
+
consistent across ordinary sections, CMS sections, and starter pages.
|
|
163
|
+
- Theme-local assets live under top-level `assets/`. Required preview images are
|
|
164
|
+
`assets/preview/desktop.png`, `tablet.png`, and `mobile.png`.
|
|
165
|
+
- Contact sections render host-provided `metadata.contactForm`; the platform
|
|
166
|
+
owns configuration, validation, submission, notifications, and limits.
|
|
167
|
+
|
|
168
|
+
## Workflow
|
|
169
|
+
|
|
170
|
+
Before editing:
|
|
171
|
+
|
|
172
|
+
1. Read the required focused guide(s).
|
|
173
|
+
2. Inspect the existing source, generated types, and established theme helpers.
|
|
174
|
+
3. Identify every affected contract: fields/defaults/render, locales, page data,
|
|
175
|
+
assets, editor behavior, and templates.
|
|
176
|
+
|
|
177
|
+
When adding or changing a section:
|
|
178
|
+
|
|
179
|
+
1. Define serializable props in its dedicated `.tsx` file.
|
|
180
|
+
2. Add `label`, useful `ai.instructions`, typed `fields`, optional
|
|
181
|
+
`blockSlots`, `defaultProps`, and `render`.
|
|
182
|
+
3. Add all editor label keys to `src/locales/en.json`.
|
|
183
|
+
4. Register it in `pageConfig` and the appropriate translated category.
|
|
184
|
+
5. Add realistic usage to starter pages or CMS templates where appropriate.
|
|
185
|
+
6. Verify public rendering plus editor desktop, tablet, and mobile behavior.
|
|
186
|
+
|
|
187
|
+
Before handoff:
|
|
1185
188
|
|
|
1186
189
|
```bash
|
|
1187
|
-
pnpm install
|
|
1188
190
|
pnpm lint
|
|
1189
191
|
pnpm typecheck
|
|
1190
|
-
pnpm
|
|
1191
|
-
pnpm dev # Vite-powered local preview
|
|
1192
|
-
pnpm build # Vite build + Suda artifact finalize + AI metadata check
|
|
192
|
+
pnpm build
|
|
1193
193
|
pnpm validate
|
|
1194
194
|
suda theme check
|
|
1195
195
|
```
|
|
1196
196
|
|
|
1197
|
-
|
|
197
|
+
Run `suda theme capture` when visuals change. Do not use removed skip flags or
|
|
198
|
+
bypass build, validation, AI metadata, locale, or screenshot checks.
|