@payload-onpage/next 0.0.0-stage → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jellologic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,21 @@
1
- # Temporary Holding Version
1
+ # @payload-onpage/next
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ payload-onpage for the Next.js App Router: the session adapter (draft mode, editor binding, sealed preview grants), `<OnPage />`, `onpageContext()`, `isDraftEnabled()`, the `router.refresh()` renderer and the proxy for share links.
4
+
5
+ ```tsx
6
+ // layout
7
+ import { OnPage } from '@payload-onpage/next'
8
+ ;<Suspense fallback={null}>
9
+ <OnPage />
10
+ </Suspense>
11
+
12
+ // proxy.ts
13
+ import { createOnPageProxy } from '@payload-onpage/next/proxy'
14
+ export const proxy = createOnPageProxy({ secret: process.env.PAYLOAD_SECRET })
15
+ ```
16
+
17
+ Entries: `.` (server: `OnPage`, session helpers), `./client` (`OnPageMount`, `OnPageClient`, `useNextRefresh`), `./session` (no client code; for payload.config), `./proxy` (edge).
18
+
19
+ Part of [**payload-onpage**](https://github.com/jellologic/payload-onpage): on-page editing and draft previews for Payload CMS + Next.js. Start with the [getting started guide](https://github.com/jellologic/payload-onpage/blob/main/docs/getting-started.md).
20
+
21
+ MIT licensed.
@@ -0,0 +1,25 @@
1
+ import type { DataSource, Locator, MediaLibrary } from '@payload-onpage/core';
2
+ import type { FieldEditorDef, Messages } from '@payload-onpage/react';
3
+ export interface OnPageMountProps {
4
+ mode: 'edit' | 'view';
5
+ /** Opened from a shared preview link (no editing). */
6
+ shared?: boolean;
7
+ /** Payload API route. Default "/api". */
8
+ apiRoute?: string;
9
+ /** Plugin endpoint prefix. Default "/onpage". */
10
+ prefix?: string;
11
+ /** Payload admin route. Default "/admin". */
12
+ adminRoute?: string;
13
+ /** Locale to edit. Default: <html lang>. */
14
+ locale?: string;
15
+ editors?: Record<string, FieldEditorDef | null>;
16
+ locator?: Locator;
17
+ media?: MediaLibrary;
18
+ dataSource?: DataSource;
19
+ /** Override editor UI strings (see `defaultMessages` in @payload-onpage/react). */
20
+ messages?: Messages;
21
+ }
22
+ export declare function OnPageMount(props: OnPageMountProps): import("react").JSX.Element;
23
+ export { OnPageClient } from '@payload-onpage/react/client';
24
+ export { useNextRefresh } from './refresh.ts';
25
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAErE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;IACrB,sDAAsD;IACtD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC,CAAA;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,mFAAmF;IACnF,QAAQ,CAAC,EAAE,QAAQ,CAAA;CACpB;AAKD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BAElD;AAED,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA"}
package/dist/client.js ADDED
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * Client side of the Next adapter. `OnPageMount` is tiny: the editor itself
5
+ * is loaded only when someone is editing or previewing.
6
+ */
7
+ import dynamic from 'next/dynamic.js';
8
+ const Editor = dynamic(() => import("./editor.js").then((m) => m.NextEditor), { ssr: false });
9
+ const Preview = dynamic(() => import("./editor.js").then((m) => m.NextPreview), { ssr: false });
10
+ export function OnPageMount(props) {
11
+ return props.mode === 'edit' ? _jsx(Editor, { ...props }) : _jsx(Preview, { ...props });
12
+ }
13
+ export { OnPageClient } from '@payload-onpage/react/client';
14
+ export { useNextRefresh } from "./refresh.js";
15
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;GAGG;AACH,OAAO,OAAO,MAAM,iBAAiB,CAAA;AAyBrC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,aAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAA;AAC9F,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,aAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAA;AAEhG,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,OAAO,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAC,MAAM,OAAK,KAAK,GAAI,CAAC,CAAC,CAAC,KAAC,OAAO,OAAK,KAAK,GAAI,CAAA;AAC/E,CAAC;AAED,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,6 @@
1
+ /** The lazily loaded part of the Next adapter: the editor and the preview toolbar, wired to Next and Payload. */
2
+ import React from 'react';
3
+ import type { OnPageMountProps } from './client.tsx';
4
+ export declare function NextEditor(props: OnPageMountProps): React.JSX.Element | null;
5
+ export declare function NextPreview(props: OnPageMountProps): React.JSX.Element;
6
+ //# sourceMappingURL=editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.tsx"],"names":[],"mappings":"AAEA,iHAAiH;AACjH,OAAO,KAAkB,MAAM,OAAO,CAAA;AAMtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AA+BpD,wBAAgB,UAAU,CAAC,KAAK,EAAE,gBAAgB,4BAwBjD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,qBAUlD"}
package/dist/editor.js ADDED
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /** The lazily loaded part of the Next adapter: the editor and the preview toolbar, wired to Next and Payload. */
4
+ import { useMemo } from 'react';
5
+ import { usePathname, useSearchParams } from 'next/navigation.js';
6
+ import { DEFAULTS } from '@payload-onpage/core';
7
+ import { payloadRest, payloadUploads } from '@payload-onpage/core/payload';
8
+ import { OnPageEditor, PreviewToolbar, inPreviewFrame } from '@payload-onpage/react';
9
+ import { lexicalEditors } from '@payload-onpage/lexical';
10
+ import { useNextRefresh } from "./refresh.js";
11
+ function useHere() {
12
+ const pathname = usePathname();
13
+ const search = useSearchParams();
14
+ const qs = search?.toString();
15
+ return `${pathname}${qs ? `?${qs}` : ''}`;
16
+ }
17
+ function useLinks(props) {
18
+ const here = useHere();
19
+ const api = props.apiRoute ?? DEFAULTS.apiRoute;
20
+ const base = `${api}${props.prefix ?? DEFAULTS.routePrefix}`;
21
+ const admin = props.adminRoute ?? DEFAULTS.adminRoute;
22
+ return useMemo(() => ({
23
+ exit: `${base}/exit?path=${encodeURIComponent(here)}`,
24
+ mode: (mode) => `${base}/mode?mode=${mode}&path=${encodeURIComponent(here)}`,
25
+ admin: (ref) => ref.kind === 'global'
26
+ ? `${admin}/globals/${ref.slug}`
27
+ : `${admin}/collections/${ref.slug}/${ref.id}`,
28
+ }), [base, admin, here]);
29
+ }
30
+ const htmlLang = () => (typeof document !== 'undefined' && document.documentElement.lang) || undefined;
31
+ export function NextEditor(props) {
32
+ const locale = props.locale ?? htmlLang();
33
+ const links = useLinks(props);
34
+ const renderer = useNextRefresh();
35
+ const api = props.apiRoute ?? DEFAULTS.apiRoute;
36
+ const dataSource = useMemo(() => props.dataSource ?? payloadRest({ apiBase: api, prefix: props.prefix }), [props.dataSource, api, props.prefix]);
37
+ const media = useMemo(() => props.media ?? payloadUploads({ apiBase: api }), [props.media, api]);
38
+ const editors = useMemo(() => ({ ...lexicalEditors, ...props.editors }), [props.editors]);
39
+ if (inPreviewFrame())
40
+ return null;
41
+ return (_jsx(OnPageEditor, { dataSource: dataSource, media: media, locator: props.locator, renderer: renderer, editors: editors, links: links, locale: locale, messages: props.messages }));
42
+ }
43
+ export function NextPreview(props) {
44
+ const links = useLinks(props);
45
+ return (_jsx(PreviewToolbar, { exitHref: links.exit, editHref: props.shared ? undefined : links.mode?.('edit'), shared: props.shared, messages: props.messages }));
46
+ }
47
+ //# sourceMappingURL=editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.js","sourceRoot":"","sources":["../src/editor.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @payload-onpage/next — payload-onpage for the Next.js App Router.
3
+ *
4
+ * // payload.config.ts (import from /session: it has no client code)
5
+ * import { nextSession } from '@payload-onpage/next/session'
6
+ * plugins: [onpage({ collections: ['pages'], session: nextSession() })]
7
+ *
8
+ * // app/(frontend)/layout.tsx — renders nothing for visitors
9
+ * <Suspense><OnPage /></Suspense>
10
+ *
11
+ * // where the page reads Payload in draft mode
12
+ * payload.find({ collection: 'pages', draft, ...(await onpageContext()) })
13
+ */
14
+ import type { ComponentType } from 'react';
15
+ import { type OnPageMountProps } from './client.tsx';
16
+ export * from './session.ts';
17
+ /** Client-only options can't cross the server/client boundary; pass them from your own `mount`. */
18
+ type ClientOnly = 'mode' | 'shared' | 'editors' | 'locator' | 'media' | 'dataSource';
19
+ export interface OnPageProps extends Omit<OnPageMountProps, ClientOnly> {
20
+ /**
21
+ * Your own client mount, to pass client-only options (custom field
22
+ * editors, locator, media library…):
23
+ *
24
+ * 'use client'
25
+ * export const Mount = (p: OnPageMountProps) => <OnPageMount {...p} editors={{ json: myJsonEditor }} />
26
+ */
27
+ mount?: ComponentType<OnPageMountProps>;
28
+ }
29
+ /**
30
+ * Mounts the editor (edit mode) or the preview toolbar (preview mode) and
31
+ * renders nothing otherwise. Static-safe: it reads cookies only in draft mode.
32
+ */
33
+ export declare function OnPage({ mount: Mount, ...props }: OnPageProps): Promise<import("react").JSX.Element | null>;
34
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAC1C,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAGjE,cAAc,cAAc,CAAA;AAE5B,mGAAmG;AACnG,KAAK,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,YAAY,CAAA;AAEpF,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC;IACrE;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAA;CACxC;AAED;;;GAGG;AACH,wBAAsB,MAAM,CAAC,EAAE,KAAK,EAAE,KAAmB,EAAE,GAAG,KAAK,EAAE,EAAE,WAAW,+CAIjF"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { OnPageMount } from "./client.js";
3
+ import { getOnPageMode, isSharedPreview } from "./session.js";
4
+ export * from "./session.js";
5
+ /**
6
+ * Mounts the editor (edit mode) or the preview toolbar (preview mode) and
7
+ * renders nothing otherwise. Static-safe: it reads cookies only in draft mode.
8
+ */
9
+ export async function OnPage({ mount: Mount = OnPageMount, ...props }) {
10
+ const mode = await getOnPageMode();
11
+ if (!mode)
12
+ return null;
13
+ return _jsx(Mount, { ...props, mode: mode, shared: await isSharedPreview() });
14
+ }
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":";AAcA,OAAO,EAAE,WAAW,EAAyB,MAAM,aAAc,CAAA;AACjE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAE7D,cAAc,cAAc,CAAA;AAgB5B;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,WAAW,EAAE,GAAG,KAAK,EAAe;IAChF,MAAM,IAAI,GAAG,MAAM,aAAa,EAAE,CAAA;IAClC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,OAAO,KAAC,KAAK,OAAK,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,eAAe,EAAE,GAAI,CAAA;AAC1E,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { NextResponse, type NextRequest } from 'next/server.js';
2
+ export interface OnPageProxyOptions {
3
+ /** Same secret as the plugin (default: Payload's `secret`, i.e. PAYLOAD_SECRET). */
4
+ secret: string | undefined;
5
+ }
6
+ /** Request headers for the rest of the pipeline, with draft mode injected for a valid grant. */
7
+ export declare function withOnPage(request: NextRequest, opts: OnPageProxyOptions, headers?: Headers): Promise<Headers>;
8
+ export declare function createOnPageProxy(opts: OnPageProxyOptions): (request: NextRequest) => Promise<NextResponse<unknown>>;
9
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE/D,MAAM,WAAW,kBAAkB;IACjC,oFAAoF;IACpF,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAC3B;AAeD,gGAAgG;AAChG,wBAAsB,UAAU,CAC9B,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,kBAAkB,EACxB,OAAO,GAAE,OAAsC,GAC9C,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,IAK5B,SAAS,WAAW,oCAGjD"}
package/dist/proxy.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The payload-onpage proxy (Next.js middleware). Required for preview links:
3
+ * it unseals the `onpage_preview` grant and, only on the shared page and
4
+ * before the grant expires, injects Next's draft-mode cookie into the request.
5
+ * Static pages then render the draft for the viewer, while the browser never
6
+ * holds a usable draft credential. It also passes the request path to server
7
+ * components.
8
+ *
9
+ * // proxy.ts (Next 16; middleware.ts on Next 15)
10
+ * import { createOnPageProxy } from '@payload-onpage/next/proxy'
11
+ * export const proxy = createOnPageProxy({ secret: process.env.PAYLOAD_SECRET })
12
+ * export const config = { matcher: ['/((?!_next|.*\\..*).*)'] }
13
+ *
14
+ * Already have one? `NextResponse.next({ request: { headers: await withOnPage(request, { secret }) } })`
15
+ */
16
+ import { COOKIE, HEADER, SEAL_PURPOSE, samePath, unseal } from '@payload-onpage/core';
17
+ import { NextResponse } from 'next/server.js';
18
+ const withoutCookies = (header, names) => (header ?? '')
19
+ .split(';')
20
+ .map((c) => c.trim())
21
+ .filter((c) => c && !names.some((n) => c.startsWith(`${n}=`)));
22
+ /** Request headers for the rest of the pipeline, with draft mode injected for a valid grant. */
23
+ export async function withOnPage(request, opts, headers = new Headers(request.headers)) {
24
+ const path = request.nextUrl.pathname;
25
+ headers.delete(HEADER.shared); // never trust it from the client
26
+ headers.set(HEADER.path, path);
27
+ const sealed = request.cookies.get(COOKIE.preview)?.value;
28
+ if (!sealed || !opts.secret)
29
+ return headers;
30
+ const grant = await unseal(sealed, opts.secret, SEAL_PURPOSE.previewCookie);
31
+ if (!grant ||
32
+ typeof grant.draft !== 'string' ||
33
+ grant.exp * 1000 <= Date.now() ||
34
+ !samePath(path, grant.path)) {
35
+ return headers;
36
+ }
37
+ const cookie = withoutCookies(headers.get('cookie'), [COOKIE.nextDraft, COOKIE.mode]);
38
+ cookie.push(`${COOKIE.nextDraft}=${grant.draft}`, `${COOKIE.mode}=view`);
39
+ headers.set('cookie', cookie.join('; '));
40
+ headers.set(HEADER.shared, '1');
41
+ return headers;
42
+ }
43
+ export function createOnPageProxy(opts) {
44
+ if (!opts.secret)
45
+ console.warn('payload-onpage: createOnPageProxy() has no secret; preview links will show published content.');
46
+ return async function proxy(request) {
47
+ return NextResponse.next({ request: { headers: await withOnPage(request, opts) } });
48
+ };
49
+ }
50
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AACrF,OAAO,EAAE,YAAY,EAAoB,MAAM,gBAAgB,CAAA;AAc/D,MAAM,cAAc,GAAG,CAAC,MAAqB,EAAE,KAAe,EAAE,EAAE,CAChE,CAAC,MAAM,IAAI,EAAE,CAAC;KACX,KAAK,CAAC,GAAG,CAAC;KACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;KACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAElE,gGAAgG;AAChG,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAAoB,EACpB,IAAwB,EACxB,UAAmB,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;IAE/C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAA;IACrC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA,CAAC,iCAAiC;IAC/D,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAC9B,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,CAAA;IACzD,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAA;IAC3C,MAAM,KAAK,GAAG,MAAM,MAAM,CAAe,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,aAAa,CAAC,CAAA;IACzF,IACE,CAAC,KAAK;QACN,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAC/B,KAAK,CAAC,GAAG,GAAG,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE;QAC9B,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,EAC3B,CAAC;QACD,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IACrF,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,SAAS,IAAI,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,MAAM,CAAC,IAAI,OAAO,CAAC,CAAA;IACxE,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IACxC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC/B,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAwB;IACxD,IAAI,CAAC,IAAI,CAAC,MAAM;QACd,OAAO,CAAC,IAAI,CACV,+FAA+F,CAChG,CAAA;IACH,OAAO,KAAK,UAAU,KAAK,CAAC,OAAoB;QAC9C,OAAO,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IACrF,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Renderer } from '@payload-onpage/core';
2
+ export declare function useNextRefresh(): Renderer;
3
+ //# sourceMappingURL=refresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../src/refresh.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAKpD,wBAAgB,cAAc,IAAI,QAAQ,CAgCzC"}
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+ /**
3
+ * The Next.js Renderer: after a save, re-render the current route on the
4
+ * server (router.refresh) inside a transition. `afterSave` resolves once the
5
+ * new render is on screen, so the editor knows when to drop its stand-ins.
6
+ * Saves landing close together share one refresh.
7
+ */
8
+ import { useEffect, useMemo, useRef, useTransition } from 'react';
9
+ import { useRouter } from 'next/navigation.js';
10
+ const DEBOUNCE = 60;
11
+ const FALLBACK = 10_000;
12
+ export function useNextRefresh() {
13
+ const router = useRouter();
14
+ const [pending, startTransition] = useTransition();
15
+ const queued = useRef([]);
16
+ const inFlight = useRef([]);
17
+ const timer = useRef(undefined);
18
+ useEffect(() => {
19
+ if (pending || !inFlight.current.length)
20
+ return;
21
+ const done = inFlight.current;
22
+ inFlight.current = [];
23
+ done.forEach((f) => f());
24
+ }, [pending]);
25
+ return useMemo(() => ({
26
+ name: 'next-refresh',
27
+ live: false,
28
+ afterSave: () => new Promise((resolve) => {
29
+ queued.current.push(resolve);
30
+ setTimeout(resolve, FALLBACK); // never hang the editor on a refresh that didn't report back
31
+ clearTimeout(timer.current);
32
+ timer.current = setTimeout(() => {
33
+ inFlight.current.push(...queued.current);
34
+ queued.current = [];
35
+ startTransition(() => router.refresh());
36
+ }, DEBOUNCE);
37
+ }),
38
+ }), [router]);
39
+ }
40
+ //# sourceMappingURL=refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.js","sourceRoot":"","sources":["../src/refresh.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAEZ;;;;;GAKG;AACH,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAG9C,MAAM,QAAQ,GAAG,EAAE,CAAA;AACnB,MAAM,QAAQ,GAAG,MAAM,CAAA;AAEvB,MAAM,UAAU,cAAc;IAC5B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAC1B,MAAM,CAAC,OAAO,EAAE,eAAe,CAAC,GAAG,aAAa,EAAE,CAAA;IAClD,MAAM,MAAM,GAAG,MAAM,CAAoB,EAAE,CAAC,CAAA;IAC5C,MAAM,QAAQ,GAAG,MAAM,CAAoB,EAAE,CAAC,CAAA;IAC9C,MAAM,KAAK,GAAG,MAAM,CAA4C,SAAS,CAAC,CAAA;IAE1E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM;YAAE,OAAM;QAC/C,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAA;QAC7B,QAAQ,CAAC,OAAO,GAAG,EAAE,CAAA;QACrB,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;IAC1B,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IAEb,OAAO,OAAO,CACZ,GAAG,EAAE,CAAC,CAAC;QACL,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE,KAAK;QACX,SAAS,EAAE,GAAG,EAAE,CACd,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAC5B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YAC5B,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA,CAAC,6DAA6D;YAC3F,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YAC3B,KAAK,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC9B,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;gBACxC,MAAM,CAAC,OAAO,GAAG,EAAE,CAAA;gBACnB,eAAe,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;YACzC,CAAC,EAAE,QAAQ,CAAC,CAAA;QACd,CAAC,CAAC;KACL,CAAC,EACF,CAAC,MAAM,CAAC,CACT,CAAA;AACH,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Next.js App Router session adapter and helpers.
3
+ *
4
+ * Editors: Next draft mode plus an `onpage` cookie saying which mode, bound to
5
+ * a hash of their auth cookie, so signing out (or in as someone else) ends it.
6
+ *
7
+ * Preview links: the browser gets only a sealed grant (`onpage_preview`). The
8
+ * proxy (./proxy) unseals it and injects draft mode into the request for the
9
+ * shared page only. The raw Next.js draft secret never reaches the browser.
10
+ *
11
+ * All helpers read `draftMode().isEnabled` first and touch cookies only when
12
+ * draft mode is on. Next exempts that read from prerender bailout, so pages
13
+ * stay static for visitors.
14
+ */
15
+ import { type OnPageMode, type SessionAdapter } from '@payload-onpage/core';
16
+ export interface NextSessionOptions {
17
+ /** How long an editor's edit/preview mode lasts, in seconds. Default 12 hours. */
18
+ maxAge?: number;
19
+ }
20
+ export declare function nextSession(opts?: NextSessionOptions): SessionAdapter;
21
+ /** "edit", "view" or null for this request. Safe to call anywhere (returns null outside a request). */
22
+ export declare function getOnPageMode(): Promise<OnPageMode | null>;
23
+ /**
24
+ * Should this request render drafts? `draftMode().isEnabled`, except that an
25
+ * editor whose session ended (signed out) no longer sees drafts. Draft mode
26
+ * turned on by something else (your own preview route) is respected.
27
+ */
28
+ export declare function isDraftEnabled(): Promise<boolean>;
29
+ /** Is this request a preview-link view (set by the proxy)? */
30
+ export declare function isSharedPreview(): Promise<boolean>;
31
+ /**
32
+ * Spread into Payload reads that feed the page: in edit mode it turns on
33
+ * auto-mapping markers, otherwise it is empty.
34
+ *
35
+ * payload.find({ collection: 'pages', where, draft, ...(await onpageContext()) })
36
+ */
37
+ export declare function onpageContext(): Promise<{
38
+ context?: {
39
+ onpage: true;
40
+ };
41
+ }>;
42
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAA0B,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGnG,MAAM,WAAW,kBAAkB;IACjC,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAyBD,wBAAgB,WAAW,CAAC,IAAI,GAAE,kBAAuB,GAAG,cAAc,CAuCzE;AAED,uGAAuG;AACvG,wBAAsB,aAAa,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAQhE;AAED;;;;GAIG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CASvD;AAED,8DAA8D;AAC9D,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAOxD;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC;IAAE,OAAO,CAAC,EAAE;QAAE,MAAM,EAAE,IAAI,CAAA;KAAE,CAAA;CAAE,CAAC,CAE7E"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Next.js App Router session adapter and helpers.
3
+ *
4
+ * Editors: Next draft mode plus an `onpage` cookie saying which mode, bound to
5
+ * a hash of their auth cookie, so signing out (or in as someone else) ends it.
6
+ *
7
+ * Preview links: the browser gets only a sealed grant (`onpage_preview`). The
8
+ * proxy (./proxy) unseals it and injects draft mode into the request for the
9
+ * shared page only. The raw Next.js draft secret never reaches the browser.
10
+ *
11
+ * All helpers read `draftMode().isEnabled` first and touch cookies only when
12
+ * draft mode is on. Next exempts that read from prerender bailout, so pages
13
+ * stay static for visitors.
14
+ */
15
+ import { COOKIE, HEADER, sha256 } from '@payload-onpage/core';
16
+ import { cookies, draftMode, headers } from 'next/headers.js';
17
+ const HASH_LENGTH = 32;
18
+ const fingerprint = async (token) => (await sha256(token)).slice(0, HASH_LENGTH);
19
+ /** `edit` | `view`, or `<mode>:<authCookie>:<hash>` when bound to a session. */
20
+ async function modeValue(mode, jar, bindTo) {
21
+ const token = bindTo ? jar.get(bindTo)?.value : undefined;
22
+ return token && bindTo ? `${mode}:${bindTo}:${await fingerprint(token)}` : mode;
23
+ }
24
+ /** The mode a cookie value grants in this request, or null (unknown, or the bound session is gone). */
25
+ async function readMode(raw, jar) {
26
+ if (!raw)
27
+ return null;
28
+ const [mode, bound, hash] = raw.split(':');
29
+ if (mode !== 'edit' && mode !== 'view')
30
+ return null;
31
+ if (bound) {
32
+ const token = jar.get(bound)?.value;
33
+ if (!token || (await fingerprint(token)) !== hash)
34
+ return null;
35
+ }
36
+ return mode;
37
+ }
38
+ export function nextSession(opts = {}) {
39
+ const maxAge = opts.maxAge ?? 12 * 3600;
40
+ return {
41
+ async enable(mode, { secure, bindTo }) {
42
+ ;
43
+ (await draftMode()).enable();
44
+ const jar = await cookies();
45
+ jar.set(COOKIE.mode, await modeValue(mode, jar, bindTo), {
46
+ httpOnly: true,
47
+ sameSite: 'lax',
48
+ secure,
49
+ path: '/',
50
+ maxAge,
51
+ });
52
+ jar.delete(COOKIE.preview);
53
+ },
54
+ async enableLink({ secure, path: _path, expiresAt, seal }) {
55
+ ;
56
+ (await draftMode()).enable();
57
+ const jar = await cookies();
58
+ const draft = jar.get(COOKIE.nextDraft)?.value;
59
+ if (!draft)
60
+ throw new Error('payload-onpage: Next.js did not set its draft-mode cookie');
61
+ // Keep the draft secret server-side: the browser only gets the sealed grant.
62
+ jar.delete(COOKIE.nextDraft);
63
+ jar.delete(COOKIE.mode);
64
+ jar.set(COOKIE.preview, await seal(draft), {
65
+ httpOnly: true,
66
+ sameSite: 'lax',
67
+ secure,
68
+ path: '/',
69
+ expires: expiresAt,
70
+ });
71
+ },
72
+ async disable() {
73
+ ;
74
+ (await draftMode()).disable();
75
+ const jar = await cookies();
76
+ jar.delete(COOKIE.mode);
77
+ jar.delete(COOKIE.preview);
78
+ },
79
+ getMode: getOnPageMode,
80
+ };
81
+ }
82
+ /** "edit", "view" or null for this request. Safe to call anywhere (returns null outside a request). */
83
+ export async function getOnPageMode() {
84
+ try {
85
+ if (!(await draftMode()).isEnabled)
86
+ return null;
87
+ const jar = await cookies();
88
+ return await readMode(jar.get(COOKIE.mode)?.value, jar);
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ /**
95
+ * Should this request render drafts? `draftMode().isEnabled`, except that an
96
+ * editor whose session ended (signed out) no longer sees drafts. Draft mode
97
+ * turned on by something else (your own preview route) is respected.
98
+ */
99
+ export async function isDraftEnabled() {
100
+ try {
101
+ if (!(await draftMode()).isEnabled)
102
+ return false;
103
+ const jar = await cookies();
104
+ const raw = jar.get(COOKIE.mode)?.value;
105
+ return raw === undefined || (await readMode(raw, jar)) !== null;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ /** Is this request a preview-link view (set by the proxy)? */
112
+ export async function isSharedPreview() {
113
+ try {
114
+ if (!(await draftMode()).isEnabled)
115
+ return false;
116
+ return (await headers()).get(HEADER.shared) === '1';
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
122
+ /**
123
+ * Spread into Payload reads that feed the page: in edit mode it turns on
124
+ * auto-mapping markers, otherwise it is empty.
125
+ *
126
+ * payload.find({ collection: 'pages', where, draft, ...(await onpageContext()) })
127
+ */
128
+ export async function onpageContext() {
129
+ return (await getOnPageMode()) === 'edit' ? { context: { onpage: true } } : {};
130
+ }
131
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAwC,MAAM,sBAAsB,CAAA;AACnG,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAS7D,MAAM,WAAW,GAAG,EAAE,CAAA;AACtB,MAAM,WAAW,GAAG,KAAK,EAAE,KAAa,EAAE,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAA;AAExF,gFAAgF;AAChF,KAAK,UAAU,SAAS,CAAC,IAAgB,EAAE,GAAQ,EAAE,MAAe;IAClE,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;IACzD,OAAO,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,MAAM,IAAI,MAAM,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACjF,CAAC;AAED,uGAAuG;AACvG,KAAK,UAAU,QAAQ,CAAC,GAAuB,EAAE,GAAQ;IACvD,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAA;IACrB,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC1C,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IACnD,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;QACnC,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,WAAW,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI;YAAE,OAAO,IAAI,CAAA;IAChE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAA2B,EAAE;IACvD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,GAAG,IAAI,CAAA;IACvC,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;YACnC,CAAC;YAAA,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAA;YAC7B,MAAM,GAAG,GAAG,MAAM,OAAO,EAAE,CAAA;YAC3B,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,SAAS,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE;gBACvD,QAAQ,EAAE,IAAI;gBACd,QAAQ,EAAE,KAAK;gBACf,MAAM;gBACN,IAAI,EAAE,GAAG;gBACT,MAAM;aACP,CAAC,CAAA;YACF,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAC5B,CAAC;QACD,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE;YACvD,CAAC;YAAA,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAA;YAC7B,MAAM,GAAG,GAAG,MAAM,OAAO,EAAE,CAAA;YAC3B,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,KAAK,CAAA;YAC9C,IAAI,CAAC,KAAK;gBAAE,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAA;YACxF,6EAA6E;YAC7E,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;YAC5B,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YACvB,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC,KAAK,CAAC,EAAE;gBACzC,QAAQ,EAAE,IAAI;gBACd,QAAQ,EAAE,KAAK;gBACf,MAAM;gBACN,IAAI,EAAE,GAAG;gBACT,OAAO,EAAE,SAAS;aACnB,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,CAAC,OAAO;YACX,CAAC;YAAA,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAA;YAC9B,MAAM,GAAG,GAAG,MAAM,OAAO,EAAE,CAAA;YAC3B,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YACvB,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAC5B,CAAC;QACD,OAAO,EAAE,aAAa;KACvB,CAAA;AACH,CAAC;AAED,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,IAAI,CAAC;QACH,IAAI,CAAC,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC,SAAS;YAAE,OAAO,IAAI,CAAA;QAC/C,MAAM,GAAG,GAAG,MAAM,OAAO,EAAE,CAAA;QAC3B,OAAO,MAAM,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,CAAA;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,IAAI,CAAC;QACH,IAAI,CAAC,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC,SAAS;YAAE,OAAO,KAAK,CAAA;QAChD,MAAM,GAAG,GAAG,MAAM,OAAO,EAAE,CAAA;QAC3B,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,CAAA;QACvC,OAAO,GAAG,KAAK,SAAS,IAAI,CAAC,MAAM,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI,CAAA;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,IAAI,CAAC;QACH,IAAI,CAAC,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC,SAAS;YAAE,OAAO,KAAK,CAAA;QAChD,OAAO,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,GAAG,CAAA;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,OAAO,CAAC,MAAM,aAAa,EAAE,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;AAChF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "@payload-onpage/next",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "payload-onpage for Next.js App Router: draft-mode session adapter, <OnPage />, onpageContext() and refresh rendering.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/jellologic/payload-onpage.git",
11
+ "directory": "packages/next"
12
+ },
13
+ "homepage": "https://github.com/jellologic/payload-onpage#readme",
14
+ "bugs": "https://github.com/jellologic/payload-onpage/issues",
15
+ "keywords": [
16
+ "payload",
17
+ "payloadcms",
18
+ "payload-plugin",
19
+ "nextjs",
20
+ "visual-editing",
21
+ "live-preview",
22
+ "inline-editing",
23
+ "cms"
24
+ ],
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./client": {
31
+ "types": "./dist/client.d.ts",
32
+ "default": "./dist/client.js"
33
+ },
34
+ "./session": {
35
+ "types": "./dist/session.d.ts",
36
+ "default": "./dist/session.js"
37
+ },
38
+ "./proxy": {
39
+ "types": "./dist/proxy.d.ts",
40
+ "default": "./dist/proxy.js"
41
+ }
42
+ },
43
+ "main": "./dist/index.js",
44
+ "types": "./dist/index.d.ts",
45
+ "files": [
46
+ "dist",
47
+ "!dist/.tsbuildinfo",
48
+ "src",
49
+ "!src/**/__tests__",
50
+ "README.md",
51
+ "LICENSE"
52
+ ],
53
+ "scripts": {
54
+ "build": "tsc -b",
55
+ "typecheck": "tsc -b",
56
+ "test": "bun test src",
57
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public",
61
+ "provenance": true
62
+ },
63
+ "dependencies": {
64
+ "@payload-onpage/core": "0.1.0",
65
+ "@payload-onpage/react": "0.1.0",
66
+ "@payload-onpage/lexical": "0.1.0"
67
+ },
68
+ "peerDependencies": {
69
+ "next": "^15.4.11 || ^16.3.8",
70
+ "react": "^19.0.0",
71
+ "react-dom": "^19.0.0"
72
+ },
73
+ "engines": {
74
+ "node": ">=20.9.0"
75
+ }
76
+ }
package/src/client.tsx ADDED
@@ -0,0 +1,40 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Client side of the Next adapter. `OnPageMount` is tiny: the editor itself
5
+ * is loaded only when someone is editing or previewing.
6
+ */
7
+ import dynamic from 'next/dynamic.js'
8
+ import type { DataSource, Locator, MediaLibrary } from '@payload-onpage/core'
9
+ import type { FieldEditorDef, Messages } from '@payload-onpage/react'
10
+
11
+ export interface OnPageMountProps {
12
+ mode: 'edit' | 'view'
13
+ /** Opened from a shared preview link (no editing). */
14
+ shared?: boolean
15
+ /** Payload API route. Default "/api". */
16
+ apiRoute?: string
17
+ /** Plugin endpoint prefix. Default "/onpage". */
18
+ prefix?: string
19
+ /** Payload admin route. Default "/admin". */
20
+ adminRoute?: string
21
+ /** Locale to edit. Default: <html lang>. */
22
+ locale?: string
23
+ // Client-only options (pass them from your own 'use client' mount):
24
+ editors?: Record<string, FieldEditorDef | null>
25
+ locator?: Locator
26
+ media?: MediaLibrary
27
+ dataSource?: DataSource
28
+ /** Override editor UI strings (see `defaultMessages` in @payload-onpage/react). */
29
+ messages?: Messages
30
+ }
31
+
32
+ const Editor = dynamic(() => import('./editor.tsx').then((m) => m.NextEditor), { ssr: false })
33
+ const Preview = dynamic(() => import('./editor.tsx').then((m) => m.NextPreview), { ssr: false })
34
+
35
+ export function OnPageMount(props: OnPageMountProps) {
36
+ return props.mode === 'edit' ? <Editor {...props} /> : <Preview {...props} />
37
+ }
38
+
39
+ export { OnPageClient } from '@payload-onpage/react/client'
40
+ export { useNextRefresh } from './refresh.ts'
package/src/editor.tsx ADDED
@@ -0,0 +1,77 @@
1
+ 'use client'
2
+
3
+ /** The lazily loaded part of the Next adapter: the editor and the preview toolbar, wired to Next and Payload. */
4
+ import React, { useMemo } from 'react'
5
+ import { usePathname, useSearchParams } from 'next/navigation.js'
6
+ import { DEFAULTS, type DocRef } from '@payload-onpage/core'
7
+ import { payloadRest, payloadUploads } from '@payload-onpage/core/payload'
8
+ import { OnPageEditor, PreviewToolbar, inPreviewFrame, type Links } from '@payload-onpage/react'
9
+ import { lexicalEditors } from '@payload-onpage/lexical'
10
+ import type { OnPageMountProps } from './client.tsx'
11
+ import { useNextRefresh } from './refresh.ts'
12
+
13
+ function useHere(): string {
14
+ const pathname = usePathname()
15
+ const search = useSearchParams()
16
+ const qs = search?.toString()
17
+ return `${pathname}${qs ? `?${qs}` : ''}`
18
+ }
19
+
20
+ function useLinks(props: OnPageMountProps): Links {
21
+ const here = useHere()
22
+ const api = props.apiRoute ?? DEFAULTS.apiRoute
23
+ const base = `${api}${props.prefix ?? DEFAULTS.routePrefix}`
24
+ const admin = props.adminRoute ?? DEFAULTS.adminRoute
25
+ return useMemo<Links>(
26
+ () => ({
27
+ exit: `${base}/exit?path=${encodeURIComponent(here)}`,
28
+ mode: (mode) => `${base}/mode?mode=${mode}&path=${encodeURIComponent(here)}`,
29
+ admin: (ref: DocRef) =>
30
+ ref.kind === 'global'
31
+ ? `${admin}/globals/${ref.slug}`
32
+ : `${admin}/collections/${ref.slug}/${ref.id}`,
33
+ }),
34
+ [base, admin, here],
35
+ )
36
+ }
37
+
38
+ const htmlLang = () =>
39
+ (typeof document !== 'undefined' && document.documentElement.lang) || undefined
40
+
41
+ export function NextEditor(props: OnPageMountProps) {
42
+ const locale = props.locale ?? htmlLang()
43
+ const links = useLinks(props)
44
+ const renderer = useNextRefresh()
45
+ const api = props.apiRoute ?? DEFAULTS.apiRoute
46
+ const dataSource = useMemo(
47
+ () => props.dataSource ?? payloadRest({ apiBase: api, prefix: props.prefix }),
48
+ [props.dataSource, api, props.prefix],
49
+ )
50
+ const media = useMemo(() => props.media ?? payloadUploads({ apiBase: api }), [props.media, api])
51
+ const editors = useMemo(() => ({ ...lexicalEditors, ...props.editors }), [props.editors])
52
+ if (inPreviewFrame()) return null
53
+ return (
54
+ <OnPageEditor
55
+ dataSource={dataSource}
56
+ media={media}
57
+ locator={props.locator}
58
+ renderer={renderer}
59
+ editors={editors}
60
+ links={links}
61
+ locale={locale}
62
+ messages={props.messages}
63
+ />
64
+ )
65
+ }
66
+
67
+ export function NextPreview(props: OnPageMountProps) {
68
+ const links = useLinks(props)
69
+ return (
70
+ <PreviewToolbar
71
+ exitHref={links.exit}
72
+ editHref={props.shared ? undefined : links.mode?.('edit')}
73
+ shared={props.shared}
74
+ messages={props.messages}
75
+ />
76
+ )
77
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @payload-onpage/next — payload-onpage for the Next.js App Router.
3
+ *
4
+ * // payload.config.ts (import from /session: it has no client code)
5
+ * import { nextSession } from '@payload-onpage/next/session'
6
+ * plugins: [onpage({ collections: ['pages'], session: nextSession() })]
7
+ *
8
+ * // app/(frontend)/layout.tsx — renders nothing for visitors
9
+ * <Suspense><OnPage /></Suspense>
10
+ *
11
+ * // where the page reads Payload in draft mode
12
+ * payload.find({ collection: 'pages', draft, ...(await onpageContext()) })
13
+ */
14
+ import type { ComponentType } from 'react'
15
+ import { OnPageMount, type OnPageMountProps } from './client.tsx'
16
+ import { getOnPageMode, isSharedPreview } from './session.ts'
17
+
18
+ export * from './session.ts'
19
+
20
+ /** Client-only options can't cross the server/client boundary; pass them from your own `mount`. */
21
+ type ClientOnly = 'mode' | 'shared' | 'editors' | 'locator' | 'media' | 'dataSource'
22
+
23
+ export interface OnPageProps extends Omit<OnPageMountProps, ClientOnly> {
24
+ /**
25
+ * Your own client mount, to pass client-only options (custom field
26
+ * editors, locator, media library…):
27
+ *
28
+ * 'use client'
29
+ * export const Mount = (p: OnPageMountProps) => <OnPageMount {...p} editors={{ json: myJsonEditor }} />
30
+ */
31
+ mount?: ComponentType<OnPageMountProps>
32
+ }
33
+
34
+ /**
35
+ * Mounts the editor (edit mode) or the preview toolbar (preview mode) and
36
+ * renders nothing otherwise. Static-safe: it reads cookies only in draft mode.
37
+ */
38
+ export async function OnPage({ mount: Mount = OnPageMount, ...props }: OnPageProps) {
39
+ const mode = await getOnPageMode()
40
+ if (!mode) return null
41
+ return <Mount {...props} mode={mode} shared={await isSharedPreview()} />
42
+ }
package/src/proxy.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The payload-onpage proxy (Next.js middleware). Required for preview links:
3
+ * it unseals the `onpage_preview` grant and, only on the shared page and
4
+ * before the grant expires, injects Next's draft-mode cookie into the request.
5
+ * Static pages then render the draft for the viewer, while the browser never
6
+ * holds a usable draft credential. It also passes the request path to server
7
+ * components.
8
+ *
9
+ * // proxy.ts (Next 16; middleware.ts on Next 15)
10
+ * import { createOnPageProxy } from '@payload-onpage/next/proxy'
11
+ * export const proxy = createOnPageProxy({ secret: process.env.PAYLOAD_SECRET })
12
+ * export const config = { matcher: ['/((?!_next|.*\\..*).*)'] }
13
+ *
14
+ * Already have one? `NextResponse.next({ request: { headers: await withOnPage(request, { secret }) } })`
15
+ */
16
+ import { COOKIE, HEADER, SEAL_PURPOSE, samePath, unseal } from '@payload-onpage/core'
17
+ import { NextResponse, type NextRequest } from 'next/server.js'
18
+
19
+ export interface OnPageProxyOptions {
20
+ /** Same secret as the plugin (default: Payload's `secret`, i.e. PAYLOAD_SECRET). */
21
+ secret: string | undefined
22
+ }
23
+
24
+ interface PreviewGrant {
25
+ path: string
26
+ ref: string
27
+ exp: number
28
+ draft: string
29
+ }
30
+
31
+ const withoutCookies = (header: string | null, names: string[]) =>
32
+ (header ?? '')
33
+ .split(';')
34
+ .map((c) => c.trim())
35
+ .filter((c) => c && !names.some((n) => c.startsWith(`${n}=`)))
36
+
37
+ /** Request headers for the rest of the pipeline, with draft mode injected for a valid grant. */
38
+ export async function withOnPage(
39
+ request: NextRequest,
40
+ opts: OnPageProxyOptions,
41
+ headers: Headers = new Headers(request.headers),
42
+ ): Promise<Headers> {
43
+ const path = request.nextUrl.pathname
44
+ headers.delete(HEADER.shared) // never trust it from the client
45
+ headers.set(HEADER.path, path)
46
+ const sealed = request.cookies.get(COOKIE.preview)?.value
47
+ if (!sealed || !opts.secret) return headers
48
+ const grant = await unseal<PreviewGrant>(sealed, opts.secret, SEAL_PURPOSE.previewCookie)
49
+ if (
50
+ !grant ||
51
+ typeof grant.draft !== 'string' ||
52
+ grant.exp * 1000 <= Date.now() ||
53
+ !samePath(path, grant.path)
54
+ ) {
55
+ return headers
56
+ }
57
+ const cookie = withoutCookies(headers.get('cookie'), [COOKIE.nextDraft, COOKIE.mode])
58
+ cookie.push(`${COOKIE.nextDraft}=${grant.draft}`, `${COOKIE.mode}=view`)
59
+ headers.set('cookie', cookie.join('; '))
60
+ headers.set(HEADER.shared, '1')
61
+ return headers
62
+ }
63
+
64
+ export function createOnPageProxy(opts: OnPageProxyOptions) {
65
+ if (!opts.secret)
66
+ console.warn(
67
+ 'payload-onpage: createOnPageProxy() has no secret; preview links will show published content.',
68
+ )
69
+ return async function proxy(request: NextRequest) {
70
+ return NextResponse.next({ request: { headers: await withOnPage(request, opts) } })
71
+ }
72
+ }
package/src/refresh.ts ADDED
@@ -0,0 +1,48 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * The Next.js Renderer: after a save, re-render the current route on the
5
+ * server (router.refresh) inside a transition. `afterSave` resolves once the
6
+ * new render is on screen, so the editor knows when to drop its stand-ins.
7
+ * Saves landing close together share one refresh.
8
+ */
9
+ import { useEffect, useMemo, useRef, useTransition } from 'react'
10
+ import { useRouter } from 'next/navigation.js'
11
+ import type { Renderer } from '@payload-onpage/core'
12
+
13
+ const DEBOUNCE = 60
14
+ const FALLBACK = 10_000
15
+
16
+ export function useNextRefresh(): Renderer {
17
+ const router = useRouter()
18
+ const [pending, startTransition] = useTransition()
19
+ const queued = useRef<Array<() => void>>([])
20
+ const inFlight = useRef<Array<() => void>>([])
21
+ const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
22
+
23
+ useEffect(() => {
24
+ if (pending || !inFlight.current.length) return
25
+ const done = inFlight.current
26
+ inFlight.current = []
27
+ done.forEach((f) => f())
28
+ }, [pending])
29
+
30
+ return useMemo<Renderer>(
31
+ () => ({
32
+ name: 'next-refresh',
33
+ live: false,
34
+ afterSave: () =>
35
+ new Promise<void>((resolve) => {
36
+ queued.current.push(resolve)
37
+ setTimeout(resolve, FALLBACK) // never hang the editor on a refresh that didn't report back
38
+ clearTimeout(timer.current)
39
+ timer.current = setTimeout(() => {
40
+ inFlight.current.push(...queued.current)
41
+ queued.current = []
42
+ startTransition(() => router.refresh())
43
+ }, DEBOUNCE)
44
+ }),
45
+ }),
46
+ [router],
47
+ )
48
+ }
package/src/session.ts ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Next.js App Router session adapter and helpers.
3
+ *
4
+ * Editors: Next draft mode plus an `onpage` cookie saying which mode, bound to
5
+ * a hash of their auth cookie, so signing out (or in as someone else) ends it.
6
+ *
7
+ * Preview links: the browser gets only a sealed grant (`onpage_preview`). The
8
+ * proxy (./proxy) unseals it and injects draft mode into the request for the
9
+ * shared page only. The raw Next.js draft secret never reaches the browser.
10
+ *
11
+ * All helpers read `draftMode().isEnabled` first and touch cookies only when
12
+ * draft mode is on. Next exempts that read from prerender bailout, so pages
13
+ * stay static for visitors.
14
+ */
15
+ import { COOKIE, HEADER, sha256, type OnPageMode, type SessionAdapter } from '@payload-onpage/core'
16
+ import { cookies, draftMode, headers } from 'next/headers.js'
17
+
18
+ export interface NextSessionOptions {
19
+ /** How long an editor's edit/preview mode lasts, in seconds. Default 12 hours. */
20
+ maxAge?: number
21
+ }
22
+
23
+ type Jar = Awaited<ReturnType<typeof cookies>>
24
+
25
+ const HASH_LENGTH = 32
26
+ const fingerprint = async (token: string) => (await sha256(token)).slice(0, HASH_LENGTH)
27
+
28
+ /** `edit` | `view`, or `<mode>:<authCookie>:<hash>` when bound to a session. */
29
+ async function modeValue(mode: OnPageMode, jar: Jar, bindTo?: string): Promise<string> {
30
+ const token = bindTo ? jar.get(bindTo)?.value : undefined
31
+ return token && bindTo ? `${mode}:${bindTo}:${await fingerprint(token)}` : mode
32
+ }
33
+
34
+ /** The mode a cookie value grants in this request, or null (unknown, or the bound session is gone). */
35
+ async function readMode(raw: string | undefined, jar: Jar): Promise<OnPageMode | null> {
36
+ if (!raw) return null
37
+ const [mode, bound, hash] = raw.split(':')
38
+ if (mode !== 'edit' && mode !== 'view') return null
39
+ if (bound) {
40
+ const token = jar.get(bound)?.value
41
+ if (!token || (await fingerprint(token)) !== hash) return null
42
+ }
43
+ return mode
44
+ }
45
+
46
+ export function nextSession(opts: NextSessionOptions = {}): SessionAdapter {
47
+ const maxAge = opts.maxAge ?? 12 * 3600
48
+ return {
49
+ async enable(mode, { secure, bindTo }) {
50
+ ;(await draftMode()).enable()
51
+ const jar = await cookies()
52
+ jar.set(COOKIE.mode, await modeValue(mode, jar, bindTo), {
53
+ httpOnly: true,
54
+ sameSite: 'lax',
55
+ secure,
56
+ path: '/',
57
+ maxAge,
58
+ })
59
+ jar.delete(COOKIE.preview)
60
+ },
61
+ async enableLink({ secure, path: _path, expiresAt, seal }) {
62
+ ;(await draftMode()).enable()
63
+ const jar = await cookies()
64
+ const draft = jar.get(COOKIE.nextDraft)?.value
65
+ if (!draft) throw new Error('payload-onpage: Next.js did not set its draft-mode cookie')
66
+ // Keep the draft secret server-side: the browser only gets the sealed grant.
67
+ jar.delete(COOKIE.nextDraft)
68
+ jar.delete(COOKIE.mode)
69
+ jar.set(COOKIE.preview, await seal(draft), {
70
+ httpOnly: true,
71
+ sameSite: 'lax',
72
+ secure,
73
+ path: '/',
74
+ expires: expiresAt,
75
+ })
76
+ },
77
+ async disable() {
78
+ ;(await draftMode()).disable()
79
+ const jar = await cookies()
80
+ jar.delete(COOKIE.mode)
81
+ jar.delete(COOKIE.preview)
82
+ },
83
+ getMode: getOnPageMode,
84
+ }
85
+ }
86
+
87
+ /** "edit", "view" or null for this request. Safe to call anywhere (returns null outside a request). */
88
+ export async function getOnPageMode(): Promise<OnPageMode | null> {
89
+ try {
90
+ if (!(await draftMode()).isEnabled) return null
91
+ const jar = await cookies()
92
+ return await readMode(jar.get(COOKIE.mode)?.value, jar)
93
+ } catch {
94
+ return null
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Should this request render drafts? `draftMode().isEnabled`, except that an
100
+ * editor whose session ended (signed out) no longer sees drafts. Draft mode
101
+ * turned on by something else (your own preview route) is respected.
102
+ */
103
+ export async function isDraftEnabled(): Promise<boolean> {
104
+ try {
105
+ if (!(await draftMode()).isEnabled) return false
106
+ const jar = await cookies()
107
+ const raw = jar.get(COOKIE.mode)?.value
108
+ return raw === undefined || (await readMode(raw, jar)) !== null
109
+ } catch {
110
+ return false
111
+ }
112
+ }
113
+
114
+ /** Is this request a preview-link view (set by the proxy)? */
115
+ export async function isSharedPreview(): Promise<boolean> {
116
+ try {
117
+ if (!(await draftMode()).isEnabled) return false
118
+ return (await headers()).get(HEADER.shared) === '1'
119
+ } catch {
120
+ return false
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Spread into Payload reads that feed the page: in edit mode it turns on
126
+ * auto-mapping markers, otherwise it is empty.
127
+ *
128
+ * payload.find({ collection: 'pages', where, draft, ...(await onpageContext()) })
129
+ */
130
+ export async function onpageContext(): Promise<{ context?: { onpage: true } }> {
131
+ return (await getOnPageMode()) === 'edit' ? { context: { onpage: true } } : {}
132
+ }