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.
@@ -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 head = (
1713
+ const headFor = (servedLayout?: unknown) => (
1685
1714
  <>
1686
1715
  <meta name="kywi:content-id" content={contentId} />
1687
- <KywiJsonLd node={node} config={runtime.config} baseUrl={baseUrl} siteId={runtime.siteId} />
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
- {head}
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
- {head}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kywi-app",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Scaffold a new Kywi CMS project — npx create-kywi-app my-site",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kywi-Software/kywi-cms#readme",