@payload-onpage/next 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +20 -2
- package/dist/client.d.ts +25 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +15 -0
- package/dist/client.js.map +1 -0
- package/dist/editor.d.ts +6 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +47 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy.d.ts +9 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +50 -0
- package/dist/proxy.js.map +1 -0
- package/dist/refresh.d.ts +3 -0
- package/dist/refresh.d.ts.map +1 -0
- package/dist/refresh.js +40 -0
- package/dist/refresh.js.map +1 -0
- package/dist/session.d.ts +42 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +131 -0
- package/dist/session.js.map +1 -0
- package/package.json +74 -4
- package/src/client.tsx +40 -0
- package/src/editor.tsx +77 -0
- package/src/index.tsx +42 -0
- package/src/proxy.ts +72 -0
- package/src/refresh.ts +48 -0
- package/src/session.ts +132 -0
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
|
-
#
|
|
1
|
+
# @payload-onpage/next
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/dist/client.d.ts
ADDED
|
@@ -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"}
|
package/dist/editor.d.ts
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/proxy.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/refresh.js
ADDED
|
@@ -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"}
|
package/dist/session.js
ADDED
|
@@ -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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,76 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payload-onpage/next",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.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
|
+
}
|