create-kywi-app 0.6.3 → 0.6.4
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/assets/agent-patterns.md +221 -16
- package/assets/kywi-custom-modules-skill.md +172 -0
- package/lib/templates.mjs +78 -11
- package/package.json +1 -1
package/assets/agent-patterns.md
CHANGED
|
@@ -17,6 +17,27 @@ edit content in the admin, or do they need a developer?"* If the answer is
|
|
|
17
17
|
Static code is for structure and brand chrome. Content — anything with words,
|
|
18
18
|
images, prices, names, dates — belongs in Kywi.
|
|
19
19
|
|
|
20
|
+
## Where the owner edits — the surfaces you're building for
|
|
21
|
+
|
|
22
|
+
"Editable" isn't an abstraction. It means the thing appears in one of three
|
|
23
|
+
shipped surfaces:
|
|
24
|
+
|
|
25
|
+
- **Admin → Content / Site Tree** — the fields on a node (which fields exist is
|
|
26
|
+
decided by the type, §2).
|
|
27
|
+
- **Admin → the layout editor** — regions, sections, columns, modules, page
|
|
28
|
+
variants, and section-level personalized/A-B containers with an arm switcher.
|
|
29
|
+
- **The public site itself** — a signed-in admin gets a toolbar on every public
|
|
30
|
+
page (page title, status, **Edit this page**), which opens that same layout
|
|
31
|
+
editor *in place*: the real page, the site's own stylesheet, chrome docked to
|
|
32
|
+
the viewport edges. Text-bearing modules are edited by typing on the page.
|
|
33
|
+
|
|
34
|
+
The build rule that follows: **prefer structures that show up in those
|
|
35
|
+
surfaces.** A headline in a `heading` module is editable in all three; the same
|
|
36
|
+
headline in a hand-written component is editable in none. Custom modules (§5)
|
|
37
|
+
must expose their copy as first-class props for the same reason — and a module
|
|
38
|
+
whose *first* declared prop is `text`, `textarea` or `richText` becomes
|
|
39
|
+
inline-editable on the front of the site for free.
|
|
40
|
+
|
|
20
41
|
## Before you build: design the content model and get sign-off
|
|
21
42
|
|
|
22
43
|
The most expensive failure in a CMS build is a poorly planned content model —
|
|
@@ -57,6 +78,8 @@ the full proposal — ten minutes of sign-off beats re-entering fifty nodes.
|
|
|
57
78
|
| Filter/group content by topic, industry, audience | Categories & tags, queried by feeds (§3) |
|
|
58
79
|
| Copy-paste a CTA/banner/snippet across pages | Save it as a reusable component (§4) |
|
|
59
80
|
| Write page sections as JSX | Build them as layout sections/modules the owner can edit (§5) |
|
|
81
|
+
| Build a page as one column of full-width headings and paragraphs | Compose: section bands, columns, object modules (§5) |
|
|
82
|
+
| Put a custom module's editable copy in a `json` prop | One `text`/`textarea`/`richText` prop per field the owner edits (§5) |
|
|
60
83
|
| Hand-code a `<form>` | Build it in the Forms admin, place the Form module (§6) |
|
|
61
84
|
| Drop images into `/public` | Upload to the Media library (§7) |
|
|
62
85
|
| Build segment-specific or experimental UI unprompted | Ask the owner first — offer personalization/A-B with examples (§8) |
|
|
@@ -113,6 +136,28 @@ by `kywi migrate`. When in doubt, admin-created.
|
|
|
113
136
|
node's own page is the detail view. Listing + detail with zero custom code.
|
|
114
137
|
- Don't shadow built-in fields (slug, body) with custom ones — Kywi will block
|
|
115
138
|
it; use the built-ins.
|
|
139
|
+
- The scaffold ships one config type (`page`) so a fresh site can publish
|
|
140
|
+
immediately. That's the floor, not the pattern — add your types in the Type
|
|
141
|
+
Designer, and only put a type in `contentTypes` when code depends on its shape.
|
|
142
|
+
|
|
143
|
+
**Creating types programmatically.** The admin UI is one client of the REST API;
|
|
144
|
+
an agent uses the same endpoints, and the order matters:
|
|
145
|
+
|
|
146
|
+
1. `POST /api/v1/content-types` — create the type.
|
|
147
|
+
2. `POST /api/v1/content-types/:type/fields` — add each field.
|
|
148
|
+
3. `POST /api/v1/content-types/apply` — **materialize the schema** (diffs the
|
|
149
|
+
type and runs the CREATE/ALTER for its side table).
|
|
150
|
+
4. *Then* create nodes — before the apply step the side table doesn't exist and
|
|
151
|
+
the write fails (cleanly rolled back since 0.6.3, but still a failure).
|
|
152
|
+
|
|
153
|
+
**Seed with an idempotent script.** Put structural setup in a
|
|
154
|
+
`scripts/seed-content.mjs` that logs in (`POST /api/v1/auth/login`) and *checks
|
|
155
|
+
before creating* every object — list types → create the missing, list fields →
|
|
156
|
+
add the missing, same for taxonomy, audiences, experiments, self-ID. It is
|
|
157
|
+
re-runnable, reviewable in git, and rebuilds the site on a fresh database. Build
|
|
158
|
+
pages, layouts and feeds over MCP or in the admin instead. The script seeds
|
|
159
|
+
structure; it doesn't own content — once the owner edits a node, a re-run must
|
|
160
|
+
not clobber it.
|
|
116
161
|
|
|
117
162
|
## 3. Taxonomy: categories & tags
|
|
118
163
|
|
|
@@ -146,22 +191,138 @@ newsletter signup blocks, campaign snippets, "as seen in" strips — should be a
|
|
|
146
191
|
## 5. Page layouts & modules — not hardcoded JSX
|
|
147
192
|
|
|
148
193
|
Pages the owner should be able to restructure — landing pages, the home page,
|
|
149
|
-
campaign pages — should be built **in the layout editor** (sections
|
|
150
|
-
modules), rendered through Kywi's layout renderer. A scaffolded app
|
|
151
|
-
renders saved layouts on every content page; keep it that way.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
194
|
+
campaign pages — should be built **in the layout editor** (regions → sections →
|
|
195
|
+
columns → modules), rendered through Kywi's layout renderer. A scaffolded app
|
|
196
|
+
already renders saved layouts on every content page; keep it that way.
|
|
197
|
+
|
|
198
|
+
**Compose.** A page is not a stack of full-width `heading` + `richText` blocks.
|
|
199
|
+
That is the shape an agent falls into when it doesn't know the palette, and it
|
|
200
|
+
reads as a styled document rather than a site. Sections are bands (each owns its
|
|
201
|
+
background and padding), `columns` splits them, and the object modules below do
|
|
202
|
+
the rest.
|
|
203
|
+
|
|
204
|
+
### The built-in palette
|
|
205
|
+
|
|
206
|
+
`name` is the exact `type` value in a layout node and in MCP `add_module`. A
|
|
207
|
+
custom module that reuses one of these names **overrides** the built-in
|
|
208
|
+
everywhere.
|
|
209
|
+
|
|
210
|
+
**Layout** — the skeleton of a page.
|
|
211
|
+
|
|
212
|
+
| `name` | Purpose |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `section` | Top-level band in a region; owns background colour and vertical padding |
|
|
215
|
+
| `container` | Width-capped, padded wrapper inside a section |
|
|
216
|
+
| `columns` | Multi-column split (`preset` picks the ratio, `gap` the spacing) |
|
|
217
|
+
| `spacer` | Fixed vertical gap |
|
|
218
|
+
| `divider` | Horizontal rule (colour, thickness) |
|
|
219
|
+
|
|
220
|
+
**Elements** — the primitives.
|
|
221
|
+
|
|
222
|
+
| `name` | Purpose |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `richText` | WYSIWYG prose — the default home for owner-authored copy |
|
|
225
|
+
| `heading` | One heading, with its level (h1–h6) |
|
|
226
|
+
| `text` | Plain paragraph text |
|
|
227
|
+
| `image` | Media-library image with alt text and caption |
|
|
228
|
+
| `button` | Labelled link with a style variant |
|
|
229
|
+
| `html` | Raw HTML escape hatch (sanitized) — not a layout tool |
|
|
230
|
+
| `blockquote` | Quote with author and citation URL |
|
|
231
|
+
| `badge` | Small label/pill, optionally linked |
|
|
232
|
+
| `icon` | Named icon with size, colour, accessible label |
|
|
233
|
+
| `video` | Video file with poster, autoplay/loop/muted |
|
|
234
|
+
| `embed` | Third-party embed code |
|
|
235
|
+
|
|
236
|
+
**Objects** — the composed blocks.
|
|
237
|
+
|
|
238
|
+
| `name` | Purpose |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `card` | Title + body + image + link — the generic tile |
|
|
241
|
+
| `accordion` | One collapsible title/content panel |
|
|
242
|
+
| `tabs` | Tabbed panels (`tabs` JSON prop) |
|
|
243
|
+
| `carousel` | Rotating slides (`items` JSON prop, autoplay/interval) |
|
|
244
|
+
| `testimonial` | Quote + author + role + avatar |
|
|
245
|
+
| `hero` | Headline, subtext, background image, one CTA button |
|
|
246
|
+
| `cta` | Headline + subtext + button — the conversion band |
|
|
247
|
+
| `pricing` | One plan: name, price, period, features, button |
|
|
248
|
+
| `faq` | One question/answer pair, optionally open by default |
|
|
249
|
+
| `alert` | Inline notice (info/warning/…), optionally dismissible |
|
|
250
|
+
| `countdown` | Counts down to a target date |
|
|
251
|
+
| `feedDisplay` | Renders a Feed (§1) — title, summary, image per item |
|
|
252
|
+
| `tagCloud` | Weighted tag list |
|
|
253
|
+
| `relatedContent` | Manual list of related links (`items` JSON prop) |
|
|
254
|
+
| `categorySummary` | One category: name, description, image, count |
|
|
255
|
+
| `categoryList` | Auto-listed child categories under a parent slug |
|
|
256
|
+
| `comments` | Moderated comment thread for the current node |
|
|
257
|
+
|
|
258
|
+
**Navigation** — wayfinding.
|
|
259
|
+
|
|
260
|
+
| `name` | Purpose |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `breadcrumbs` | Trail of ancestor links |
|
|
263
|
+
| `navMenu` | A named menu (`menuSlug`) rendered to a depth |
|
|
264
|
+
| `navigation` | Site navigation with a style variant and depth |
|
|
265
|
+
| `siteMap` | Nested list of the site tree |
|
|
266
|
+
| `searchBox` | Compact search input posting to a results URL |
|
|
267
|
+
| `search` | Full search form (method, label, results URL) |
|
|
268
|
+
|
|
269
|
+
**Personalization**, **Compliance**, and **Saved** (references to things
|
|
270
|
+
authored elsewhere in the admin).
|
|
271
|
+
|
|
272
|
+
| `name` | Purpose |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `variantContainer` | Module-level arms revealed client-side per audience — read §8 first |
|
|
275
|
+
| `personalizationBadge` | Mount point for the "you're seeing X" transparency control |
|
|
276
|
+
| `socialShare` | Share links for a URL/title across networks |
|
|
277
|
+
| `map` | Embedded map at a lat/lng or address |
|
|
278
|
+
| `cookieConsent` | Consent banner with message + privacy-policy link |
|
|
279
|
+
| `component` | Places a saved reusable component by slug (§4) |
|
|
280
|
+
| `form` | Renders a form built in the Forms admin (§6) |
|
|
281
|
+
| `formEmbed` | Embeds a form by slug — lighter-weight placement of the same |
|
|
282
|
+
|
|
283
|
+
### Which do I reach for? (in order)
|
|
284
|
+
|
|
285
|
+
1. **A built-in already does it** → use it. Read the tables before inventing
|
|
286
|
+
anything; "the palette is thin" is almost always "I didn't look".
|
|
287
|
+
2. **Prose, a heading, an image, a link** → `richText` / `heading` / `image` /
|
|
288
|
+
`button`. Never build a module to hold copy.
|
|
289
|
+
3. **A repeating set of items** → a collection: folder + Feed + `feedDisplay`
|
|
290
|
+
(§1). Know the ceiling: Feed Display renders **title, summary and image
|
|
291
|
+
only**. If the cards need a price or a job title, denormalize it into
|
|
292
|
+
`summary` at authoring time, or build a custom module that reads the feed.
|
|
293
|
+
4. **The same designed section on several pages** → build it once and **Save as
|
|
294
|
+
Component** (§4). Zero code, live references, owner-editable.
|
|
295
|
+
5. **A composition or brand block the palette genuinely lacks** — media⇄text
|
|
296
|
+
split, stat callout, steps band, tabbed code sample, diagram frame → **one
|
|
297
|
+
custom module, reused**. Not a one-off `html` blob pasted per page.
|
|
298
|
+
|
|
299
|
+
### Custom modules
|
|
300
|
+
|
|
301
|
+
Three steps, all required: `defineModule({ name, label, component, category,
|
|
302
|
+
props })` in `kywi.config.ts` `modules: []`; a `'use client'` React component
|
|
303
|
+
taking `{ props }: { props: Record<string, unknown> }`; an entry in
|
|
304
|
+
`lib/modules.tsx` `moduleComponents` keyed by the same `name`. Skip the third
|
|
305
|
+
and the module renders "Unknown module" in the editor *and* on the live page.
|
|
306
|
+
Full recipe, worked example and hazards: the **`kywi-custom-modules` skill**.
|
|
307
|
+
|
|
308
|
+
**Design the props for the owner, not for yourself.** The props panel gives the
|
|
309
|
+
owner real controls for `text`/`textarea`/`richText` (text inputs),
|
|
310
|
+
`image`/`file` (media picker), `boolean` (checkbox) and `number`/`date`/`color`
|
|
311
|
+
(native inputs); `select`, `slug`, `relationship` and `multiSelect` fall back to
|
|
312
|
+
a plain text input, so name the allowed values in the label. A `json` prop is a
|
|
313
|
+
raw JSON textarea — developer territory. A "three stats" module with `stats: {
|
|
314
|
+
type: 'json' }` has taken the copy away from the owner; the same module with
|
|
315
|
+
`stat1Value` / `stat1Label` … as `text` props hasn't.
|
|
316
|
+
|
|
317
|
+
**Declare the primary copy prop first.** The front-of-site editor makes a
|
|
318
|
+
module's *first* prop inline-editable on the page when its type is `text`,
|
|
319
|
+
`textarea` or `richText` — so `props: { headline, … }` can be typed over
|
|
320
|
+
directly on the live page, and `props: { variant, headline, … }` cannot.
|
|
321
|
+
|
|
322
|
+
Also: create **saved layouts** (Layouts admin) as page templates — "Landing
|
|
323
|
+
page", "Case study" — so new pages start from a consistent skeleton. Hand-written
|
|
324
|
+
JSX stays fine for genuinely fixed chrome (a bespoke 404, legal boilerplate) —
|
|
325
|
+
but if marketing will ever want to swap a headline, it's a layout page.
|
|
165
326
|
|
|
166
327
|
## 6. Forms: always the Forms builder
|
|
167
328
|
|
|
@@ -209,6 +370,48 @@ default variant must stand alone (personalization is progressive enhancement);
|
|
|
209
370
|
one experiment per conversion goal, and let it conclude before layering more.
|
|
210
371
|
If no, skip it — the machinery is there when they grow into it.
|
|
211
372
|
|
|
373
|
+
### Variant containers: pick the right shape
|
|
374
|
+
|
|
375
|
+
Two different things are called `variantContainer`, and they are not
|
|
376
|
+
interchangeable.
|
|
377
|
+
|
|
378
|
+
- **Section-level** — a *region* node, sibling of sections (`defaultSections` +
|
|
379
|
+
`variants[].sections`). Resolved **on the server** from the visitor's audience
|
|
380
|
+
or experiment arm, so the first paint is already correct: no flash, and the
|
|
381
|
+
page is complete with JavaScript off. This is the default for anything the
|
|
382
|
+
server can know — rule-based audiences (UTM, referrer, the `kywi_audience`
|
|
383
|
+
pin) and A/B experiments. Since 0.6.3 it also renders in the layout editor as
|
|
384
|
+
a badged block with an **arm switcher**, each arm editable with the ordinary
|
|
385
|
+
section/column/module tools, so server-resolution no longer costs the owner
|
|
386
|
+
their editing surface.
|
|
387
|
+
- **Module-level** — the `variantContainer` *module*, placed in a column; its
|
|
388
|
+
arms are HTML strings (`defaultContent`, `variants: [{audienceId, label,
|
|
389
|
+
content}]`). Every arm ships in the HTML (default visible, the rest
|
|
390
|
+
`display:none`) and the browser runtime reveals the matching one. Use it
|
|
391
|
+
**only for signals the server cannot read at first paint** — in practice, a
|
|
392
|
+
self-ID answer held in `localStorage`.
|
|
393
|
+
|
|
394
|
+
Four traps, each of which fails silently:
|
|
395
|
+
|
|
396
|
+
1. **Use the layout module, not the React component.** `VariantContainer.tsx`
|
|
397
|
+
(`core/src/components/personalization/`) has a loading branch that renders
|
|
398
|
+
the skeleton and nothing else — a JS-off visitor gets an empty container —
|
|
399
|
+
and its `ab_test` path draws a fresh `Math.random()` per render, so the same
|
|
400
|
+
visitor sees a different arm on every refresh. The layout module
|
|
401
|
+
(`VariantContainerModule`) always renders the default arm.
|
|
402
|
+
2. **Leave `skeleton` off.** With `skeleton: true` the container ships
|
|
403
|
+
`aria-busy="true"` and only the client runtime clears it — a JS-off visitor
|
|
404
|
+
sits in a loading state forever.
|
|
405
|
+
3. **An audience match beats the experiment.** `resolveVariant` returns the
|
|
406
|
+
audience arm first, so a visitor who matches an audience never enters the
|
|
407
|
+
split. Don't run an experiment and an audience variant on the same container.
|
|
408
|
+
4. **The client runtime needs `/kywi.js`.** Self-ID, behavioral re-evaluation
|
|
409
|
+
and the transparency badge no-op silently unless the built
|
|
410
|
+
`@kywi-software/js` bundle is at `public/kywi.js`.
|
|
411
|
+
|
|
412
|
+
Standing rule: **the default arm must be complete on its own.** It is what
|
|
413
|
+
search engines, answer engines, and every opted-out visitor receive.
|
|
414
|
+
|
|
212
415
|
## 9. Editorial workflow: drafts, review, versions
|
|
213
416
|
|
|
214
417
|
For any site with more than one author — or an owner who wants a safety net —
|
|
@@ -288,6 +491,8 @@ early, so ask at scoping time.
|
|
|
288
491
|
- Marketing imagery in `/public`.
|
|
289
492
|
- A "blog" that is a folder of `.mdx` files the owner can't edit.
|
|
290
493
|
- One-off content types created in config for shapes the owner will evolve.
|
|
494
|
+
- A page built as a flat stack of full-width `heading` + `richText` modules.
|
|
495
|
+
- A custom module whose owner-facing copy lives in a `json` prop.
|
|
291
496
|
- Personalization built speculatively, with no owner request behind it.
|
|
292
497
|
- Direct-publish-only workflow on a multi-author site.
|
|
293
498
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kywi-custom-modules
|
|
3
|
+
description: Use when a page needs a block the built-in module palette doesn't have — a media/text split, stat callout, steps band, tabbed code sample, diagram frame, branded hero — or whenever you catch yourself pasting bespoke markup into an `html` module or writing a page section as JSX. Covers when a custom module is the right call, the three-step recipe, owner-editable props, and the hazards that break SSR, JS-off visitors, or the owner's editing surface.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build a custom module (only when the palette falls short)
|
|
7
|
+
|
|
8
|
+
A custom module is a React component the *developer* owns and the *owner*
|
|
9
|
+
places and configures. It's the right tool for composition the built-in palette
|
|
10
|
+
lacks — and the wrong tool for anything the palette already does, for copy, and
|
|
11
|
+
for one-off page decoration.
|
|
12
|
+
|
|
13
|
+
## Workflow
|
|
14
|
+
|
|
15
|
+
### 1. Confirm you need one
|
|
16
|
+
|
|
17
|
+
Walk the ladder in AGENTS.md §5 first: built-in module → `richText`/`heading`/
|
|
18
|
+
`image`/`button` → collection + `feedDisplay` → saved component → custom module.
|
|
19
|
+
|
|
20
|
+
**Build one when:**
|
|
21
|
+
|
|
22
|
+
- The page needs a *composition* the palette has no shape for: media⇄text
|
|
23
|
+
split, stat/metric callout row, numbered steps band, tabbed code sample,
|
|
24
|
+
diagram frame, a hero with a product visual beside the copy.
|
|
25
|
+
- The same brand-specific block will recur across pages and needs consistent
|
|
26
|
+
markup and styling.
|
|
27
|
+
- A built-in is 90% right but needs different markup — register a module with
|
|
28
|
+
the built-in's `name` to **override** it everywhere (editor and public site).
|
|
29
|
+
|
|
30
|
+
**Don't build one when:**
|
|
31
|
+
|
|
32
|
+
- A built-in covers it. Read the §5 palette tables; "the palette is thin" is
|
|
33
|
+
almost always "I didn't look".
|
|
34
|
+
- It's copy. Copy goes in `richText` — a module per paragraph is a smell.
|
|
35
|
+
- It's a repeating set of items. That's a collection (folder + feed +
|
|
36
|
+
`feedDisplay`), not a module with a hardcoded array.
|
|
37
|
+
- It'd be used once, on one page. Compose it from built-ins, or if the design
|
|
38
|
+
is genuinely bespoke chrome, keep it in the page component and accept that
|
|
39
|
+
the owner can't edit it — deliberately, and say so at handover.
|
|
40
|
+
- You'd need it because the owner's content doesn't fit `feedDisplay`'s
|
|
41
|
+
title/summary/image ceiling. Denormalizing into `summary` is usually cheaper.
|
|
42
|
+
|
|
43
|
+
### 2. Design the props before writing the component
|
|
44
|
+
|
|
45
|
+
The props *are* the owner's editing surface. Decide, field by field, who edits
|
|
46
|
+
it — then pick the type:
|
|
47
|
+
|
|
48
|
+
| Prop type | Editor control | Use for |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `text` | text input | headlines, eyebrows, labels, stat values |
|
|
51
|
+
| `textarea` | multi-line input | short body copy, quotes |
|
|
52
|
+
| `richText` | rich-text input | owner-authored HTML body copy |
|
|
53
|
+
| `image` / `file` | media picker | anything from the Media library |
|
|
54
|
+
| `boolean` | checkbox | layout flips (`reverse`), show/hide |
|
|
55
|
+
| `number` / `date` / `color` | native inputs | counts, deadlines, accents |
|
|
56
|
+
| `select` / `slug` / `relationship` | plain text input (today) | constrained values — name the options in the label |
|
|
57
|
+
| `json` | raw JSON textarea | **developer territory** — structure the owner never edits |
|
|
58
|
+
|
|
59
|
+
Two rules that decide whether the module is really editable:
|
|
60
|
+
|
|
61
|
+
- **Keep owner copy out of `json`.** `stats: {type:'json'}` hands the owner a
|
|
62
|
+
JSON blob; `stat1Value` / `stat1Label` / `stat2Value` … as `text` props hands
|
|
63
|
+
them fields. Use `json` only for structure that never changes after build.
|
|
64
|
+
- **Declare the primary copy prop first.** The front-of-site editor makes a
|
|
65
|
+
module's *first* prop inline-editable on the live page when its type is
|
|
66
|
+
`text`, `textarea` or `richText`. Put the headline first; put `variant` and
|
|
67
|
+
layout switches after it.
|
|
68
|
+
|
|
69
|
+
### 3. Register it — `defineModule` in `kywi.config.ts`
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { defineKywiConfig, defineModule } from '@kywi-software/core/config'
|
|
73
|
+
|
|
74
|
+
export default defineKywiConfig({
|
|
75
|
+
// …sites, themes, contentTypes…
|
|
76
|
+
modules: [
|
|
77
|
+
defineModule({
|
|
78
|
+
name: 'splitFeature', // the layout node `type` + MCP add_module type
|
|
79
|
+
label: 'Media / text split', // what the owner sees in the palette
|
|
80
|
+
component: 'splitFeature', // the key you'll map in lib/modules.tsx
|
|
81
|
+
category: 'Marketing', // groups it in the palette
|
|
82
|
+
props: {
|
|
83
|
+
heading: { type: 'text', label: 'Heading' }, // first = inline-editable
|
|
84
|
+
body: { type: 'richText', label: 'Body' },
|
|
85
|
+
image: { type: 'image', label: 'Image' },
|
|
86
|
+
imageAlt: { type: 'text', label: 'Image alt text' },
|
|
87
|
+
reverse: { type: 'boolean', label: 'Image on the left', defaultValue: false },
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 4. Write the component
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
// lib/split-feature.tsx
|
|
98
|
+
'use client'
|
|
99
|
+
import { resolveMediaRef } from '@kywi-software/core/layout'
|
|
100
|
+
|
|
101
|
+
export function SplitFeature({ props }: { props: Record<string, unknown> }) {
|
|
102
|
+
const src = resolveMediaRef(props.image) // media UUID → public file URL
|
|
103
|
+
return (
|
|
104
|
+
<div className={`split-feature${props.reverse === true ? ' is-reversed' : ''}`}>
|
|
105
|
+
<div className="split-feature__copy">
|
|
106
|
+
<h2>{String(props.heading ?? '')}</h2>
|
|
107
|
+
<div dangerouslySetInnerHTML={{ __html: String(props.body ?? '') }} />
|
|
108
|
+
</div>
|
|
109
|
+
{src ? <img src={src} alt={String(props.imageAlt ?? '')} /> : null}
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Every prop is `unknown` — coerce and default each one. A module that throws on a
|
|
116
|
+
missing prop breaks the editor canvas as well as the page.
|
|
117
|
+
|
|
118
|
+
### 5. Map it in `lib/modules.tsx`
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { SplitFeature } from './split-feature'
|
|
122
|
+
|
|
123
|
+
export const moduleComponents: ModuleComponentMap = {
|
|
124
|
+
splitFeature: SplitFeature,
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This one map is passed to both `KywiAdminApp` and `KywiLayout`, so the module
|
|
129
|
+
renders identically in the editor canvas and live. **Skip this step and the
|
|
130
|
+
module renders "Unknown module" in both** — the most common custom-module bug.
|
|
131
|
+
The host merges `{ ...BUILT_IN_MODULE_COMPONENTS, ...moduleComponents }`, so an
|
|
132
|
+
entry named after a built-in replaces it.
|
|
133
|
+
|
|
134
|
+
### 6. Hazards checklist
|
|
135
|
+
|
|
136
|
+
- **Unique ids need `React.useId()`.** A module-scope counter produces different
|
|
137
|
+
ids on server and client and breaks hydration. Same for `Date.now()` and
|
|
138
|
+
`Math.random()` in render.
|
|
139
|
+
- **Escape owner text inside `<script type="application/ld+json">`.** Replace
|
|
140
|
+
every `<` with the JS escape `\u003c`, break up `-->`, and escape the line
|
|
141
|
+
separators U+2028/U+2029 (matched via
|
|
142
|
+
`String.fromCharCode(0x2028, 0x2029)`) — otherwise a stray character in owner
|
|
143
|
+
copy ends the script tag early.
|
|
144
|
+
- **`dangerouslySetInnerHTML` is the pattern for `richText`/HTML props** — the
|
|
145
|
+
values are sanitized by Kywi before storage and on render. Don't reach for it
|
|
146
|
+
for plain `text` props.
|
|
147
|
+
- **Stay safe with JavaScript off.** A `'use client'` module still server-renders
|
|
148
|
+
its initial HTML — but anything that only appears after an effect is invisible
|
|
149
|
+
to crawlers and JS-off visitors. Interactivity is progressive enhancement over
|
|
150
|
+
complete server-rendered markup.
|
|
151
|
+
- **Style through theme tokens** (`var(--kywi-color-primary)`, `--kywi-spacing-*`)
|
|
152
|
+
in the site stylesheet, not hardcoded hex — the owner retunes the theme from
|
|
153
|
+
`kywi.config.ts` and your module should follow.
|
|
154
|
+
- **Don't fetch in the module.** Data comes from props (or, for feeds, the
|
|
155
|
+
host's server-side hydration). A module that fetches breaks the editor canvas.
|
|
156
|
+
|
|
157
|
+
### 7. Verify
|
|
158
|
+
|
|
159
|
+
1. Restart the dev server (config changes need a restart).
|
|
160
|
+
2. Open the layout editor: the module appears in the palette under its
|
|
161
|
+
`category`, with its `label`.
|
|
162
|
+
3. Place it, fill every prop from the props panel, save — it renders in the
|
|
163
|
+
canvas (not "Unknown module") and on the public page.
|
|
164
|
+
4. Open the public page as a signed-in admin → **Edit this page**: the first
|
|
165
|
+
text prop is editable by typing on the page.
|
|
166
|
+
5. Load the page with JavaScript disabled: the content is all there.
|
|
167
|
+
|
|
168
|
+
### 8. Handover note
|
|
169
|
+
|
|
170
|
+
Record the module in `CONTENT-MODEL.md` (reusable components / modules) with one
|
|
171
|
+
line per prop: what the owner changes and where. A module the owner doesn't know
|
|
172
|
+
exists is a module they'll ask a developer to change.
|
package/lib/templates.mjs
CHANGED
|
@@ -102,7 +102,7 @@ function packageJson(a) {
|
|
|
102
102
|
/** @param {Answers} a */
|
|
103
103
|
function kywiConfig(a) {
|
|
104
104
|
const providers = a.authProviders.map((p) => `'${p}'`).join(', ')
|
|
105
|
-
return `import { defineKywiConfig, defineSite, defineTheme } from '@kywi-software/core/config'
|
|
105
|
+
return `import { defineKywiConfig, defineSite, defineTheme, defineModule } from '@kywi-software/core/config'
|
|
106
106
|
import { assertProductionAuthSecret } from '@kywi-software/core/host'
|
|
107
107
|
|
|
108
108
|
// Refuse to build/start in production with a missing or dev-fallback AUTH_SECRET
|
|
@@ -179,9 +179,14 @@ export default defineKywiConfig({
|
|
|
179
179
|
}),
|
|
180
180
|
],
|
|
181
181
|
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
182
|
+
// Config-defined content types. "Page" ships so a fresh site can create and
|
|
183
|
+
// publish pages immediately — that's the floor, not the pattern.
|
|
184
|
+
//
|
|
185
|
+
// DEFAULT: create your own types in the admin's **Type Designer**
|
|
186
|
+
// (admin → Content Types). Admin-created types are runtime-managed, so the
|
|
187
|
+
// site owner can add a field next year without a developer or a migration.
|
|
188
|
+
// Add a type HERE only when the *developer* must own its shape — code depends
|
|
189
|
+
// on it, it's reviewed in git, it migrates with \`pnpm migrate\`.
|
|
185
190
|
contentTypes: [
|
|
186
191
|
{
|
|
187
192
|
name: 'page',
|
|
@@ -190,6 +195,29 @@ export default defineKywiConfig({
|
|
|
190
195
|
fields: [],
|
|
191
196
|
},
|
|
192
197
|
],
|
|
198
|
+
|
|
199
|
+
// Custom layout modules (\`defineModule\`) — the palette additions this project
|
|
200
|
+
// owns. Register a module here, then map its \`name\` → the React component that
|
|
201
|
+
// renders it in \`lib/modules.tsx\` (both the admin editor and the public site
|
|
202
|
+
// read that map). Reach for one only when the built-in palette genuinely lacks
|
|
203
|
+
// the composition; see AGENTS.md §5 and the \`kywi-custom-modules\` skill.
|
|
204
|
+
//
|
|
205
|
+
// modules: [
|
|
206
|
+
// defineModule({
|
|
207
|
+
// name: 'splitFeature', // layout node \`type\` + MCP add_module type
|
|
208
|
+
// label: 'Media / text split', // what the owner sees in the palette
|
|
209
|
+
// component: 'splitFeature', // the key you map in lib/modules.tsx
|
|
210
|
+
// category: 'Marketing',
|
|
211
|
+
// props: {
|
|
212
|
+
// // First text prop = inline-editable on the live page. Keep owner
|
|
213
|
+
// // copy in text/textarea/richText props, never in a json blob.
|
|
214
|
+
// heading: { type: 'text', label: 'Heading' },
|
|
215
|
+
// body: { type: 'richText', label: 'Body' },
|
|
216
|
+
// image: { type: 'image', label: 'Image' },
|
|
217
|
+
// },
|
|
218
|
+
// }),
|
|
219
|
+
// ],
|
|
220
|
+
modules: [],
|
|
193
221
|
})
|
|
194
222
|
`
|
|
195
223
|
}
|
|
@@ -2150,7 +2178,10 @@ in \`kywi.config.ts\` (\`modules: [defineModule({ name: 'pricingTable', … })]\
|
|
|
2150
2178
|
then map its \`name\` to the React component that renders it in \`lib/modules.tsx\`.
|
|
2151
2179
|
That one map is passed to both the admin editor and the public site, so a custom
|
|
2152
2180
|
module renders identically in the canvas and live (it shows an "Unknown module"
|
|
2153
|
-
placeholder until you add the entry). See the
|
|
2181
|
+
placeholder until you add the entry). See the commented example in
|
|
2182
|
+
\`kywi.config.ts\`, the worked example in \`lib/modules.tsx\`, and the
|
|
2183
|
+
\`kywi-custom-modules\` skill for when a custom module is the right call at all —
|
|
2184
|
+
the built-in palette (47 modules) covers most of what a site needs.
|
|
2154
2185
|
|
|
2155
2186
|
### Create your first page
|
|
2156
2187
|
|
|
@@ -2171,12 +2202,10 @@ ${themingBlock}
|
|
|
2171
2202
|
## Project layout
|
|
2172
2203
|
|
|
2173
2204
|
\`\`\`
|
|
2174
|
-
kywi.config.ts your config: sites, themes, content types, auth, mode, admin.features
|
|
2205
|
+
kywi.config.ts your config: sites, themes, content types, custom modules, auth, mode, admin.features
|
|
2175
2206
|
AGENTS.md guidance for AI agents working on this site (Kywi's building patterns)
|
|
2176
2207
|
CLAUDE.md points AI agents to AGENTS.md
|
|
2177
|
-
.claude/skills/
|
|
2178
|
-
.claude/skills/kywi-collections/SKILL.md collections skill (loaded automatically)
|
|
2179
|
-
.claude/skills/kywi-personalization/SKILL.md personalization skill (loaded automatically)
|
|
2208
|
+
${SKILLS.map(({ slug }) => `.claude/skills/${slug}/SKILL.md ${slug.replace(/^kywi-/, '')} skill (loaded automatically)`).join('\n')}
|
|
2180
2209
|
middleware.ts auth gate + session refresh + cookie→bearer bridge
|
|
2181
2210
|
next.config.mjs required Next config to consume @kywi-software/core
|
|
2182
2211
|
lib/kywi.ts server runtime (DB, API handler, content scope)
|
|
@@ -2223,6 +2252,38 @@ function agentPatternsDoc() {
|
|
|
2223
2252
|
return _agentPatternsDoc
|
|
2224
2253
|
}
|
|
2225
2254
|
|
|
2255
|
+
/**
|
|
2256
|
+
* Version of the agent-guidance SURFACE (this AGENTS.md shape + the skill set),
|
|
2257
|
+
* independent of the package version. Bump when the guidance changes in a way a
|
|
2258
|
+
* retrofitted app should pick up (new skill, restructured patterns doc), so an
|
|
2259
|
+
* app can compare its stamp against a newer create-kywi-app and know it's stale.
|
|
2260
|
+
*/
|
|
2261
|
+
export const GUIDANCE_VERSION = 1
|
|
2262
|
+
|
|
2263
|
+
/**
|
|
2264
|
+
* This package's own version, read from its package.json (resolved relative to
|
|
2265
|
+
* THIS module, same mechanism as {@link agentPatternsDoc}). Cached.
|
|
2266
|
+
* @returns {string}
|
|
2267
|
+
*/
|
|
2268
|
+
let _pkgVersion
|
|
2269
|
+
function packageVersion() {
|
|
2270
|
+
if (_pkgVersion === undefined) {
|
|
2271
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
|
|
2272
|
+
_pkgVersion = JSON.parse(readFileSync(pkgPath, 'utf8')).version
|
|
2273
|
+
}
|
|
2274
|
+
return _pkgVersion
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
/**
|
|
2278
|
+
* The staleness stamp emitted as AGENTS.md's first line — an HTML comment, so it
|
|
2279
|
+
* is invisible when the file is rendered but greppable by tooling and by an agent
|
|
2280
|
+
* asking "is this project's guidance current?".
|
|
2281
|
+
* @returns {string}
|
|
2282
|
+
*/
|
|
2283
|
+
export function guidanceStamp() {
|
|
2284
|
+
return `<!-- kywi-agent-guidance v${GUIDANCE_VERSION} (create-kywi-app ${packageVersion()}) -->`
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2226
2287
|
/**
|
|
2227
2288
|
* Kywi's Claude Code project skills — each ships as a package asset and is
|
|
2228
2289
|
* embedded verbatim into every generated app's .claude/skills/<slug>/SKILL.md
|
|
@@ -2235,6 +2296,9 @@ export const SKILLS = [
|
|
|
2235
2296
|
{ slug: 'kywi-content-model', asset: 'kywi-content-model-skill.md' },
|
|
2236
2297
|
// When adding any collection: folder + feed + Feed Display, end to end.
|
|
2237
2298
|
{ slug: 'kywi-collections', asset: 'kywi-collections-skill.md' },
|
|
2299
|
+
// When the built-in module palette falls short: the defineModule recipe,
|
|
2300
|
+
// owner-editable props, and the SSR/JS-off hazards.
|
|
2301
|
+
{ slug: 'kywi-custom-modules', asset: 'kywi-custom-modules-skill.md' },
|
|
2238
2302
|
// When the owner wants personalization/A-B: confirm the use case, then build.
|
|
2239
2303
|
{ slug: 'kywi-personalization', asset: 'kywi-personalization-skill.md' },
|
|
2240
2304
|
]
|
|
@@ -2406,10 +2470,13 @@ export function agentsMd(a, landmarks = scaffoldLandmarks(a.mode)) {
|
|
|
2406
2470
|
skill, loaded automatically before building anything.`)
|
|
2407
2471
|
bullets.push(`- \`.claude/skills/kywi-collections/SKILL.md\` — collections skill, loaded
|
|
2408
2472
|
automatically when adding any collection.`)
|
|
2473
|
+
bullets.push(`- \`.claude/skills/kywi-custom-modules/SKILL.md\` — custom-module skill, loaded
|
|
2474
|
+
automatically when the built-in module palette falls short.`)
|
|
2409
2475
|
bullets.push(`- \`.claude/skills/kywi-personalization/SKILL.md\` — personalization skill,
|
|
2410
2476
|
loaded automatically when the owner wants personalization or A/B testing.`)
|
|
2411
2477
|
|
|
2412
|
-
const header =
|
|
2478
|
+
const header = `${guidanceStamp()}
|
|
2479
|
+
# Agent guide — ${a.projectName}
|
|
2413
2480
|
|
|
2414
2481
|
${intro}
|
|
2415
2482
|
${modeSentence}
|
|
@@ -2451,7 +2518,7 @@ holds Kywi's official building patterns and a map of where things live in this a
|
|
|
2451
2518
|
|
|
2452
2519
|
Core principle: **model content in the CMS instead of hardcoding it** — see \`AGENTS.md\`.
|
|
2453
2520
|
|
|
2454
|
-
Before building out a new content area, use the \`kywi-content-model\` skill (design the model, get sign-off) — skills in \`.claude/skills/\` also cover collections and personalization.
|
|
2521
|
+
Before building out a new content area, use the \`kywi-content-model\` skill (design the model, get sign-off) — skills in \`.claude/skills/\` also cover collections, custom modules, and personalization.
|
|
2455
2522
|
`
|
|
2456
2523
|
}
|
|
2457
2524
|
|
package/package.json
CHANGED