create-kywi-app 0.6.1 → 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.
- package/lib/templates.mjs +121 -35
- 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<{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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 =
|
|
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)
|
|
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
|
|
1376
|
-
* authenticated admin
|
|
1377
|
-
*
|
|
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
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1386
|
-
*
|
|
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
|
|
1438
|
-
* renders this for
|
|
1439
|
-
*
|
|
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
|
|
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-
|
|
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,
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
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
|
|
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