create-kywi-app 0.6.2 → 0.6.3

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.
Files changed (2) hide show
  1. package/lib/templates.mjs +121 -35
  2. package/package.json +1 -1
package/lib/templates.mjs CHANGED
@@ -755,20 +755,30 @@ export function clientRuntimeEnabled(): boolean {
755
755
 
756
756
  /** The self-ID widget config in the serializable shape the client runtime reads. */
757
757
  export interface PublicSelfIdWidget {
758
- fields: Array<{ id: string; label: string; type: 'select'; options?: string[]; required?: boolean }>
758
+ fields: Array<{
759
+ id: string
760
+ label: string
761
+ type: 'select'
762
+ options?: Array<{ value: string; label: string }>
763
+ required?: boolean
764
+ }>
759
765
  headline: string
760
766
  subheadline?: string
761
767
  submitLabel: string
762
768
  skipLabel?: string
763
- displayMode: 'modal' | 'inline' | 'slide-in'
769
+ displayMode: 'modal' | 'inline' | 'slide-in' | 'hello_bar_top' | 'hello_bar_bottom' | 'drawer'
764
770
  frequency: 'once' | 'session' | 'always'
765
771
  trigger: SelfIdTrigger
766
772
  }
767
773
 
774
+ // The admin's five display modes (SelfIdWidgetConfig['displayMode']) and the
775
+ // client runtime's (PublicSelfIdWidget['displayMode'], @kywi-software/js) are the
776
+ // same set — the runtime implements hello_bar_top/hello_bar_bottom/drawer as
777
+ // their own layouts, not folded into slide-in (kywi-cms#97) — so this is a
778
+ // passthrough. Kept as a named function (rather than assigning displayMode
779
+ // directly) so the two types stay checked against each other at compile time.
768
780
  function mapDisplayMode(mode: SelfIdWidgetConfig['displayMode']): PublicSelfIdWidget['displayMode'] {
769
- if (mode === 'modal') return 'modal'
770
- if (mode === 'inline') return 'inline'
771
- return 'slide-in' // hello bars + drawer both animate in from an edge
781
+ return mode
772
782
  }
773
783
 
774
784
  function mapFrequency(freq: SelfIdWidgetConfig['frequency']): PublicSelfIdWidget['frequency'] {
@@ -797,7 +807,10 @@ export async function resolveSelfIdWidget(runtime: KywiRuntime): Promise<PublicS
797
807
  id: f.id,
798
808
  label: f.label,
799
809
  type: 'select' as const,
800
- options: f.picklist.map((p) => p.value),
810
+ // {value, label} pairs, not bare values — the client runtime renders the
811
+ // label and submits the value (kywi-cms#96); dropping the label here is
812
+ // what made every option render as its raw value.
813
+ options: f.picklist,
801
814
  required: f.required,
802
815
  }))
803
816
  if (fields.length === 0) return null
@@ -921,10 +934,33 @@ async function fetchFreshAccessToken(origin: string, refreshToken: string): Prom
921
934
  }
922
935
  }
923
936
 
937
+ // Markdown content negotiation by URL suffix. Core serves markdown at
938
+ // \`/api/v1/ax/md/slug/<slug>\` and the AX layer enables it (ax.markdown), but the
939
+ // scaffold never wires the friendly \`/<path>.md\` URL the AX pitch (and the
940
+ // developer copy) names — so it 404s despite the feature being on. Rewrite any
941
+ // GET/HEAD for \`/<path>.md\` onto the core route so the named URL actually
942
+ // resolves. Nested paths keep their full slug (e.g. /docs/foo.md -> docs/foo).
943
+ // Never intercepts Next internals or the API — those never carry this suffix,
944
+ // but are excluded explicitly since the matcher's own exclusion is broad.
945
+ function handleMarkdownNegotiation(req: NextRequest): NextResponse | null {
946
+ const { pathname } = req.nextUrl
947
+ if (req.method !== 'GET' && req.method !== 'HEAD') return null
948
+ if (!pathname.endsWith('.md')) return null
949
+ if (pathname.startsWith('/api/') || pathname.startsWith('/_next/')) return null
950
+ const slug = pathname.replace(/^\\//, '').replace(/\\.md$/, '')
951
+ if (!slug) return null
952
+ const url = req.nextUrl.clone()
953
+ url.pathname = '/api/v1/ax/md/slug/' + slug
954
+ return NextResponse.rewrite(url)
955
+ }
956
+
924
957
  export async function middleware(req: NextRequest): Promise<NextResponse> {
925
958
  const { pathname } = req.nextUrl
926
959
  if (isAuthEndpoint(pathname)) return NextResponse.next()
927
960
 
961
+ const md = handleMarkdownNegotiation(req)
962
+ if (md) return md
963
+
928
964
  // Public pages are never auth-gated — just give them a stable visitor id.
929
965
  const isAdmin = pathname === '/admin' || pathname.startsWith('/admin/')
930
966
  const isApi = pathname.startsWith('/api/v1/')
@@ -980,6 +1016,10 @@ export const config = {
980
1016
  // any path with a file extension (static assets, /favicon.ico, /kywi.js, and
981
1017
  // the AX files /robots.txt, /sitemap.xml, /llms*.txt).
982
1018
  '/((?!_next/|.*\\\\..*).*)',
1019
+ // \`/<path>.md\` — the markdown-negotiation URL. The extension-excluding
1020
+ // pattern above skips it, so it needs its own entry to reach
1021
+ // handleMarkdownNegotiation.
1022
+ '/((?!_next/|api/).*\\\\.md)',
983
1023
  ],
984
1024
  }
985
1025
  `
@@ -1239,12 +1279,17 @@ function hasRenderableLayout(layout: LayoutDocument | null | undefined): layout
1239
1279
  // Is the current visitor a signed-in admin who may edit? The public route is
1240
1280
  // outside the middleware's auth matcher, so verify the httpOnly session cookie
1241
1281
  // here and derive the content permissions the overlay needs. Returns null for
1242
- // anyone who cannot edit — no edit DOM is emitted for them.
1282
+ // anyone who cannot edit — no edit DOM (not even the browse-mode toolbar) is
1283
+ // emitted for them. Called on EVERY request (not just ?kywi-edit=1 ones) so the
1284
+ // toolbar can surface itself for a signed-in admin who is just browsing — this
1285
+ // is a cookie read + JWT verify, no DB round-trip, so the cost on an anonymous
1286
+ // visitor's fast path is a fast null return (\`readSessionClaims\` bails
1287
+ // immediately when there's no cookie).
1243
1288
  async function resolveEditPermissions() {
1244
1289
  const token = (await cookies()).get(ACCESS_COOKIE)?.value
1245
1290
  const claims = await readSessionClaims(token, config.auth.secret)
1246
1291
  if (!claims || !canAccessContent(claims.role, 'write')) return null
1247
- return { canEdit: true, canPublish: canAccessContent(claims.role, 'publish') }
1292
+ return { canEdit: true, canPublish: canAccessContent(claims.role, 'publish'), role: claims.role }
1248
1293
  }
1249
1294
 
1250
1295
  // "/" resolves the seeded Home node; any other URL resolves the published node at
@@ -1264,9 +1309,13 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
1264
1309
  const featured = mediaUrl(node['featuredImageId'])
1265
1310
  const layout = node['layout'] as LayoutDocument | null | undefined
1266
1311
 
1267
- // ?kywi-edit=1 opts an authenticated admin into the front-of-site overlay.
1312
+ // Resolved on every request so a signed-in admin gets the persistent browse
1313
+ // toolbar even when just browsing — ?kywi-edit=1 only decides whether the
1314
+ // page auto-enters the full overlay editor on mount (kywi-cms#93). An
1315
+ // anonymous/read-only visitor resolves to perms === null: zero extra client
1316
+ // JS, identical output to before this route ever heard of the overlay.
1268
1317
  const editRequested = (await searchParams)['kywi-edit'] === '1'
1269
- const perms = editRequested ? await resolveEditPermissions() : null
1318
+ const perms = await resolveEditPermissions()
1270
1319
 
1271
1320
  // Server-side personalization (#50), evaluated once: the winning audience (or a
1272
1321
  // kywi_preview_init preview) drives page variants + variantContainers, and this
@@ -1347,12 +1396,16 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
1347
1396
  )
1348
1397
  }
1349
1398
 
1350
- // Mount the overlay (client) only for an authenticated admin who asked for it.
1399
+ // Mount the overlay (client) for any authenticated admin (write role+)
1400
+ // browse mode renders just the slim toolbar; ?kywi-edit=1 (editRequested)
1401
+ // additionally auto-starts the full overlay editor. Nothing mounts for an
1402
+ // anonymous or read-only visitor.
1351
1403
  if (perms) {
1352
1404
  return (
1353
1405
  <KywiFrontEdit
1354
1406
  canEdit={perms.canEdit}
1355
1407
  canPublish={perms.canPublish}
1408
+ editRequested={editRequested}
1356
1409
  contentId={contentId}
1357
1410
  contentType={contentType}
1358
1411
  pageTitle={title}
@@ -1372,18 +1425,25 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
1372
1425
  }
1373
1426
 
1374
1427
  /**
1375
- * Front-of-site edit overlay (client). Mounted by the public page ONLY for an
1376
- * authenticated admin who opened the page with ?kywi-edit=1 (the page verifies
1377
- * the session cookie server-side first).
1428
+ * Front-of-site edit overlay (client). Mounted by the public page for ANY
1429
+ * authenticated admin (write role+) the page verifies the session cookie
1430
+ * server-side first (resolveEditPermissions, called unconditionally) and only
1431
+ * mounts this when that check passes, so canEdit is always true here.
1378
1432
  *
1379
1433
  * Two modes, same component:
1380
- * - **Browse** — core's slim `KywiEditToolbar` over the live page, with the
1381
- * editable regions outlined via the `data-kywi-*` protocol.
1382
- * - **Edit** (`?kywi-edit=1`, or the toolbar's Edit toggle) — the FULL
1383
- * `OverlayShell` layout editor, the same one the admin app mounts, rendered
1384
- * from the same core module registry + custom renderers. Save PUTs the layout
1385
- * document; Publish PUTs the layout then flips status — exactly the API the
1386
- * admin editor uses.
1434
+ * - **Browse** (default) — core's slim `KywiEditToolbar`, a persistent
1435
+ * WordPress-admin-bar-style strip over the live page (page title, draft/
1436
+ * published status, "Edit this page", "Go to admin") — this is the
1437
+ * discoverability fix (kywi-cms#93): an admin browsing the public site
1438
+ * normally, with no query param, now sees the entry point. The editable
1439
+ * regions are also outlined via the `data-kywi-*` protocol.
1440
+ * - **Edit** (the toolbar's "Edit this page", or landing with
1441
+ * `?kywi-edit=1`) — the FULL `OverlayShell` layout editor, the same one the
1442
+ * admin app mounts, rendered from the same core module registry + custom
1443
+ * renderers. Save PUTs the layout document; Publish PUTs the layout then
1444
+ * flips status — exactly the API the admin editor uses. Entering/leaving
1445
+ * edit mode keeps `?kywi-edit=1` in sync via history.replaceState, so a
1446
+ * reload (or a shared link) lands back in the same mode.
1387
1447
  *
1388
1448
  * The OverlayShell is lazy-loaded (`next/dynamic`, client-only): its weight
1389
1449
  * (dnd-kit, canvas, side panels) is only fetched when an editor actually enters
@@ -1420,6 +1480,8 @@ const OverlayShell = dynamic(
1420
1480
  export interface KywiFrontEditProps {
1421
1481
  canEdit: boolean
1422
1482
  canPublish: boolean
1483
+ /** True when the page was requested with \`?kywi-edit=1\` — auto-starts the full overlay editor on mount. */
1484
+ editRequested: boolean
1423
1485
  contentId: string
1424
1486
  contentType: string
1425
1487
  pageTitle: string
@@ -1434,13 +1496,19 @@ export interface KywiFrontEditProps {
1434
1496
  }
1435
1497
 
1436
1498
  /**
1437
- * Wraps the public page with the front-of-site edit affordance. The page only
1438
- * renders this for a signed-in admin who asked to edit (?kywi-edit=1), so the
1439
- * permission gate (canEdit) is always satisfied here.
1499
+ * Wraps the public page with the front-of-site edit affordance. The page
1500
+ * renders this for ANY signed-in admin (write role+) canEdit is always true
1501
+ * here, since the page only mounts KywiFrontEdit once resolveEditPermissions
1502
+ * has already confirmed it server-side. Browse mode shows the persistent
1503
+ * KywiEditToolbar (the primary discoverability fix, kywi-cms#93); the full
1504
+ * OverlayShell editor only mounts once edit mode actually starts — either the
1505
+ * toolbar's "Edit this page" button, or landing with \`?kywi-edit=1\`
1506
+ * (editRequested), which auto-starts it once on mount.
1440
1507
  */
1441
1508
  export function KywiFrontEdit({
1442
1509
  canEdit,
1443
1510
  canPublish,
1511
+ editRequested,
1444
1512
  contentId,
1445
1513
  contentType,
1446
1514
  pageTitle,
@@ -1452,11 +1520,14 @@ export function KywiFrontEdit({
1452
1520
  }: KywiFrontEditProps) {
1453
1521
  const edit = useKywiEditMode({ canEdit, canPublish })
1454
1522
 
1455
- // ?kywi-edit=1 means "enter edit mode now" — flip it on once after mount.
1523
+ // ?kywi-edit=1 (editRequested) means "enter edit mode now" — flip it on once
1524
+ // after mount. Without it the page mounts straight into browse mode (the
1525
+ // persistent toolbar), which is the common case now that KywiFrontEdit
1526
+ // renders for every signed-in admin, not only deep-linked ones.
1456
1527
  const { startEdit, endEdit } = edit
1457
1528
  React.useEffect(() => {
1458
- startEdit()
1459
- }, [startEdit])
1529
+ if (editRequested) startEdit()
1530
+ }, [editRequested, startEdit])
1460
1531
 
1461
1532
  // Registries + renderers for the editor: built from core (no module list is
1462
1533
  // re-declared here) and merged with this app's custom module renderers from
@@ -1514,15 +1585,23 @@ export function KywiFrontEdit({
1514
1585
  }, [contentId])
1515
1586
 
1516
1587
  // Edit mode: the full layout editor, in place, over the live page.
1588
+ // NOTE: no \`kywi-admin-shell\` here (kywi-cms#94). That class is the admin
1589
+ // design system's base+reset — font family, font size, colours, heading
1590
+ // resets — and wrapping the page in it re-typesets the very content the
1591
+ // owner is trying to judge at real width. The editor chrome carries its own
1592
+ // styling; the page keeps the site's.
1517
1593
  if (edit.isEditMode && edit.canEdit) {
1518
1594
  return (
1519
- <div className="kywi-admin-shell kywi-frontend-edit">
1595
+ <div className="kywi-frontend-edit">
1520
1596
  <OverlayShell
1521
1597
  editMode={edit}
1522
1598
  initialLayout={initialLayout}
1523
1599
  contentId={contentId}
1524
1600
  contentType={contentType}
1525
1601
  pageTitle={pageTitle}
1602
+ /* The page's own body wrapper, so the site's page-level CSS (width,
1603
+ gutters, rhythm) still applies while editing in place. */
1604
+ pageClassName="page page--layout"
1526
1605
  themeName="default"
1527
1606
  themeRegistry={themeRegistry}
1528
1607
  moduleRegistry={moduleRegistry}
@@ -1969,14 +2048,21 @@ wired in \`app/(site)/[[...slug]]/page.tsx\`). Pages without a layout fall back
1969
2048
  their Body rich text. Reach for the Layout tab when a page needs sections,
1970
2049
  columns, or modules; use the Body for simple prose.
1971
2050
 
1972
- **Front-of-site editor.** Signed in as an admin, append \`?kywi-edit=1\` to any
1973
- public page to edit it in place. You get the full **Layout editor** (the same
1974
- drag-and-drop canvas, module palette and props panel as the admin's Layout tab),
1975
- mounted right over the live page add sections and modules, then **Save** (PUTs
1976
- the layout) or **Publish** (saves + publishes). Exit the editor for the slim
1977
- browse toolbar with the editable-region outlines. It all lives in
2051
+ **Front-of-site editor.** Signed in as an admin, every public page shows a slim
2052
+ toolbar across the top the page title, its draft/published status, **Edit
2053
+ this page**, and **Go to admin** (a WordPress-admin-bar equivalent, and the
2054
+ primary way to discover in-place editing; there's also an **Edit on site**
2055
+ link on each item in the admin content editor, for the reverse direction).
2056
+ Click **Edit this page** — or open a page with \`?kywi-edit=1\` as a deep link
2057
+ to get the full **Layout editor** (the same drag-and-drop canvas, module
2058
+ palette and props panel as the admin's Layout tab) mounted right over the live
2059
+ page: add sections and modules, then **Save** (PUTs the layout) or **Publish**
2060
+ (saves + publishes). **Done** returns you to the browse toolbar. Entering or
2061
+ leaving the editor keeps \`?kywi-edit=1\` in the URL in sync, so reloading (or
2062
+ sharing the link) lands back in the same mode. It all lives in
1978
2063
  \`app/(site)/kywi-front-edit.tsx\`; the editor bundle (and the admin stylesheet) is
1979
- lazy-loaded, so pages your visitors see never carry its weight. Note: custom
2064
+ lazy-loaded, so pages your visitors see never carry its weight an anonymous
2065
+ or read-only visitor gets no toolbar and no extra client JS. Note: custom
1980
2066
  \`defineModule\` types still RENDER on the canvas, but they don't appear in the
1981
2067
  front-of-site editor's insert palette (it uses the built-in module set) — add
1982
2068
  them from the admin's Layout tab instead.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kywi-app",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
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",