create-kywi-app 0.6.7 → 0.7.0

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
@@ -1090,7 +1090,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
1090
1090
  /** @param {Answers} a — coupled public site shell. */
1091
1091
  function siteLayout(a) {
1092
1092
  return `import React from 'react'
1093
- import { themeTokenStyleBlock } from '@kywi-software/core/layout'
1093
+ import { headers } from 'next/headers'
1094
+ import { themeTokenStyleBlock, BUILT_IN_MODULE_COMPONENTS } from '@kywi-software/core/layout'
1095
+ import { navTreeToMenuItems, normalizePath } from '@kywi-software/core/nav'
1094
1096
  // Neutral, token-driven defaults for everything Kywi renders (prose, the layout
1095
1097
  // grid + modules, forms, the edit overlay). Restyle via theme tokens, not by
1096
1098
  // editing this — see README → "Theming".
@@ -1098,12 +1100,23 @@ import '@kywi-software/core/site/styles.css'
1098
1100
  // This app's OWN chrome (header / footer / page wrapper). Yours to edit freely.
1099
1101
  import './site.css'
1100
1102
  import config from '../../kywi.config'
1103
+ import { getKywi } from '../../lib/kywi'
1101
1104
 
1102
1105
  /**
1103
1106
  * Public site shell. Header + footer carry this project's brand; edit them (and
1104
1107
  * site.css) freely — this is your app's own layer over the DB-backed content
1105
1108
  * that ${'app/(site)/[[...slug]]'} renders.
1106
1109
  *
1110
+ * The header/footer nav is DATA-DRIVEN, never hand-typed: it renders the
1111
+ * owner-managed \`'main'\` menu (Admin → Menus) when one exists, falling back to
1112
+ * the published Site Tree (\`isNav\` pages) so a fresh site has working nav out
1113
+ * of the box — and the footer renders a \`'footer'\` menu when one exists. NEVER
1114
+ * hardcode this list: a menu item's href resolves from its linked page and
1115
+ * can never drift the way a pasted URL can (see AGENTS.md → Navigation).
1116
+ * Rendered through core's shared nav renderer (\`BUILT_IN_MODULE_COMPONENTS\`)
1117
+ * rather than bespoke markup, so it gets the same CSS and \`@kywi-software/js\`
1118
+ * enhancement as a \`navigation\` module placed in the page body.
1119
+ *
1107
1120
  * The active site's theme tokens (kywi.config.ts → themes[].tokens) are flattened
1108
1121
  * into a \`:root { --kywi-* }\` block by \`themeTokenStyleBlock\` and injected below,
1109
1122
  * so kywi.config.ts is the single source of truth for the palette and spacing and
@@ -1113,7 +1126,26 @@ const siteTheme =
1113
1126
  config.themes.find((t) => t.name === config.sites[0]?.theme) ?? config.themes[0]
1114
1127
  const themeVars = themeTokenStyleBlock(siteTheme?.tokens)
1115
1128
 
1116
- export default function SiteLayout({ children }: { children: React.ReactNode }) {
1129
+ const NavRenderer = BUILT_IN_MODULE_COMPONENTS.navigation
1130
+
1131
+ // Content edits (a renamed page, a reordered menu) must show up without a
1132
+ // restart — same reasoning as the page route.
1133
+ export const dynamic = 'force-dynamic'
1134
+
1135
+ export default async function SiteLayout({ children }: { children: React.ReactNode }) {
1136
+ const { scope, siteId } = await getKywi()
1137
+
1138
+ // The middleware forwards the full request URL as \`x-kywi-url\` (see
1139
+ // middleware.ts) so this shared layout can resolve which page is current —
1140
+ // without it, isActive/isAncestor on nav items would have nothing to match.
1141
+ const h = await headers()
1142
+ const url = h.get('x-kywi-url')
1143
+ const currentPath = normalizePath(url ? new URL(url).pathname : '/')
1144
+
1145
+ const mainMenu = await scope.menus.getResolved(siteId, 'main', currentPath)
1146
+ const headerItems = mainMenu ?? navTreeToMenuItems(await scope.nav.getTree(siteId, currentPath))
1147
+ const footerItems = await scope.menus.getResolved(siteId, 'footer', currentPath)
1148
+
1117
1149
  return (
1118
1150
  <div className="site-shell">
1119
1151
  {themeVars ? <style dangerouslySetInnerHTML={{ __html: themeVars }} /> : null}
@@ -1121,9 +1153,12 @@ export default function SiteLayout({ children }: { children: React.ReactNode })
1121
1153
  <header className="site-header">
1122
1154
  <div className="site-header__inner">
1123
1155
  <a className="site-brand" href="/">${escapeJsxText(a.projectName)}</a>
1124
- <nav className="site-nav" aria-label="Primary">
1156
+ <div className="site-nav">
1157
+ {headerItems.length > 0 && (
1158
+ <NavRenderer props={{ items: headerItems, variant: 'horizontal', ariaLabel: 'Primary', depth: 2 }} />
1159
+ )}
1125
1160
  <a className="site-nav__admin" href="/admin">Admin →</a>
1126
- </nav>
1161
+ </div>
1127
1162
  </div>
1128
1163
  </header>
1129
1164
 
@@ -1131,7 +1166,10 @@ export default function SiteLayout({ children }: { children: React.ReactNode })
1131
1166
 
1132
1167
  <footer className="site-footer">
1133
1168
  <div className="site-footer__inner">
1134
- Powered by <a href="https://kywi.dev">Kywi CMS</a>
1169
+ {footerItems && footerItems.length > 0 && (
1170
+ <NavRenderer props={{ items: footerItems, variant: 'footer', ariaLabel: 'Footer' }} />
1171
+ )}
1172
+ <p className="site-footer__credit">Powered by <a href="https://kywi.dev">Kywi CMS</a></p>
1135
1173
  </div>
1136
1174
  </footer>
1137
1175
  </div>
@@ -1183,6 +1221,14 @@ function siteStyles(a) {
1183
1221
  color: var(--kywi-color-heading, #0f172a);
1184
1222
  text-decoration: none;
1185
1223
  }
1224
+ /* Wraps the data-driven nav (rendered by core's shared nav renderer — see
1225
+ app/(site)/layout.tsx) alongside the Admin link. The nav itself carries its
1226
+ own .kywi-nav-* classes, styled by @kywi-software/core/site/styles.css. */
1227
+ .site-nav {
1228
+ display: flex;
1229
+ align-items: center;
1230
+ gap: var(--kywi-spacing-md, 1rem);
1231
+ }
1186
1232
  .site-nav__admin {
1187
1233
  color: var(--kywi-color-primary, #2563eb);
1188
1234
  text-decoration: none;
@@ -1229,6 +1275,8 @@ function siteStyles(a) {
1229
1275
  font-size: 0.875rem;
1230
1276
  }
1231
1277
  .site-footer__inner a { color: var(--kywi-color-muted, #64748b); }
1278
+ /* Sits below the optional footer nav (a 'footer' menu, when one exists). */
1279
+ .site-footer__credit { margin: var(--kywi-spacing-md, 1rem) 0 0; }
1232
1280
  `
1233
1281
  }
1234
1282
 
@@ -1239,7 +1287,7 @@ import type { Metadata } from 'next'
1239
1287
  import { notFound } from 'next/navigation'
1240
1288
  import { cookies, headers } from 'next/headers'
1241
1289
  import { KywiBody, KywiEditableAttribute, KywiEditableRegion } from '@kywi-software/core/scope-client'
1242
- import { KywiLayout, KywiRegion, AudienceMetaTags, hydrateLayoutFeeds, type LayoutDocument } from '@kywi-software/core/layout'
1290
+ import { KywiLayout, KywiRegion, AudienceMetaTags, hydrateLayoutFeeds, hydrateLayoutNav, type LayoutDocument } from '@kywi-software/core/layout'
1243
1291
  import { KywiJsonLd } from '@kywi-software/core/scope'
1244
1292
  import { ACCESS_COOKIE, canAccessContent, readSessionClaims } from '@kywi-software/core/host'
1245
1293
  import config from '../../../lib/config'
@@ -1400,7 +1448,15 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
1400
1448
  // hydrate feeds and resolve components on the layout THIS visitor sees.
1401
1449
  // variantContainer arms resolve at render time from \`personalization\`.
1402
1450
  const personalized = personalizeLayout(layout, perso.audienceId)
1403
- const hydrated = await hydrateLayoutFeeds(personalized, buildFeedResolver(runtime))
1451
+ const feedsHydrated = await hydrateLayoutFeeds(personalized, buildFeedResolver(runtime))
1452
+ // Resolve every navMenu/navigation/siteMap module placed in THIS layout
1453
+ // (menuSlug → a real menu; the tree otherwise) — the same seam as feeds,
1454
+ // so a nav module dropped into any region/section just works (#112).
1455
+ const currentPath = String(node['path'] ?? '/')
1456
+ const hydrated = await hydrateLayoutNav(
1457
+ feedsHydrated,
1458
+ runtime.scope.menus.createHydrationResolver(runtime.siteId, currentPath),
1459
+ )
1404
1460
  const componentResolver = await buildComponentResolver(hydrated, runtime)
1405
1461
  const sectionComponentResolver = await buildSectionComponentResolver(hydrated, runtime)
1406
1462
  content = (
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.0",
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",