create-kywi-app 0.15.0 → 0.15.1

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
@@ -653,6 +679,27 @@ interchangeable. Two of them are current.
653
679
  forced — so a page that already has one is not broken, it is simply not the
654
680
  shape to add more of.
655
681
 
682
+ **Two signals make campaign handoffs and site-owned self-ID cheap
683
+ (kywi-cms#196/#200).** `query.<param>` matches the query string of the request
684
+ being served — `/developers?from=marketer` is the rule `query.from equals
685
+ marketer`, matched on that very request with no reload — and `session.entryPage`
686
+ is the session's first page (path + query), so the rule survives the visitor
687
+ navigating on. For an explicit control instead of the built-in self-ID widget,
688
+ `Kywi.setSelfId({ role: 'marketer' })` records the answer, resolves and PINS the
689
+ audience server-side and reloads; the pin is the part that matters, because the
690
+ losing arms are pruned on the server (see #167 below) and only a fresh server
691
+ render can show them. A plain form post to `/api/v1/kywi/self-id` with a
692
+ `returnTo` field does the same with JavaScript off. Two rules that decide
693
+ whether it works at all: **the endpoint stores only self-ID fields the site
694
+ declared** (Audiences → Self-ID Fields), so an undeclared key is dropped; and
695
+ **an audience with no rules never matches from answers**, because resolution
696
+ runs the same rules everything else does. For a chooser whose options ARE
697
+ audiences, flag them **public**, read the options from
698
+ `GET /api/v1/kywi/audiences` (which publishes only `{ id, name, label }`), and
699
+ pass the id straight back — `Kywi.setSelfId({}, { audienceId })`, or a hidden
700
+ `audienceId` input on the form — which pins the visitor's pick directly, rules
701
+ or no rules.
702
+
656
703
  **Neither container ever nests.** Not one inside the other, not one inside
657
704
  itself. A module already sitting in a personalized section's arm cannot be
658
705
  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 ? (
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.1",
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",