create-kywi-app 0.15.0 → 0.15.2
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
CHANGED
|
@@ -331,6 +331,32 @@ reads as a styled document rather than a site. Sections are bands (each owns its
|
|
|
331
331
|
background and padding), `columns` splits them, and the object modules below do
|
|
332
332
|
the rest.
|
|
333
333
|
|
|
334
|
+
### Module props: omitted takes the default, `''` stays blank
|
|
335
|
+
|
|
336
|
+
Every module declares its props, and most declare a `defaultValue` — the copy or
|
|
337
|
+
setting the module ships with so it renders as *something* the moment it is
|
|
338
|
+
placed. That default is applied on the **write path**, so it applies however the
|
|
339
|
+
module got there: dragged in by the editor, added with MCP `add_module`, or
|
|
340
|
+
written straight into a layout document.
|
|
341
|
+
|
|
342
|
+
The rule has two halves, and the second is the one that matters:
|
|
343
|
+
|
|
344
|
+
- **A prop you OMIT takes the module's declared default.** You do not need to
|
|
345
|
+
restate a module's own defaults to make it look right, and a custom module
|
|
346
|
+
does not need a second copy of them baked into its component.
|
|
347
|
+
- **A prop you write as `''` stays `''`.** An empty string is an instruction —
|
|
348
|
+
"blank on purpose" — not an absent value. Same for `null`, `0` and `false`:
|
|
349
|
+
presence of the key is the whole signal.
|
|
350
|
+
|
|
351
|
+
So `add_module` with no `props` gives you a module that renders; `add_module`
|
|
352
|
+
with `props: { eyebrow: '' }` gives you one with a deliberately empty eyebrow
|
|
353
|
+
and every other prop defaulted. To find out what a module's defaults actually
|
|
354
|
+
are, call `list_module_types` — each prop reports its `default`.
|
|
355
|
+
|
|
356
|
+
Defaults are seeded onto modules a write **adds**, never re-applied to modules
|
|
357
|
+
already on the page. Saving a page for an unrelated reason will not put back a
|
|
358
|
+
prop that someone deliberately left off an existing module.
|
|
359
|
+
|
|
334
360
|
### The built-in palette
|
|
335
361
|
|
|
336
362
|
`name` is the exact `type` value in a layout node and in MCP `add_module`. A
|
|
@@ -463,7 +489,10 @@ already answered.
|
|
|
463
489
|
When consent is required and no page places the module, the API logs a one-time
|
|
464
490
|
warning on its first request: nobody can grant consent, so every personalization
|
|
465
491
|
cookie stays suppressed and audiences, A/B bucketing and UTM attribution serve
|
|
466
|
-
default content to everyone.
|
|
492
|
+
default content to everyone. A host that mounts the banner itself in app chrome
|
|
493
|
+
instead of a page or blueprint layout (kywi-cms#193) is invisible to that scan,
|
|
494
|
+
so set `theme.personalization.consentBanner: 'host'` to declare the placement
|
|
495
|
+
and skip the check.
|
|
467
496
|
|
|
468
497
|
### Which do I reach for? (in order)
|
|
469
498
|
|
|
@@ -653,6 +682,27 @@ interchangeable. Two of them are current.
|
|
|
653
682
|
forced — so a page that already has one is not broken, it is simply not the
|
|
654
683
|
shape to add more of.
|
|
655
684
|
|
|
685
|
+
**Two signals make campaign handoffs and site-owned self-ID cheap
|
|
686
|
+
(kywi-cms#196/#200).** `query.<param>` matches the query string of the request
|
|
687
|
+
being served — `/developers?from=marketer` is the rule `query.from equals
|
|
688
|
+
marketer`, matched on that very request with no reload — and `session.entryPage`
|
|
689
|
+
is the session's first page (path + query), so the rule survives the visitor
|
|
690
|
+
navigating on. For an explicit control instead of the built-in self-ID widget,
|
|
691
|
+
`Kywi.setSelfId({ role: 'marketer' })` records the answer, resolves and PINS the
|
|
692
|
+
audience server-side and reloads; the pin is the part that matters, because the
|
|
693
|
+
losing arms are pruned on the server (see #167 below) and only a fresh server
|
|
694
|
+
render can show them. A plain form post to `/api/v1/kywi/self-id` with a
|
|
695
|
+
`returnTo` field does the same with JavaScript off. Two rules that decide
|
|
696
|
+
whether it works at all: **the endpoint stores only self-ID fields the site
|
|
697
|
+
declared** (Audiences → Self-ID Fields), so an undeclared key is dropped; and
|
|
698
|
+
**an audience with no rules never matches from answers**, because resolution
|
|
699
|
+
runs the same rules everything else does. For a chooser whose options ARE
|
|
700
|
+
audiences, flag them **public**, read the options from
|
|
701
|
+
`GET /api/v1/kywi/audiences` (which publishes only `{ id, name, label }`), and
|
|
702
|
+
pass the id straight back — `Kywi.setSelfId({}, { audienceId })`, or a hidden
|
|
703
|
+
`audienceId` input on the form — which pins the visitor's pick directly, rules
|
|
704
|
+
or no rules.
|
|
705
|
+
|
|
656
706
|
**Neither container ever nests.** Not one inside the other, not one inside
|
|
657
707
|
itself. A module already sitting in a personalized section's arm cannot be
|
|
658
708
|
promoted — the editor withholds the action, `personalize_module` returns a
|
|
@@ -34,6 +34,36 @@ Admin → Audiences: define the rules, or configure the self-ID widget and its
|
|
|
34
34
|
fields. Use the audience test tool to confirm the rules match the intended
|
|
35
35
|
visitors before wiring anything to it.
|
|
36
36
|
|
|
37
|
+
### 3b. Two shortcuts worth knowing before you build rules
|
|
38
|
+
|
|
39
|
+
- **A campaign link is a one-line rule.** `query.<param>` matches the query
|
|
40
|
+
string of the request being served, so `/developers?from=marketer` is
|
|
41
|
+
`query.from equals marketer` — and it matches on that very request, no
|
|
42
|
+
reload. `session.entryPage` is the first page (path + query) of the
|
|
43
|
+
visitor's session, so a rule on it keeps matching after they navigate on.
|
|
44
|
+
Neither needs a `utm_*` param invented for the purpose.
|
|
45
|
+
- **A site-owned "What brings you here?" control** does not need the built-in
|
|
46
|
+
hello-bar widget. Call `Kywi.setSelfId({ role: 'marketer' })`: it records the
|
|
47
|
+
answer, resolves the audience server-side, pins it and reloads, so the server
|
|
48
|
+
re-renders with that audience's arms. `Kywi.clearSelfId()` undoes it. With
|
|
49
|
+
JavaScript off, a plain `<form method="post" action="/api/v1/kywi/self-id">`
|
|
50
|
+
carrying a `returnTo` field does the same thing and 303s back to the page.
|
|
51
|
+
|
|
52
|
+
Two things about it that are easy to get wrong:
|
|
53
|
+
|
|
54
|
+
- **Declare the self-ID field first** (Admin → Audiences → Self-ID Fields).
|
|
55
|
+
The endpoint stores only the fields the site declared, so submitting
|
|
56
|
+
`{ role }` to a site with no `role` field stores nothing and matches
|
|
57
|
+
nothing.
|
|
58
|
+
- **An audience with no rules never matches from answers.** Resolution runs
|
|
59
|
+
the same rules the rest of the engine does; an audience with zero condition
|
|
60
|
+
groups matches nobody. If you want a chooser whose options ARE audiences,
|
|
61
|
+
flag them **public** in the audience editor, read the options from
|
|
62
|
+
`GET /api/v1/kywi/audiences` (`{ id, name, label }`, never the rules), and
|
|
63
|
+
pass the id back: `Kywi.setSelfId({}, { audienceId })`, or an
|
|
64
|
+
`<input type="hidden" name="audienceId">` on the no-JS form. That pins the
|
|
65
|
+
audience the visitor picked directly, rules or no rules.
|
|
66
|
+
|
|
37
67
|
### 4a. Page-variant path
|
|
38
68
|
|
|
39
69
|
In the layout editor on the target page: **+ Page Variant** → select the
|
package/lib/templates.mjs
CHANGED
|
@@ -1055,6 +1055,9 @@ import {
|
|
|
1055
1055
|
// back. Its own dependency-free entry (never the DB-backed audiences barrel), so
|
|
1056
1056
|
// it is importable from this edge middleware.
|
|
1057
1057
|
import { UTM_COOKIE, utmCookieValue } from '@kywi-software/core/audiences/utm-persistence'
|
|
1058
|
+
// Same kind of edge-safe leaf: the kywi_entry writer, whose value core reads
|
|
1059
|
+
// back as \`session.entryPage\` (kywi-cms#196).
|
|
1060
|
+
import { ENTRY_COOKIE, entryPageCookieValue } from '@kywi-software/core/audiences/entry-page'
|
|
1058
1061
|
// Same kind of edge-safe leaf entry: the cookie-consent category map + gate. The
|
|
1059
1062
|
// visitor's decision (written by the \`cookieConsent\` layout module) decides
|
|
1060
1063
|
// whether the personalization cookies below may be persisted at all.
|
|
@@ -1155,6 +1158,25 @@ function handlePublicRequest(req: NextRequest): NextResponse {
|
|
|
1155
1158
|
if (utmValue) {
|
|
1156
1159
|
res.cookies.set(UTM_COOKIE, utmValue, { path: '/', maxAge: 60 * 60 * 24 * 30, sameSite: 'lax' })
|
|
1157
1160
|
}
|
|
1161
|
+
// Record the session's ENTRY page (path + query) so rules on
|
|
1162
|
+
// \`session.entryPage\` keep matching once the visitor navigates on
|
|
1163
|
+
// (kywi-cms#196). Written once, consent-gated like the two above, and with no
|
|
1164
|
+
// maxAge — a new visit is a new entry page. The landing request needs no
|
|
1165
|
+
// cookie: core falls back to the request's own path, so \`?from=marketer\`
|
|
1166
|
+
// personalizes the page it links to on that very request.
|
|
1167
|
+
// Sec-Fetch-Dest / Accept are passed so ONLY a document request can name the
|
|
1168
|
+
// entry page: a subresource that happens to be the session's first request
|
|
1169
|
+
// (\`/kywi.js\`, a font, a route handler) would otherwise record itself as the
|
|
1170
|
+
// page the visitor arrived on, and every entry-page rule would match an asset.
|
|
1171
|
+
const entryValue = entryPageCookieValue(req.nextUrl.href, {
|
|
1172
|
+
cookieHeader,
|
|
1173
|
+
secFetchDest: req.headers.get('sec-fetch-dest'),
|
|
1174
|
+
accept: req.headers.get('accept'),
|
|
1175
|
+
...gate,
|
|
1176
|
+
})
|
|
1177
|
+
if (entryValue) {
|
|
1178
|
+
res.cookies.set(ENTRY_COOKIE, entryValue, { path: '/', sameSite: 'lax' })
|
|
1179
|
+
}
|
|
1158
1180
|
return res
|
|
1159
1181
|
}
|
|
1160
1182
|
|
|
@@ -1680,11 +1702,24 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
|
|
|
1680
1702
|
// Comments module's content-id anchor (#29); the audience/visitor ids for
|
|
1681
1703
|
// client tooling (#50); and — when the theme opts in — the optional client
|
|
1682
1704
|
// runtime that mounts the self-ID widget and re-evaluates audiences.
|
|
1705
|
+
//
|
|
1706
|
+
// \`head\` is a FUNCTION because JSON-LD must describe the layout THIS visitor
|
|
1707
|
+
// is served (#195): the served document only exists after the arms are pruned
|
|
1708
|
+
// below, and building the description from the STORED layout would ship the
|
|
1709
|
+
// DEFAULT arm's copy to a matched visitor — the #167 leak, through structured
|
|
1710
|
+
// data. The no-layout branch passes nothing, and JSON-LD then describes the
|
|
1711
|
+
// node's body/metaDescription/summary alone.
|
|
1683
1712
|
const baseUrl = await requestBaseUrl()
|
|
1684
|
-
const
|
|
1713
|
+
const headFor = (servedLayout?: unknown) => (
|
|
1685
1714
|
<>
|
|
1686
1715
|
<meta name="kywi:content-id" content={contentId} />
|
|
1687
|
-
<KywiJsonLd
|
|
1716
|
+
<KywiJsonLd
|
|
1717
|
+
node={node}
|
|
1718
|
+
config={runtime.config}
|
|
1719
|
+
baseUrl={baseUrl}
|
|
1720
|
+
siteId={runtime.siteId}
|
|
1721
|
+
{...(servedLayout ? { layout: servedLayout } : {})}
|
|
1722
|
+
/>
|
|
1688
1723
|
<AudienceMetaTags audienceId={perso.audienceId} visitorId={perso.visitorId} />
|
|
1689
1724
|
{clientRuntimeEnabled() && perso.audiences.length > 0 ? (
|
|
1690
1725
|
<PersonalizationRuntime
|
|
@@ -1737,7 +1772,7 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
|
|
|
1737
1772
|
const served = pruneLayoutToServedArms(hydrated, perso.personalization)
|
|
1738
1773
|
content = (
|
|
1739
1774
|
<article className="page page--layout" data-kywi-content-id={contentId}>
|
|
1740
|
-
{
|
|
1775
|
+
{headFor(served)}
|
|
1741
1776
|
<KywiLayout
|
|
1742
1777
|
layout={served}
|
|
1743
1778
|
personalization={perso.personalization}
|
|
@@ -1752,7 +1787,7 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
|
|
|
1752
1787
|
} else {
|
|
1753
1788
|
content = (
|
|
1754
1789
|
<article className="page" data-kywi-content-id={contentId}>
|
|
1755
|
-
{
|
|
1790
|
+
{headFor()}
|
|
1756
1791
|
{featured ? <img className="page__featured" src={featured} alt="" /> : null}
|
|
1757
1792
|
<h1 className="page__title">{title}</h1>
|
|
1758
1793
|
{perms ? (
|
|
@@ -2627,7 +2662,11 @@ Audiences, experiments and the self-ID widget you configure in the admin resolve
|
|
|
2627
2662
|
tooling instead, set \`personalization: { requireConsent: false }\` on your
|
|
2628
2663
|
theme in \`kywi.config.ts\` **and** \`KYWI_REQUIRE_CONSENT=false\` in the
|
|
2629
2664
|
environment (the edge middleware cannot read the config) — that hands
|
|
2630
|
-
compliance to your tooling and restores the un-gated cookie behaviour.
|
|
2665
|
+
compliance to your tooling and restores the un-gated cookie behaviour. If
|
|
2666
|
+
you mount \`cookieConsent\` yourself in \`app/(site)/layout.tsx\` instead of
|
|
2667
|
+
placing it through a page or blueprint layout, set
|
|
2668
|
+
\`personalization: { consentBanner: 'host' }\` too, or the API's one-time
|
|
2669
|
+
startup check will warn that nothing places it.
|
|
2631
2670
|
- **Preview links** — an admin preview link sets \`kywi_preview_init\` and the page
|
|
2632
2671
|
honours it, so you can preview an audience's experience.
|
|
2633
2672
|
|
package/package.json
CHANGED