create-kywi-app 0.6.7 → 0.7.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.
@@ -260,8 +260,8 @@ everywhere.
260
260
  | `name` | Purpose |
261
261
  |---|---|
262
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 |
263
+ | `navMenu` | A named menu (`menuSlug`) in a `variant`: horizontal / mega / vertical / footer |
264
+ | `navigation` | Site navigation in the same four variants, to a depth |
265
265
  | `siteMap` | Nested list of the site tree |
266
266
  | `searchBox` | Compact search input posting to a results URL |
267
267
  | `search` | Full search form (method, label, results URL) |
@@ -328,6 +328,62 @@ page", "Case study" — so new pages start from a consistent skeleton. Hand-writ
328
328
  JSX stays fine for genuinely fixed chrome (a bespoke 404, legal boilerplate) —
329
329
  but if marketing will ever want to swap a headline, it's a layout page.
330
330
 
331
+ ### Navigation: menus vs. the site tree
332
+
333
+ Two sources feed every nav module, and both resolve to the exact same
334
+ render-ready shape — `navMenu`/`navigation` never know which one produced it:
335
+
336
+ - **Menus** (Menus admin) — an owner-managed, named list of items. Each item
337
+ either links a content node (its href resolves live off that page — publish,
338
+ rename, or move the page and the menu link follows) or carries an external
339
+ URL, plus a label, an optional description (used by `mega` panels) and an
340
+ open-in-new-window flag. Reference one from a `navMenu` module's `menuSlug`
341
+ prop.
342
+ - **The site tree** — every page with `isNav` checked (Site Tree admin), in
343
+ `sortOrder`. A page's nav label is its `menuTitle` when the editor set one,
344
+ else its `title` — so a long page title ("Plans and Pricing for Teams") can
345
+ show a short nav label ("Pricing") without a second, independently-maintained
346
+ copy of the link. `navigation` and `siteMap` modules render this tree, to
347
+ `depth` levels.
348
+
349
+ **NEVER hardcode the site nav in chrome.** A hand-typed `<nav>` with pasted
350
+ hrefs is exactly the failure mode this exists to close: a prior marketing
351
+ build's hardcoded header had 7 of its 13 links quietly 404ing, because nothing
352
+ checked them against the pages that actually existed. A menu item's href is
353
+ derived from its linked page, so it cannot drift the way a pasted URL can; a
354
+ site-tree nav module can't drift either, because it IS the tree. Build the
355
+ header/footer from a `navMenu`/`navigation` module (or, in a scaffolded app's
356
+ own chrome, from `scope.menus`/`scope.nav` directly — see below) and never from
357
+ a list of `<a>` tags an agent typed out.
358
+
359
+ `navMenu`'s `menuSlug` resolves server-side against a real, owner-managed menu.
360
+ Both `navMenu` and `navigation` still accept hand-authored `items` JSON — as
361
+ the fallback when a `menuSlug` names a menu that doesn't (yet) exist, or as a
362
+ deliberate one-off override — but a named menu or the site tree is how a
363
+ *maintained* nav gets built, not hand-typed JSON.
364
+
365
+ **Variants** (`variant` prop, shared by `navMenu` and `navigation`):
366
+ `horizontal` (dropdown panels — a top bar), `mega` (full-width multi-column
367
+ panels with descriptions — reach for this when a top-level item has several
368
+ children worth previewing), `vertical` (nested fly-outs — a sidebar), `footer`
369
+ (grouped columns of links, no interactivity). Every variant is JS-off safe:
370
+ links always render server-side, and below the mobile breakpoint the bar
371
+ collapses behind a native `<details>` hamburger. The interactive variants carry
372
+ `data-kywi-nav` + `data-kywi-nav-variant`; `@kywi-software/js` auto-boots
373
+ hover-intent, keyboard and outside-click handling on any element with
374
+ `data-kywi-nav` — nothing to wire up beyond loading `/kywi.js`.
375
+
376
+ **A scaffolded app's own header/footer follow the same rule.**
377
+ `app/(site)/layout.tsx` renders the `'main'` menu when the site has one,
378
+ falling back to the `isNav` site tree — so a fresh install has a working header
379
+ with zero manual wiring, and an owner who later builds a `'main'` menu in the
380
+ admin overrides it with no code change. The footer renders a `'footer'` menu
381
+ only when one exists (no tree fallback there — an unbuilt footer menu should be
382
+ empty, not suddenly cluttered with the whole site tree). Both render through
383
+ the same shared nav renderer as the `navigation` module, never hand-rolled
384
+ markup, so the header and footer get the identical CSS and
385
+ `@kywi-software/js` enhancement as any nav module placed in the page body.
386
+
331
387
  ## 6. Forms: always the Forms builder
332
388
 
333
389
  Never hand-code a `<form>`. Build forms in the Forms admin (fields, multi-step,
package/lib/templates.mjs CHANGED
@@ -1087,10 +1087,58 @@ export default function RootLayout({ children }: { children: React.ReactNode })
1087
1087
  `
1088
1088
  }
1089
1089
 
1090
+ /**
1091
+ * Client-side wrapper around core's SHARED nav renderer — the same component the
1092
+ * `navigation` module uses — so the site header and footer get identical markup,
1093
+ * CSS (`.kywi-nav-*`) and @kywi-software/js enhancement to any nav placed in a
1094
+ * page body. `items` is whatever app/(site)/layout.tsx resolved server-side (the
1095
+ * owner-managed menu, or the site tree as fallback); nothing is decided here.
1096
+ *
1097
+ * WHY THIS FILE EXISTS — it is `'use client'` on purpose, and the layout that
1098
+ * renders it is a Server Component. `BUILT_IN_MODULE_COMPONENTS` is exported
1099
+ * from one of core's `'use client'` modules, so a Server Component that imports
1100
+ * the map gets a client *reference*, not a real object:
1101
+ * `BUILT_IN_MODULE_COMPONENTS.navigation` reads back `undefined` there and React
1102
+ * throws "Element type is invalid… got: undefined" on every page (kywi-cms#112
1103
+ * follow-up). Crossing the boundary here, in a client module, is what makes the
1104
+ * lookup resolve to the real component — mirrors apps/reference/components/nav.tsx.
1105
+ */
1106
+ function siteNavComponent() {
1107
+ return `'use client'
1108
+
1109
+ import React from 'react'
1110
+ import { BUILT_IN_MODULE_COMPONENTS } from '@kywi-software/core/layout'
1111
+ import type { NavMenuItem } from '@kywi-software/core/nav'
1112
+
1113
+ // BUILT_IN_MODULE_COMPONENTS is a Record<string, …>, so a literal-key lookup is
1114
+ // typed as possibly-undefined under noUncheckedIndexedAccess; 'navigation' is
1115
+ // always present — it's one of core's own built-in modules, not user data.
1116
+ const NavRenderer = BUILT_IN_MODULE_COMPONENTS.navigation!
1117
+
1118
+ export interface SiteNavProps {
1119
+ items: NavMenuItem[]
1120
+ /** \`horizontal\`/\`mega\`/\`vertical\` for the header bar, \`footer\` for the footer columns. */
1121
+ variant: 'horizontal' | 'mega' | 'vertical' | 'footer'
1122
+ ariaLabel: string
1123
+ depth?: number
1124
+ }
1125
+
1126
+ /** Renders one resolved menu (header or footer) through core's shared nav renderer. */
1127
+ export function SiteNav({ items, variant, ariaLabel, depth }: SiteNavProps) {
1128
+ if (items.length === 0) return null
1129
+ return (
1130
+ <NavRenderer props={{ items, variant, ariaLabel, ...(depth ? { depth } : {}) }} />
1131
+ )
1132
+ }
1133
+ `
1134
+ }
1135
+
1090
1136
  /** @param {Answers} a — coupled public site shell. */
1091
1137
  function siteLayout(a) {
1092
1138
  return `import React from 'react'
1139
+ import { headers } from 'next/headers'
1093
1140
  import { themeTokenStyleBlock } from '@kywi-software/core/layout'
1141
+ import { navTreeToMenuItems, normalizePath } from '@kywi-software/core/nav'
1094
1142
  // Neutral, token-driven defaults for everything Kywi renders (prose, the layout
1095
1143
  // grid + modules, forms, the edit overlay). Restyle via theme tokens, not by
1096
1144
  // editing this — see README → "Theming".
@@ -1098,12 +1146,25 @@ import '@kywi-software/core/site/styles.css'
1098
1146
  // This app's OWN chrome (header / footer / page wrapper). Yours to edit freely.
1099
1147
  import './site.css'
1100
1148
  import config from '../../kywi.config'
1149
+ import { getKywi } from '../../lib/kywi'
1150
+ import { SiteNav } from '../../components/site-nav'
1101
1151
 
1102
1152
  /**
1103
1153
  * Public site shell. Header + footer carry this project's brand; edit them (and
1104
1154
  * site.css) freely — this is your app's own layer over the DB-backed content
1105
1155
  * that ${'app/(site)/[[...slug]]'} renders.
1106
1156
  *
1157
+ * The header/footer nav is DATA-DRIVEN, never hand-typed: it renders the
1158
+ * owner-managed \`'main'\` menu (Admin → Menus) when one exists, falling back to
1159
+ * the published Site Tree (\`isNav\` pages) so a fresh site has working nav out
1160
+ * of the box — and the footer renders a \`'footer'\` menu when one exists. NEVER
1161
+ * hardcode this list: a menu item's href resolves from its linked page and
1162
+ * can never drift the way a pasted URL can (see AGENTS.md → Navigation).
1163
+ * Rendered through core's shared nav renderer via the generated
1164
+ * \`components/site-nav.tsx\` (a \`'use client'\` wrapper — see that file for why)
1165
+ * rather than bespoke markup, so it gets the same CSS and \`@kywi-software/js\`
1166
+ * enhancement as a \`navigation\` module placed in the page body.
1167
+ *
1107
1168
  * The active site's theme tokens (kywi.config.ts → themes[].tokens) are flattened
1108
1169
  * into a \`:root { --kywi-* }\` block by \`themeTokenStyleBlock\` and injected below,
1109
1170
  * so kywi.config.ts is the single source of truth for the palette and spacing and
@@ -1113,7 +1174,24 @@ const siteTheme =
1113
1174
  config.themes.find((t) => t.name === config.sites[0]?.theme) ?? config.themes[0]
1114
1175
  const themeVars = themeTokenStyleBlock(siteTheme?.tokens)
1115
1176
 
1116
- export default function SiteLayout({ children }: { children: React.ReactNode }) {
1177
+ // Content edits (a renamed page, a reordered menu) must show up without a
1178
+ // restart — same reasoning as the page route.
1179
+ export const dynamic = 'force-dynamic'
1180
+
1181
+ export default async function SiteLayout({ children }: { children: React.ReactNode }) {
1182
+ const { scope, siteId } = await getKywi()
1183
+
1184
+ // The middleware forwards the full request URL as \`x-kywi-url\` (see
1185
+ // middleware.ts) so this shared layout can resolve which page is current —
1186
+ // without it, isActive/isAncestor on nav items would have nothing to match.
1187
+ const h = await headers()
1188
+ const url = h.get('x-kywi-url')
1189
+ const currentPath = normalizePath(url ? new URL(url).pathname : '/')
1190
+
1191
+ const mainMenu = await scope.menus.getResolved(siteId, 'main', currentPath)
1192
+ const headerItems = mainMenu ?? navTreeToMenuItems(await scope.nav.getTree(siteId, currentPath))
1193
+ const footerItems = await scope.menus.getResolved(siteId, 'footer', currentPath)
1194
+
1117
1195
  return (
1118
1196
  <div className="site-shell">
1119
1197
  {themeVars ? <style dangerouslySetInnerHTML={{ __html: themeVars }} /> : null}
@@ -1121,9 +1199,12 @@ export default function SiteLayout({ children }: { children: React.ReactNode })
1121
1199
  <header className="site-header">
1122
1200
  <div className="site-header__inner">
1123
1201
  <a className="site-brand" href="/">${escapeJsxText(a.projectName)}</a>
1124
- <nav className="site-nav" aria-label="Primary">
1202
+ <div className="site-nav">
1203
+ {headerItems.length > 0 && (
1204
+ <SiteNav items={headerItems} variant="horizontal" ariaLabel="Primary" depth={2} />
1205
+ )}
1125
1206
  <a className="site-nav__admin" href="/admin">Admin →</a>
1126
- </nav>
1207
+ </div>
1127
1208
  </div>
1128
1209
  </header>
1129
1210
 
@@ -1131,7 +1212,10 @@ export default function SiteLayout({ children }: { children: React.ReactNode })
1131
1212
 
1132
1213
  <footer className="site-footer">
1133
1214
  <div className="site-footer__inner">
1134
- Powered by <a href="https://kywi.dev">Kywi CMS</a>
1215
+ {footerItems && footerItems.length > 0 && (
1216
+ <SiteNav items={footerItems} variant="footer" ariaLabel="Footer" />
1217
+ )}
1218
+ <p className="site-footer__credit">Powered by <a href="https://kywi.dev">Kywi CMS</a></p>
1135
1219
  </div>
1136
1220
  </footer>
1137
1221
  </div>
@@ -1183,6 +1267,14 @@ function siteStyles(a) {
1183
1267
  color: var(--kywi-color-heading, #0f172a);
1184
1268
  text-decoration: none;
1185
1269
  }
1270
+ /* Wraps the data-driven nav (rendered by core's shared nav renderer — see
1271
+ app/(site)/layout.tsx) alongside the Admin link. The nav itself carries its
1272
+ own .kywi-nav-* classes, styled by @kywi-software/core/site/styles.css. */
1273
+ .site-nav {
1274
+ display: flex;
1275
+ align-items: center;
1276
+ gap: var(--kywi-spacing-md, 1rem);
1277
+ }
1186
1278
  .site-nav__admin {
1187
1279
  color: var(--kywi-color-primary, #2563eb);
1188
1280
  text-decoration: none;
@@ -1229,6 +1321,8 @@ function siteStyles(a) {
1229
1321
  font-size: 0.875rem;
1230
1322
  }
1231
1323
  .site-footer__inner a { color: var(--kywi-color-muted, #64748b); }
1324
+ /* Sits below the optional footer nav (a 'footer' menu, when one exists). */
1325
+ .site-footer__credit { margin: var(--kywi-spacing-md, 1rem) 0 0; }
1232
1326
  `
1233
1327
  }
1234
1328
 
@@ -1239,7 +1333,7 @@ import type { Metadata } from 'next'
1239
1333
  import { notFound } from 'next/navigation'
1240
1334
  import { cookies, headers } from 'next/headers'
1241
1335
  import { KywiBody, KywiEditableAttribute, KywiEditableRegion } from '@kywi-software/core/scope-client'
1242
- import { KywiLayout, KywiRegion, AudienceMetaTags, hydrateLayoutFeeds, type LayoutDocument } from '@kywi-software/core/layout'
1336
+ import { KywiLayout, KywiRegion, AudienceMetaTags, hydrateLayoutFeeds, hydrateLayoutNav, type LayoutDocument } from '@kywi-software/core/layout'
1243
1337
  import { KywiJsonLd } from '@kywi-software/core/scope'
1244
1338
  import { ACCESS_COOKIE, canAccessContent, readSessionClaims } from '@kywi-software/core/host'
1245
1339
  import config from '../../../lib/config'
@@ -1400,7 +1494,15 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
1400
1494
  // hydrate feeds and resolve components on the layout THIS visitor sees.
1401
1495
  // variantContainer arms resolve at render time from \`personalization\`.
1402
1496
  const personalized = personalizeLayout(layout, perso.audienceId)
1403
- const hydrated = await hydrateLayoutFeeds(personalized, buildFeedResolver(runtime))
1497
+ const feedsHydrated = await hydrateLayoutFeeds(personalized, buildFeedResolver(runtime))
1498
+ // Resolve every navMenu/navigation/siteMap module placed in THIS layout
1499
+ // (menuSlug → a real menu; the tree otherwise) — the same seam as feeds,
1500
+ // so a nav module dropped into any region/section just works (#112).
1501
+ const currentPath = String(node['path'] ?? '/')
1502
+ const hydrated = await hydrateLayoutNav(
1503
+ feedsHydrated,
1504
+ runtime.scope.menus.createHydrationResolver(runtime.siteId, currentPath),
1505
+ )
1404
1506
  const componentResolver = await buildComponentResolver(hydrated, runtime)
1405
1507
  const sectionComponentResolver = await buildSectionComponentResolver(hydrated, runtime)
1406
1508
  content = (
@@ -2236,7 +2338,7 @@ lib/config.ts single import path for kywi.config.ts
2236
2338
  app/api/v1/[...kywi]/route.ts the versioned API (delegates to core)
2237
2339
  app/admin/[[...admin]]/page.tsx mounts the FULL core admin (all surfaces) at /admin
2238
2340
  lib/modules.tsx custom (defineModule) module renderers (admin + public)
2239
- app/{llms,robots,sitemap,…} root AX routes (llms.txt, robots.txt, sitemap.xml, …)${a.mode === 'coupled' ? '\napp/(site)/layout.tsx public shell: theme tokens + your header/footer\napp/(site)/site.css your site chrome styles (edit freely)\napp/(site)/[[...slug]]/page.tsx renders published pages (layout + SEO + JSON-LD + i18n)\nlib/site.ts public-render helpers: path/locale resolution, feeds, personalization\ncomponents/personalization-runtime.tsx optional client runtime (self-ID widget, live re-eval)\napp/(site)/kywi-front-edit.tsx front-of-site editor: browse toolbar + in-place Layout editor (?kywi-edit=1, lazy-loaded)' : '\napp/page.tsx returns 404 (no public rendering in this mode)'}
2341
+ app/{llms,robots,sitemap,…} root AX routes (llms.txt, robots.txt, sitemap.xml, …)${a.mode === 'coupled' ? '\napp/(site)/layout.tsx public shell: theme tokens + your header/footer\napp/(site)/site.css your site chrome styles (edit freely)\napp/(site)/[[...slug]]/page.tsx renders published pages (layout + SEO + JSON-LD + i18n)\nlib/site.ts public-render helpers: path/locale resolution, feeds, personalization\ncomponents/site-nav.tsx client wrapper around the shared nav renderer (header + footer)\ncomponents/personalization-runtime.tsx optional client runtime (self-ID widget, live re-eval)\napp/(site)/kywi-front-edit.tsx front-of-site editor: browse toolbar + in-place Layout editor (?kywi-edit=1, lazy-loaded)' : '\napp/page.tsx returns 404 (no public rendering in this mode)'}
2240
2342
  \`\`\`
2241
2343
  `
2242
2344
  }
@@ -2620,6 +2722,11 @@ export function buildFileSet(answers) {
2620
2722
  // page at its slug. More-specific /admin and /api routes take precedence.
2621
2723
  files['app/(site)/layout.tsx'] = siteLayout(answers)
2622
2724
  files['app/(site)/site.css'] = siteStyles(answers)
2725
+ // 'use client' wrapper around core's shared nav renderer — required because
2726
+ // BUILT_IN_MODULE_COMPONENTS is exported from a 'use client' core module, so
2727
+ // the (Server Component) site layout above must render it through here
2728
+ // rather than looking it up directly (see siteNavComponent's doc comment).
2729
+ files['components/site-nav.tsx'] = siteNavComponent()
2623
2730
  files['app/(site)/[[...slug]]/page.tsx'] = siteSlugPage()
2624
2731
  // Public-render helpers: path/locale resolution, feed + component resolvers,
2625
2732
  // personalization + experiments.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kywi-app",
3
- "version": "0.6.7",
3
+ "version": "0.7.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",