payload-live-preview 1.1.0 → 1.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/dist/payload.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var g=Object.defineProperty,o=(e,l)=>g(e,"name",{value:l,configurable:!0});function y(e){let l=e.baseUrl.replace(/\/+$/,""),o=e.fallback??"/",i=void 0===e.previewParam?"preview":e.previewParam;return t=>{let r=u(t.locale),n={data:t.data,locale:r},a=(void 0!==t.collectionConfig?e.collections?.[t.collectionConfig.slug]:void 0)??(void 0!==t.globalConfig?e.globals?.[t.globalConfig.slug]:void 0),c=o;if(void 0!==a){let e="function"==typeof a?a(n):a;e.length>0&&(c=e)}if(c.startsWith("/")||(c=`/${c}`),null===i)return`${l}${c}`;let v=c.includes("?")?"&":"?";return`${l}${c}${v}${i}=true`}}function u(e){if(void 0!==e)return"string"==typeof e?e.length>0?e:void 0:e.code.length>0?e.code:void 0}o(y,"buildLivePreviewUrl"),o(u,"normaliseLocale"),exports.buildLivePreviewUrl=y;
1
+ "use strict";var g=Object.defineProperty,o=(e,l)=>g(e,"name",{value:l,configurable:!0});function b(e){let l=e.baseUrl.replace(/\/+$/,""),o=void 0===e.fallback?"/":e.fallback,i=void 0===e.previewParam?"preview":e.previewParam;return t=>{let n=f(t.locale),r={data:t.data,locale:n},a=v(e,t),c=o;if(void 0!==a){let e="function"==typeof a?a(r):a;if(null===e)return null;e.length>0&&(c=e)}if(null===c)return null;if(c.startsWith("/")||(c=`/${c}`),null===i)return`${l}${c}`;let u=c.includes("?")?"&":"?";return`${l}${c}${u}${i}=true`}}function v(e,l){let o=l.collectionConfig?.slug;if(void 0!==o&&void 0!==e.collections&&Object.hasOwn(e.collections,o))return e.collections[o];let i=l.globalConfig?.slug;return void 0!==i&&void 0!==e.globals&&Object.hasOwn(e.globals,i)?e.globals[i]:void 0}function f(e){if(void 0!==e)return"string"==typeof e?e.length>0?e:void 0:e.code.length>0?e.code:void 0}o(b,"buildLivePreviewUrl"),o(v,"findResolver"),o(f,"normaliseLocale"),exports.buildLivePreviewUrl=b;
2
2
  //# sourceMappingURL=payload.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"payload.cjs","names":["g","Object","defineProperty","o","e","t","value","configurable","buildLivePreviewUrl","options","base","baseUrl","replace","fallback","previewParam","args","locale","normaliseLocale","context","data","resolver","collectionConfig","collections","slug","globalConfig","globals","path","resolved","length","startsWith","separator","includes","code","__name","exports","y"],"sources":["../src/payload/index.ts"],"sourcesContent":["/**\n * Backend-side helpers for `payload.config.ts`.\n *\n * Payload's `admin.livePreview.url` callback maps the document being\n * edited to the frontend URL that should be shown in the preview\n * iframe. In practice every project hand-writes the same lookup-table\n * boilerplate (global → path, collection → localized route, fallback\n * when a new draft has no slug yet). `buildLivePreviewUrl` packages\n * that pattern.\n *\n * This module is dependency-free and framework-agnostic — it does not\n * import `payload`, it just produces a function with the documented\n * callback shape.\n *\n * ```ts\n * // payload.config.ts\n * import { buildLivePreviewUrl } from 'payload-live-preview/payload';\n *\n * admin: {\n * livePreview: {\n * url: buildLivePreviewUrl({\n * baseUrl: process.env.FRONTEND_URL ?? 'http://localhost:4321',\n * collections: {\n * services: ({ data, locale }) => `/${locale}/services/${String(data.slug ?? '')}`,\n * posts: ({ data }) => `/blog/${String(data.slug ?? '')}`,\n * },\n * globals: {\n * homepage: '/',\n * contact: ({ locale }) => `/${locale}/contact`,\n * },\n * fallback: '/',\n * }),\n * collections: ['services', 'posts'],\n * globals: ['homepage', 'contact'],\n * },\n * }\n * ```\n *\n * @module @payload\n */\n\n/** Arguments Payload passes to `admin.livePreview.url`. */\nexport interface LivePreviewUrlArgs {\n readonly data: Record<string, unknown>;\n readonly locale?: string | { readonly code: string };\n readonly collectionConfig?: { readonly slug: string };\n readonly globalConfig?: { readonly slug: string };\n readonly [extra: string]: unknown;\n}\n\n/** Context handed to per-slug path resolvers. */\nexport interface PathResolverContext {\n readonly data: Record<string, unknown>;\n /** Normalised locale code (string), or `undefined`. */\n readonly locale: string | undefined;\n}\n\nexport type PathResolver = string | ((context: PathResolverContext) => string);\n\nexport interface BuildLivePreviewUrlOptions {\n /** Frontend origin, e.g. `https://site.example` (no trailing slash needed). */\n readonly baseUrl: string;\n /** Per-collection path resolvers keyed by collection slug. */\n readonly collections?: Readonly<Record<string, PathResolver>>;\n /** Per-global path resolvers keyed by global slug. */\n readonly globals?: Readonly<Record<string, PathResolver>>;\n /**\n * Path used when no resolver matches, or when a resolver returns an\n * empty string (e.g. a brand-new draft without a slug). Default `/`.\n */\n readonly fallback?: string;\n /**\n * Query parameter appended so the frontend can detect preview intent\n * (`isPreviewRequest` checks it). This client-controlled signal does\n * not authenticate or authorize draft access or response changes.\n * Set `null` to disable. Default `'preview'` → `?preview=true`.\n */\n readonly previewParam?: string | null;\n}\n\n/**\n * Build an `admin.livePreview.url` callback from declarative slug →\n * path mappings.\n */\nexport function buildLivePreviewUrl(\n options: BuildLivePreviewUrlOptions,\n): (args: LivePreviewUrlArgs) => string {\n const base = options.baseUrl.replace(/\\/+$/, '');\n const fallback = options.fallback ?? '/';\n const previewParam = options.previewParam === undefined ? 'preview' : options.previewParam;\n\n return (args) => {\n const locale = normaliseLocale(args.locale);\n const context: PathResolverContext = { data: args.data, locale };\n\n const resolver =\n (args.collectionConfig !== undefined\n ? options.collections?.[args.collectionConfig.slug]\n : undefined) ??\n (args.globalConfig !== undefined ? options.globals?.[args.globalConfig.slug] : undefined);\n\n let path = fallback;\n if (resolver !== undefined) {\n const resolved = typeof resolver === 'function' ? resolver(context) : resolver;\n if (resolved.length > 0) path = resolved;\n }\n if (!path.startsWith('/')) path = `/${path}`;\n\n if (previewParam === null) return `${base}${path}`;\n const separator = path.includes('?') ? '&' : '?';\n return `${base}${path}${separator}${previewParam}=true`;\n };\n}\n\nfunction normaliseLocale(locale: LivePreviewUrlArgs['locale']): string | undefined {\n if (locale === undefined) return undefined;\n if (typeof locale === 'string') return locale.length > 0 ? locale : undefined;\n return locale.code.length > 0 ? locale.code : undefined;\n}\n"],"mappings":"aAoFO,IAAAA,EAAAC,OAAAC,eAAAC,EAAA,CAAAC,EAAAC,IAAAL,EAAAI,EAAA,QAAAE,MAAAD,EAAAE,cAAA,aAASC,EACdC,GAEA,IAAMC,EAAOD,EAAQE,QAAQC,QAAQ,OAAQ,IACvCC,EAAWJ,EAAQI,UAAY,IAC/BC,OAAwC,IAAzBL,EAAQK,aAA6B,UAAYL,EAAQK,aAE9E,OAAQC,IACN,IAAMC,EAASC,EAAgBF,EAAKC,QAC9BE,EAA+B,CAAEC,KAAMJ,EAAKI,KAAMH,UAElDI,QACuB,IAA1BL,EAAKM,iBACFZ,EAAQa,cAAcP,EAAKM,iBAAiBE,WAC5C,UACmB,IAAtBR,EAAKS,aAA6Bf,EAAQgB,UAAUV,EAAKS,aAAaD,WAAQ,GAE7EG,EAAOb,EACX,QAAiB,IAAbO,EAAwB,CAC1B,IAAMO,EAA+B,mBAAbP,EAA0BA,EAASF,GAAWE,EAClEO,EAASC,OAAS,IAAGF,EAAOC,EAClC,CAGA,GAFKD,EAAKG,WAAW,OAAMH,EAAO,IAAIA,KAEjB,OAAjBZ,EAAuB,MAAO,GAAGJ,IAAOgB,IAC5C,IAAMI,EAAYJ,EAAKK,SAAS,KAAO,IAAM,IAC7C,MAAO,GAAGrB,IAAOgB,IAAOI,IAAYhB,SAExC,CAEA,SAASG,EAAgBD,GACvB,QAAe,IAAXA,EACJ,MAAsB,iBAAXA,EAA4BA,EAAOY,OAAS,EAAIZ,OAAS,EAC7DA,EAAOgB,KAAKJ,OAAS,EAAIZ,EAAOgB,UAAO,CAChD,CAlCgBC,EAAAzB,EAAA,uBA8BPyB,EAAAhB,EAAA,mBAAAiB,QAAA1B,oBAAA2B","ignoreList":[]}
1
+ {"version":3,"file":"payload.cjs","names":["buildLivePreviewUrl","options","base","baseUrl","replace","fallback","previewParam","args","locale","normaliseLocale","context","data","entry","findResolver","path","resolved","length","startsWith","separator","includes","collectionSlug","collectionConfig","slug","collections","Object","hasOwn","globalSlug","globalConfig","globals","code","__name","exports","b"],"sources":["../src/payload/index.ts"],"sourcesContent":["/**\n * Backend-side helpers for `payload.config.ts`.\n *\n * Payload's `admin.livePreview.url` callback maps the document being\n * edited to the frontend URL that should be shown in the preview\n * iframe. In practice every project hand-writes the same lookup-table\n * boilerplate (global → path, collection → localized route, fallback\n * when a new draft has no slug yet). `buildLivePreviewUrl` packages\n * that pattern.\n *\n * This module is dependency-free and framework-agnostic — it does not\n * import `payload`, it just produces a function with the documented\n * callback shape.\n *\n * ```ts\n * // payload.config.ts\n * import { buildLivePreviewUrl } from 'payload-live-preview/payload';\n *\n * admin: {\n * livePreview: {\n * url: buildLivePreviewUrl({\n * baseUrl: process.env.FRONTEND_URL ?? 'http://localhost:4321',\n * collections: {\n * services: ({ data, locale }) => `/${locale}/services/${String(data.slug ?? '')}`,\n * posts: ({ data }) => `/blog/${String(data.slug ?? '')}`,\n * },\n * globals: {\n * homepage: '/',\n * contact: ({ locale }) => `/${locale}/contact`,\n * },\n * fallback: '/',\n * }),\n * collections: ['services', 'posts'],\n * globals: ['homepage', 'contact'],\n * },\n * }\n * ```\n *\n * @module @payload\n */\n\n/** Arguments Payload passes to `admin.livePreview.url`. */\nexport interface LivePreviewUrlArgs {\n readonly data: Record<string, unknown>;\n readonly locale?: string | { readonly code: string };\n readonly collectionConfig?: { readonly slug: string };\n readonly globalConfig?: { readonly slug: string };\n readonly [extra: string]: unknown;\n}\n\n/** Context handed to per-slug path resolvers. */\nexport interface PathResolverContext {\n readonly data: Record<string, unknown>;\n /** Normalised locale code (string), or `undefined`. */\n readonly locale: string | undefined;\n}\n\nexport type PathResolver = string | ((context: PathResolverContext) => string);\n\n/**\n * Path resolver that may decline a document entirely.\n *\n * `null` means \"this document has no preview target\" — Payload then shows no\n * preview iframe. An empty string keeps its 1.x meaning and falls back.\n */\nexport type NullablePathResolver =\n string | null | ((context: PathResolverContext) => string | null);\n\nexport interface BuildLivePreviewUrlOptions {\n /** Frontend origin, e.g. `https://site.example` (no trailing slash needed). */\n readonly baseUrl: string;\n /** Per-collection path resolvers keyed by collection slug. */\n readonly collections?: Readonly<Record<string, PathResolver>>;\n /** Per-global path resolvers keyed by global slug. */\n readonly globals?: Readonly<Record<string, PathResolver>>;\n /**\n * Path used when no resolver matches, or when a resolver returns an\n * empty string (e.g. a brand-new draft without a slug). Default `/`.\n */\n readonly fallback?: string;\n /**\n * Query parameter appended so the frontend can detect preview intent\n * (`isPreviewRequest` checks it). This client-controlled signal does\n * not authenticate or authorize draft access or response changes.\n * Set `null` to disable. Default `'preview'` → `?preview=true`.\n */\n readonly previewParam?: string | null;\n}\n\n/**\n * Options that let a document have no preview target at all.\n *\n * Identical to {@link BuildLivePreviewUrlOptions} except that resolvers and\n * `fallback` accept `null`. Using any of them widens the built callback's\n * return type to `string | null`, which is what Payload's own `url` callback\n * accepts — a document without a reachable route should show no iframe rather\n * than silently point at a public page.\n */\nexport interface BuildLivePreviewUrlNullableOptions {\n /** Frontend origin, e.g. `https://site.example` (no trailing slash needed). */\n readonly baseUrl: string;\n /** Per-collection path resolvers keyed by collection slug. */\n readonly collections?: Readonly<Record<string, NullablePathResolver>>;\n /** Per-global path resolvers keyed by global slug. */\n readonly globals?: Readonly<Record<string, NullablePathResolver>>;\n /**\n * Path used when no resolver matches, or when a resolver returns an empty\n * string. `null` declines every unmapped document instead of sending it to\n * a default route. Default `/`.\n */\n readonly fallback?: string | null;\n /** See {@link BuildLivePreviewUrlOptions.previewParam}. */\n readonly previewParam?: string | null;\n}\n\n/**\n * Build an `admin.livePreview.url` callback from declarative slug →\n * path mappings.\n *\n * With string-only resolvers the callback always produces a URL, exactly as\n * in 1.0. Let a resolver or `fallback` return `null` and the callback becomes\n * nullable, so a document with no reachable route resolves to no preview\n * instead of to a default public page.\n */\nexport function buildLivePreviewUrl(\n options: BuildLivePreviewUrlOptions,\n): (args: LivePreviewUrlArgs) => string;\nexport function buildLivePreviewUrl(\n options: BuildLivePreviewUrlNullableOptions,\n): (args: LivePreviewUrlArgs) => string | null;\nexport function buildLivePreviewUrl(\n options: BuildLivePreviewUrlNullableOptions,\n): (args: LivePreviewUrlArgs) => string | null {\n const base = options.baseUrl.replace(/\\/+$/, '');\n // `??` would swallow an explicit `null`, which is the whole point of the\n // nullable form: only an absent option falls back to the default path.\n const fallback = options.fallback === undefined ? '/' : options.fallback;\n const previewParam = options.previewParam === undefined ? 'preview' : options.previewParam;\n\n return (args) => {\n const locale = normaliseLocale(args.locale);\n const context: PathResolverContext = { data: args.data, locale };\n\n // A registered resolver whose value is `null` must not be mistaken for an\n // absent one, so presence is tested rather than coalesced.\n const entry = findResolver(options, args);\n\n let path: string | null = fallback;\n if (entry !== undefined) {\n const resolved = typeof entry === 'function' ? entry(context) : entry;\n // `null` is the resolver declining the document; an empty string keeps\n // its 1.x meaning and falls back.\n if (resolved === null) return null;\n if (resolved.length > 0) path = resolved;\n }\n if (path === null) return null;\n if (!path.startsWith('/')) path = `/${path}`;\n\n if (previewParam === null) return `${base}${path}`;\n const separator = path.includes('?') ? '&' : '?';\n return `${base}${path}${separator}${previewParam}=true`;\n };\n}\n\n/**\n * Registered resolver for this document, or `undefined` when the slug is not\n * mapped at all. A mapped-but-`null` entry returns `null`, which is a resolver\n * value in its own right.\n */\nfunction findResolver(\n options: BuildLivePreviewUrlNullableOptions,\n args: LivePreviewUrlArgs,\n): NullablePathResolver | undefined {\n const collectionSlug = args.collectionConfig?.slug;\n if (\n collectionSlug !== undefined &&\n options.collections !== undefined &&\n Object.hasOwn(options.collections, collectionSlug)\n ) {\n return options.collections[collectionSlug];\n }\n const globalSlug = args.globalConfig?.slug;\n if (\n globalSlug !== undefined &&\n options.globals !== undefined &&\n Object.hasOwn(options.globals, globalSlug)\n ) {\n return options.globals[globalSlug];\n }\n return undefined;\n}\n\nfunction normaliseLocale(locale: LivePreviewUrlArgs['locale']): string | undefined {\n if (locale === undefined) return undefined;\n if (typeof locale === 'string') return locale.length > 0 ? locale : undefined;\n return locale.code.length > 0 ? locale.code : undefined;\n}\n"],"mappings":"wFAkIO,SAASA,EACdC,GAEA,IAAMC,EAAOD,EAAQE,QAAQC,QAAQ,OAAQ,IAGvCC,OAAgC,IAArBJ,EAAQI,SAAyB,IAAMJ,EAAQI,SAC1DC,OAAwC,IAAzBL,EAAQK,aAA6B,UAAYL,EAAQK,aAE9E,OAAQC,IACN,IAAMC,EAASC,EAAgBF,EAAKC,QAC9BE,EAA+B,CAAEC,KAAMJ,EAAKI,KAAMH,UAIlDI,EAAQC,EAAaZ,EAASM,GAEhCO,EAAsBT,EAC1B,QAAc,IAAVO,EAAqB,CACvB,IAAMG,EAA4B,mBAAVH,EAAuBA,EAAMF,GAAWE,EAGhE,GAAiB,OAAbG,EAAmB,OAAO,KAC1BA,EAASC,OAAS,IAAGF,EAAOC,EAClC,CACA,GAAa,OAATD,EAAe,OAAO,KAG1B,GAFKA,EAAKG,WAAW,OAAMH,EAAO,IAAIA,KAEjB,OAAjBR,EAAuB,MAAO,GAAGJ,IAAOY,IAC5C,IAAMI,EAAYJ,EAAKK,SAAS,KAAO,IAAM,IAC7C,MAAO,GAAGjB,IAAOY,IAAOI,IAAYZ,SAExC,CAOA,SAASO,EACPZ,EACAM,GAEA,IAAMa,EAAiBb,EAAKc,kBAAkBC,KAC9C,QACqB,IAAnBF,QACwB,IAAxBnB,EAAQsB,aACRC,OAAOC,OAAOxB,EAAQsB,YAAaH,GAEnC,OAAOnB,EAAQsB,YAAYH,GAE7B,IAAMM,EAAanB,EAAKoB,cAAcL,KACtC,YACiB,IAAfI,QACoB,IAApBzB,EAAQ2B,SACRJ,OAAOC,OAAOxB,EAAQ2B,QAASF,GAExBzB,EAAQ2B,QAAQF,QALzB,CAQF,CAEA,SAASjB,EAAgBD,GACvB,QAAe,IAAXA,EACJ,MAAsB,iBAAXA,EAA4BA,EAAOQ,OAAS,EAAIR,OAAS,EAC7DA,EAAOqB,KAAKb,OAAS,EAAIR,EAAOqB,UAAO,CAChD,CAlEgBC,EAAA9B,EAAA,uBAuCP8B,EAAAjB,EAAA,gBAuBAiB,EAAArB,EAAA,mBAAAsB,QAAA/B,oBAAAgC","ignoreList":[]}
@@ -59,6 +59,13 @@ interface PathResolverContext {
59
59
  readonly locale: string | undefined;
60
60
  }
61
61
  type PathResolver = string | ((context: PathResolverContext) => string);
62
+ /**
63
+ * Path resolver that may decline a document entirely.
64
+ *
65
+ * `null` means "this document has no preview target" — Payload then shows no
66
+ * preview iframe. An empty string keeps its 1.x meaning and falls back.
67
+ */
68
+ type NullablePathResolver = string | null | ((context: PathResolverContext) => string | null);
62
69
  interface BuildLivePreviewUrlOptions {
63
70
  /** Frontend origin, e.g. `https://site.example` (no trailing slash needed). */
64
71
  readonly baseUrl: string;
@@ -79,10 +86,41 @@ interface BuildLivePreviewUrlOptions {
79
86
  */
80
87
  readonly previewParam?: string | null;
81
88
  }
89
+ /**
90
+ * Options that let a document have no preview target at all.
91
+ *
92
+ * Identical to {@link BuildLivePreviewUrlOptions} except that resolvers and
93
+ * `fallback` accept `null`. Using any of them widens the built callback's
94
+ * return type to `string | null`, which is what Payload's own `url` callback
95
+ * accepts — a document without a reachable route should show no iframe rather
96
+ * than silently point at a public page.
97
+ */
98
+ interface BuildLivePreviewUrlNullableOptions {
99
+ /** Frontend origin, e.g. `https://site.example` (no trailing slash needed). */
100
+ readonly baseUrl: string;
101
+ /** Per-collection path resolvers keyed by collection slug. */
102
+ readonly collections?: Readonly<Record<string, NullablePathResolver>>;
103
+ /** Per-global path resolvers keyed by global slug. */
104
+ readonly globals?: Readonly<Record<string, NullablePathResolver>>;
105
+ /**
106
+ * Path used when no resolver matches, or when a resolver returns an empty
107
+ * string. `null` declines every unmapped document instead of sending it to
108
+ * a default route. Default `/`.
109
+ */
110
+ readonly fallback?: string | null;
111
+ /** See {@link BuildLivePreviewUrlOptions.previewParam}. */
112
+ readonly previewParam?: string | null;
113
+ }
82
114
  /**
83
115
  * Build an `admin.livePreview.url` callback from declarative slug →
84
116
  * path mappings.
117
+ *
118
+ * With string-only resolvers the callback always produces a URL, exactly as
119
+ * in 1.0. Let a resolver or `fallback` return `null` and the callback becomes
120
+ * nullable, so a document with no reachable route resolves to no preview
121
+ * instead of to a default public page.
85
122
  */
86
123
  declare function buildLivePreviewUrl(options: BuildLivePreviewUrlOptions): (args: LivePreviewUrlArgs) => string;
124
+ declare function buildLivePreviewUrl(options: BuildLivePreviewUrlNullableOptions): (args: LivePreviewUrlArgs) => string | null;
87
125
 
88
- export { type BuildLivePreviewUrlOptions, type LivePreviewUrlArgs, type PathResolver, type PathResolverContext, buildLivePreviewUrl };
126
+ export { type BuildLivePreviewUrlNullableOptions, type BuildLivePreviewUrlOptions, type LivePreviewUrlArgs, type NullablePathResolver, type PathResolver, type PathResolverContext, buildLivePreviewUrl };
package/dist/payload.d.ts CHANGED
@@ -59,6 +59,13 @@ interface PathResolverContext {
59
59
  readonly locale: string | undefined;
60
60
  }
61
61
  type PathResolver = string | ((context: PathResolverContext) => string);
62
+ /**
63
+ * Path resolver that may decline a document entirely.
64
+ *
65
+ * `null` means "this document has no preview target" — Payload then shows no
66
+ * preview iframe. An empty string keeps its 1.x meaning and falls back.
67
+ */
68
+ type NullablePathResolver = string | null | ((context: PathResolverContext) => string | null);
62
69
  interface BuildLivePreviewUrlOptions {
63
70
  /** Frontend origin, e.g. `https://site.example` (no trailing slash needed). */
64
71
  readonly baseUrl: string;
@@ -79,10 +86,41 @@ interface BuildLivePreviewUrlOptions {
79
86
  */
80
87
  readonly previewParam?: string | null;
81
88
  }
89
+ /**
90
+ * Options that let a document have no preview target at all.
91
+ *
92
+ * Identical to {@link BuildLivePreviewUrlOptions} except that resolvers and
93
+ * `fallback` accept `null`. Using any of them widens the built callback's
94
+ * return type to `string | null`, which is what Payload's own `url` callback
95
+ * accepts — a document without a reachable route should show no iframe rather
96
+ * than silently point at a public page.
97
+ */
98
+ interface BuildLivePreviewUrlNullableOptions {
99
+ /** Frontend origin, e.g. `https://site.example` (no trailing slash needed). */
100
+ readonly baseUrl: string;
101
+ /** Per-collection path resolvers keyed by collection slug. */
102
+ readonly collections?: Readonly<Record<string, NullablePathResolver>>;
103
+ /** Per-global path resolvers keyed by global slug. */
104
+ readonly globals?: Readonly<Record<string, NullablePathResolver>>;
105
+ /**
106
+ * Path used when no resolver matches, or when a resolver returns an empty
107
+ * string. `null` declines every unmapped document instead of sending it to
108
+ * a default route. Default `/`.
109
+ */
110
+ readonly fallback?: string | null;
111
+ /** See {@link BuildLivePreviewUrlOptions.previewParam}. */
112
+ readonly previewParam?: string | null;
113
+ }
82
114
  /**
83
115
  * Build an `admin.livePreview.url` callback from declarative slug →
84
116
  * path mappings.
117
+ *
118
+ * With string-only resolvers the callback always produces a URL, exactly as
119
+ * in 1.0. Let a resolver or `fallback` return `null` and the callback becomes
120
+ * nullable, so a document with no reachable route resolves to no preview
121
+ * instead of to a default public page.
85
122
  */
86
123
  declare function buildLivePreviewUrl(options: BuildLivePreviewUrlOptions): (args: LivePreviewUrlArgs) => string;
124
+ declare function buildLivePreviewUrl(options: BuildLivePreviewUrlNullableOptions): (args: LivePreviewUrlArgs) => string | null;
87
125
 
88
- export { type BuildLivePreviewUrlOptions, type LivePreviewUrlArgs, type PathResolver, type PathResolverContext, buildLivePreviewUrl };
126
+ export { type BuildLivePreviewUrlNullableOptions, type BuildLivePreviewUrlOptions, type LivePreviewUrlArgs, type NullablePathResolver, type PathResolver, type PathResolverContext, buildLivePreviewUrl };
package/dist/payload.js CHANGED
@@ -1,2 +1,2 @@
1
- var e=Object.defineProperty,l=(l,i)=>e(l,"name",{value:i,configurable:!0});function i(e){let l=e.baseUrl.replace(/\/+$/,""),i=e.fallback??"/",t=void 0===e.previewParam?"preview":e.previewParam;return r=>{let n=o(r.locale),a={data:r.data,locale:n},c=(void 0!==r.collectionConfig?e.collections?.[r.collectionConfig.slug]:void 0)??(void 0!==r.globalConfig?e.globals?.[r.globalConfig.slug]:void 0),v=i;if(void 0!==c){let e="function"==typeof c?c(a):c;e.length>0&&(v=e)}if(v.startsWith("/")||(v=`/${v}`),null===t)return`${l}${v}`;let d=v.includes("?")?"&":"?";return`${l}${v}${d}${t}=true`}}function o(e){if(void 0!==e)return"string"==typeof e?e.length>0?e:void 0:e.code.length>0?e.code:void 0}l(i,"buildLivePreviewUrl"),l(o,"normaliseLocale");export{i as buildLivePreviewUrl};
1
+ var e=Object.defineProperty,l=(l,i)=>e(l,"name",{value:i,configurable:!0});function i(e){let l=e.baseUrl.replace(/\/+$/,""),i=void 0===e.fallback?"/":e.fallback,t=void 0===e.previewParam?"preview":e.previewParam;return r=>{let a=n(r.locale),c={data:r.data,locale:a},u=o(e,r),v=i;if(void 0!==u){let e="function"==typeof u?u(c):u;if(null===e)return null;e.length>0&&(v=e)}if(null===v)return null;if(v.startsWith("/")||(v=`/${v}`),null===t)return`${l}${v}`;let d=v.includes("?")?"&":"?";return`${l}${v}${d}${t}=true`}}function o(e,l){let i=l.collectionConfig?.slug;if(void 0!==i&&void 0!==e.collections&&Object.hasOwn(e.collections,i))return e.collections[i];let o=l.globalConfig?.slug;return void 0!==o&&void 0!==e.globals&&Object.hasOwn(e.globals,o)?e.globals[o]:void 0}function n(e){if(void 0!==e)return"string"==typeof e?e.length>0?e:void 0:e.code.length>0?e.code:void 0}l(i,"buildLivePreviewUrl"),l(o,"findResolver"),l(n,"normaliseLocale");export{i as buildLivePreviewUrl};
2
2
  //# sourceMappingURL=payload.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"payload.js","names":["g","Object","defineProperty","o","e","t","value","configurable","buildLivePreviewUrl","options","base","baseUrl","replace","fallback","previewParam","args","locale","normaliseLocale","context","data","resolver","collectionConfig","collections","slug","globalConfig","globals","path","resolved","length","startsWith","separator","includes","code","__name","y"],"sources":["../src/payload/index.ts"],"sourcesContent":["/**\n * Backend-side helpers for `payload.config.ts`.\n *\n * Payload's `admin.livePreview.url` callback maps the document being\n * edited to the frontend URL that should be shown in the preview\n * iframe. In practice every project hand-writes the same lookup-table\n * boilerplate (global → path, collection → localized route, fallback\n * when a new draft has no slug yet). `buildLivePreviewUrl` packages\n * that pattern.\n *\n * This module is dependency-free and framework-agnostic — it does not\n * import `payload`, it just produces a function with the documented\n * callback shape.\n *\n * ```ts\n * // payload.config.ts\n * import { buildLivePreviewUrl } from 'payload-live-preview/payload';\n *\n * admin: {\n * livePreview: {\n * url: buildLivePreviewUrl({\n * baseUrl: process.env.FRONTEND_URL ?? 'http://localhost:4321',\n * collections: {\n * services: ({ data, locale }) => `/${locale}/services/${String(data.slug ?? '')}`,\n * posts: ({ data }) => `/blog/${String(data.slug ?? '')}`,\n * },\n * globals: {\n * homepage: '/',\n * contact: ({ locale }) => `/${locale}/contact`,\n * },\n * fallback: '/',\n * }),\n * collections: ['services', 'posts'],\n * globals: ['homepage', 'contact'],\n * },\n * }\n * ```\n *\n * @module @payload\n */\n\n/** Arguments Payload passes to `admin.livePreview.url`. */\nexport interface LivePreviewUrlArgs {\n readonly data: Record<string, unknown>;\n readonly locale?: string | { readonly code: string };\n readonly collectionConfig?: { readonly slug: string };\n readonly globalConfig?: { readonly slug: string };\n readonly [extra: string]: unknown;\n}\n\n/** Context handed to per-slug path resolvers. */\nexport interface PathResolverContext {\n readonly data: Record<string, unknown>;\n /** Normalised locale code (string), or `undefined`. */\n readonly locale: string | undefined;\n}\n\nexport type PathResolver = string | ((context: PathResolverContext) => string);\n\nexport interface BuildLivePreviewUrlOptions {\n /** Frontend origin, e.g. `https://site.example` (no trailing slash needed). */\n readonly baseUrl: string;\n /** Per-collection path resolvers keyed by collection slug. */\n readonly collections?: Readonly<Record<string, PathResolver>>;\n /** Per-global path resolvers keyed by global slug. */\n readonly globals?: Readonly<Record<string, PathResolver>>;\n /**\n * Path used when no resolver matches, or when a resolver returns an\n * empty string (e.g. a brand-new draft without a slug). Default `/`.\n */\n readonly fallback?: string;\n /**\n * Query parameter appended so the frontend can detect preview intent\n * (`isPreviewRequest` checks it). This client-controlled signal does\n * not authenticate or authorize draft access or response changes.\n * Set `null` to disable. Default `'preview'` → `?preview=true`.\n */\n readonly previewParam?: string | null;\n}\n\n/**\n * Build an `admin.livePreview.url` callback from declarative slug →\n * path mappings.\n */\nexport function buildLivePreviewUrl(\n options: BuildLivePreviewUrlOptions,\n): (args: LivePreviewUrlArgs) => string {\n const base = options.baseUrl.replace(/\\/+$/, '');\n const fallback = options.fallback ?? '/';\n const previewParam = options.previewParam === undefined ? 'preview' : options.previewParam;\n\n return (args) => {\n const locale = normaliseLocale(args.locale);\n const context: PathResolverContext = { data: args.data, locale };\n\n const resolver =\n (args.collectionConfig !== undefined\n ? options.collections?.[args.collectionConfig.slug]\n : undefined) ??\n (args.globalConfig !== undefined ? options.globals?.[args.globalConfig.slug] : undefined);\n\n let path = fallback;\n if (resolver !== undefined) {\n const resolved = typeof resolver === 'function' ? resolver(context) : resolver;\n if (resolved.length > 0) path = resolved;\n }\n if (!path.startsWith('/')) path = `/${path}`;\n\n if (previewParam === null) return `${base}${path}`;\n const separator = path.includes('?') ? '&' : '?';\n return `${base}${path}${separator}${previewParam}=true`;\n };\n}\n\nfunction normaliseLocale(locale: LivePreviewUrlArgs['locale']): string | undefined {\n if (locale === undefined) return undefined;\n if (typeof locale === 'string') return locale.length > 0 ? locale : undefined;\n return locale.code.length > 0 ? locale.code : undefined;\n}\n"],"mappings":"AAoFO,IAAAA,EAAAC,OAAAC,eAAAC,EAAA,CAAAC,EAAAC,IAAAL,EAAAI,EAAA,QAAAE,MAAAD,EAAAE,cAAA,aAASC,EACdC,GAEA,IAAMC,EAAOD,EAAQE,QAAQC,QAAQ,OAAQ,IACvCC,EAAWJ,EAAQI,UAAY,IAC/BC,OAAwC,IAAzBL,EAAQK,aAA6B,UAAYL,EAAQK,aAE9E,OAAQC,IACN,IAAMC,EAASC,EAAgBF,EAAKC,QAC9BE,EAA+B,CAAEC,KAAMJ,EAAKI,KAAMH,UAElDI,QACuB,IAA1BL,EAAKM,iBACFZ,EAAQa,cAAcP,EAAKM,iBAAiBE,WAC5C,UACmB,IAAtBR,EAAKS,aAA6Bf,EAAQgB,UAAUV,EAAKS,aAAaD,WAAQ,GAE7EG,EAAOb,EACX,QAAiB,IAAbO,EAAwB,CAC1B,IAAMO,EAA+B,mBAAbP,EAA0BA,EAASF,GAAWE,EAClEO,EAASC,OAAS,IAAGF,EAAOC,EAClC,CAGA,GAFKD,EAAKG,WAAW,OAAMH,EAAO,IAAIA,KAEjB,OAAjBZ,EAAuB,MAAO,GAAGJ,IAAOgB,IAC5C,IAAMI,EAAYJ,EAAKK,SAAS,KAAO,IAAM,IAC7C,MAAO,GAAGrB,IAAOgB,IAAOI,IAAYhB,SAExC,CAEA,SAASG,EAAgBD,GACvB,QAAe,IAAXA,EACJ,MAAsB,iBAAXA,EAA4BA,EAAOY,OAAS,EAAIZ,OAAS,EAC7DA,EAAOgB,KAAKJ,OAAS,EAAIZ,EAAOgB,UAAO,CAChD,CAlCgBC,EAAAzB,EAAA,uBA8BPyB,EAAAhB,EAAA,0BAAAiB","ignoreList":[]}
1
+ {"version":3,"file":"payload.js","names":["buildLivePreviewUrl","options","base","baseUrl","replace","fallback","previewParam","args","locale","normaliseLocale","context","data","entry","findResolver","path","resolved","length","startsWith","separator","includes","collectionSlug","collectionConfig","slug","collections","Object","hasOwn","globalSlug","globalConfig","globals","code","__name","b"],"sources":["../src/payload/index.ts"],"sourcesContent":["/**\n * Backend-side helpers for `payload.config.ts`.\n *\n * Payload's `admin.livePreview.url` callback maps the document being\n * edited to the frontend URL that should be shown in the preview\n * iframe. In practice every project hand-writes the same lookup-table\n * boilerplate (global → path, collection → localized route, fallback\n * when a new draft has no slug yet). `buildLivePreviewUrl` packages\n * that pattern.\n *\n * This module is dependency-free and framework-agnostic — it does not\n * import `payload`, it just produces a function with the documented\n * callback shape.\n *\n * ```ts\n * // payload.config.ts\n * import { buildLivePreviewUrl } from 'payload-live-preview/payload';\n *\n * admin: {\n * livePreview: {\n * url: buildLivePreviewUrl({\n * baseUrl: process.env.FRONTEND_URL ?? 'http://localhost:4321',\n * collections: {\n * services: ({ data, locale }) => `/${locale}/services/${String(data.slug ?? '')}`,\n * posts: ({ data }) => `/blog/${String(data.slug ?? '')}`,\n * },\n * globals: {\n * homepage: '/',\n * contact: ({ locale }) => `/${locale}/contact`,\n * },\n * fallback: '/',\n * }),\n * collections: ['services', 'posts'],\n * globals: ['homepage', 'contact'],\n * },\n * }\n * ```\n *\n * @module @payload\n */\n\n/** Arguments Payload passes to `admin.livePreview.url`. */\nexport interface LivePreviewUrlArgs {\n readonly data: Record<string, unknown>;\n readonly locale?: string | { readonly code: string };\n readonly collectionConfig?: { readonly slug: string };\n readonly globalConfig?: { readonly slug: string };\n readonly [extra: string]: unknown;\n}\n\n/** Context handed to per-slug path resolvers. */\nexport interface PathResolverContext {\n readonly data: Record<string, unknown>;\n /** Normalised locale code (string), or `undefined`. */\n readonly locale: string | undefined;\n}\n\nexport type PathResolver = string | ((context: PathResolverContext) => string);\n\n/**\n * Path resolver that may decline a document entirely.\n *\n * `null` means \"this document has no preview target\" — Payload then shows no\n * preview iframe. An empty string keeps its 1.x meaning and falls back.\n */\nexport type NullablePathResolver =\n string | null | ((context: PathResolverContext) => string | null);\n\nexport interface BuildLivePreviewUrlOptions {\n /** Frontend origin, e.g. `https://site.example` (no trailing slash needed). */\n readonly baseUrl: string;\n /** Per-collection path resolvers keyed by collection slug. */\n readonly collections?: Readonly<Record<string, PathResolver>>;\n /** Per-global path resolvers keyed by global slug. */\n readonly globals?: Readonly<Record<string, PathResolver>>;\n /**\n * Path used when no resolver matches, or when a resolver returns an\n * empty string (e.g. a brand-new draft without a slug). Default `/`.\n */\n readonly fallback?: string;\n /**\n * Query parameter appended so the frontend can detect preview intent\n * (`isPreviewRequest` checks it). This client-controlled signal does\n * not authenticate or authorize draft access or response changes.\n * Set `null` to disable. Default `'preview'` → `?preview=true`.\n */\n readonly previewParam?: string | null;\n}\n\n/**\n * Options that let a document have no preview target at all.\n *\n * Identical to {@link BuildLivePreviewUrlOptions} except that resolvers and\n * `fallback` accept `null`. Using any of them widens the built callback's\n * return type to `string | null`, which is what Payload's own `url` callback\n * accepts — a document without a reachable route should show no iframe rather\n * than silently point at a public page.\n */\nexport interface BuildLivePreviewUrlNullableOptions {\n /** Frontend origin, e.g. `https://site.example` (no trailing slash needed). */\n readonly baseUrl: string;\n /** Per-collection path resolvers keyed by collection slug. */\n readonly collections?: Readonly<Record<string, NullablePathResolver>>;\n /** Per-global path resolvers keyed by global slug. */\n readonly globals?: Readonly<Record<string, NullablePathResolver>>;\n /**\n * Path used when no resolver matches, or when a resolver returns an empty\n * string. `null` declines every unmapped document instead of sending it to\n * a default route. Default `/`.\n */\n readonly fallback?: string | null;\n /** See {@link BuildLivePreviewUrlOptions.previewParam}. */\n readonly previewParam?: string | null;\n}\n\n/**\n * Build an `admin.livePreview.url` callback from declarative slug →\n * path mappings.\n *\n * With string-only resolvers the callback always produces a URL, exactly as\n * in 1.0. Let a resolver or `fallback` return `null` and the callback becomes\n * nullable, so a document with no reachable route resolves to no preview\n * instead of to a default public page.\n */\nexport function buildLivePreviewUrl(\n options: BuildLivePreviewUrlOptions,\n): (args: LivePreviewUrlArgs) => string;\nexport function buildLivePreviewUrl(\n options: BuildLivePreviewUrlNullableOptions,\n): (args: LivePreviewUrlArgs) => string | null;\nexport function buildLivePreviewUrl(\n options: BuildLivePreviewUrlNullableOptions,\n): (args: LivePreviewUrlArgs) => string | null {\n const base = options.baseUrl.replace(/\\/+$/, '');\n // `??` would swallow an explicit `null`, which is the whole point of the\n // nullable form: only an absent option falls back to the default path.\n const fallback = options.fallback === undefined ? '/' : options.fallback;\n const previewParam = options.previewParam === undefined ? 'preview' : options.previewParam;\n\n return (args) => {\n const locale = normaliseLocale(args.locale);\n const context: PathResolverContext = { data: args.data, locale };\n\n // A registered resolver whose value is `null` must not be mistaken for an\n // absent one, so presence is tested rather than coalesced.\n const entry = findResolver(options, args);\n\n let path: string | null = fallback;\n if (entry !== undefined) {\n const resolved = typeof entry === 'function' ? entry(context) : entry;\n // `null` is the resolver declining the document; an empty string keeps\n // its 1.x meaning and falls back.\n if (resolved === null) return null;\n if (resolved.length > 0) path = resolved;\n }\n if (path === null) return null;\n if (!path.startsWith('/')) path = `/${path}`;\n\n if (previewParam === null) return `${base}${path}`;\n const separator = path.includes('?') ? '&' : '?';\n return `${base}${path}${separator}${previewParam}=true`;\n };\n}\n\n/**\n * Registered resolver for this document, or `undefined` when the slug is not\n * mapped at all. A mapped-but-`null` entry returns `null`, which is a resolver\n * value in its own right.\n */\nfunction findResolver(\n options: BuildLivePreviewUrlNullableOptions,\n args: LivePreviewUrlArgs,\n): NullablePathResolver | undefined {\n const collectionSlug = args.collectionConfig?.slug;\n if (\n collectionSlug !== undefined &&\n options.collections !== undefined &&\n Object.hasOwn(options.collections, collectionSlug)\n ) {\n return options.collections[collectionSlug];\n }\n const globalSlug = args.globalConfig?.slug;\n if (\n globalSlug !== undefined &&\n options.globals !== undefined &&\n Object.hasOwn(options.globals, globalSlug)\n ) {\n return options.globals[globalSlug];\n }\n return undefined;\n}\n\nfunction normaliseLocale(locale: LivePreviewUrlArgs['locale']): string | undefined {\n if (locale === undefined) return undefined;\n if (typeof locale === 'string') return locale.length > 0 ? locale : undefined;\n return locale.code.length > 0 ? locale.code : undefined;\n}\n"],"mappings":"2EAkIO,SAASA,EACdC,GAEA,IAAMC,EAAOD,EAAQE,QAAQC,QAAQ,OAAQ,IAGvCC,OAAgC,IAArBJ,EAAQI,SAAyB,IAAMJ,EAAQI,SAC1DC,OAAwC,IAAzBL,EAAQK,aAA6B,UAAYL,EAAQK,aAE9E,OAAQC,IACN,IAAMC,EAASC,EAAgBF,EAAKC,QAC9BE,EAA+B,CAAEC,KAAMJ,EAAKI,KAAMH,UAIlDI,EAAQC,EAAaZ,EAASM,GAEhCO,EAAsBT,EAC1B,QAAc,IAAVO,EAAqB,CACvB,IAAMG,EAA4B,mBAAVH,EAAuBA,EAAMF,GAAWE,EAGhE,GAAiB,OAAbG,EAAmB,OAAO,KAC1BA,EAASC,OAAS,IAAGF,EAAOC,EAClC,CACA,GAAa,OAATD,EAAe,OAAO,KAG1B,GAFKA,EAAKG,WAAW,OAAMH,EAAO,IAAIA,KAEjB,OAAjBR,EAAuB,MAAO,GAAGJ,IAAOY,IAC5C,IAAMI,EAAYJ,EAAKK,SAAS,KAAO,IAAM,IAC7C,MAAO,GAAGjB,IAAOY,IAAOI,IAAYZ,SAExC,CAOA,SAASO,EACPZ,EACAM,GAEA,IAAMa,EAAiBb,EAAKc,kBAAkBC,KAC9C,QACqB,IAAnBF,QACwB,IAAxBnB,EAAQsB,aACRC,OAAOC,OAAOxB,EAAQsB,YAAaH,GAEnC,OAAOnB,EAAQsB,YAAYH,GAE7B,IAAMM,EAAanB,EAAKoB,cAAcL,KACtC,YACiB,IAAfI,QACoB,IAApBzB,EAAQ2B,SACRJ,OAAOC,OAAOxB,EAAQ2B,QAASF,GAExBzB,EAAQ2B,QAAQF,QALzB,CAQF,CAEA,SAASjB,EAAgBD,GACvB,QAAe,IAAXA,EACJ,MAAsB,iBAAXA,EAA4BA,EAAOQ,OAAS,EAAIR,OAAS,EAC7DA,EAAOqB,KAAKb,OAAS,EAAIR,EAAOqB,UAAO,CAChD,CAlEgBC,EAAA9B,EAAA,uBAuCP8B,EAAAjB,EAAA,gBAuBAiB,EAAArB,EAAA,0BAAAsB","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-live-preview",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Live preview for Payload CMS in Astro and any SSR/static frontend (SvelteKit, Nuxt, Next.js, plain HTML). Patches the DOM live, no rebuild.",
5
5
  "author": "relative23",
6
6
  "license": "MIT",