create-kywi-app 0.6.6 → 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.
- package/assets/agent-patterns.md +58 -2
- package/lib/templates.mjs +63 -7
- package/package.json +1 -1
package/assets/agent-patterns.md
CHANGED
|
@@ -260,8 +260,8 @@ everywhere.
|
|
|
260
260
|
| `name` | Purpose |
|
|
261
261
|
|---|---|
|
|
262
262
|
| `breadcrumbs` | Trail of ancestor links |
|
|
263
|
-
| `navMenu` | A named menu (`menuSlug`)
|
|
264
|
-
| `navigation` | Site navigation
|
|
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 {
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
|
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