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.
- package/assets/agent-patterns.md +58 -2
- package/lib/templates.mjs +114 -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
|
@@ -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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
|
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