From d8c545e3134d7658515dcda42e9304e57d0077c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 10:57:50 +0000 Subject: [PATCH 1/6] feat: Adding URLField and PublishedAt helpers to payload-helper https://claude.ai/code/session_01NwyDXYNHC8fkcbWpmRpBfg --- .../src/common/PublishedAt.test.ts | 127 ++++++++++++++++++ .../payload-helper/src/common/PublishedAt.ts | 40 ++++++ .../src/common/URLField.test.ts | 81 +++++++++++ .../payload-helper/src/common/URLField.ts | 38 ++++++ packages/payload-helper/src/common/index.ts | 4 + packages/payload-helper/src/index.ts | 4 +- 6 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 packages/payload-helper/src/common/PublishedAt.test.ts create mode 100644 packages/payload-helper/src/common/PublishedAt.ts create mode 100644 packages/payload-helper/src/common/URLField.test.ts create mode 100644 packages/payload-helper/src/common/URLField.ts diff --git a/packages/payload-helper/src/common/PublishedAt.test.ts b/packages/payload-helper/src/common/PublishedAt.test.ts new file mode 100644 index 00000000..373b25eb --- /dev/null +++ b/packages/payload-helper/src/common/PublishedAt.test.ts @@ -0,0 +1,127 @@ +import type { DateField } from 'payload'; +import { describe, expect, test, vi } from 'vitest'; +import { PublishedAt } from './PublishedAt.js'; + +describe('PublishedAt', () => { + test('returns a date field with correct defaults', () => { + const field = PublishedAt(); + + expect(field).toMatchObject({ + name: 'publishedAt', + type: 'date', + required: true, + admin: { + position: 'sidebar', + date: { + pickerAppearance: 'dayOnly', + }, + }, + }); + }); + + test('sets default value to current ISO date string', () => { + const field = PublishedAt(); + const defaultValue = (field as DateField).defaultValue; + + expect(typeof defaultValue).toBe('function'); + + const before = new Date().toISOString(); + const result = (defaultValue as () => string)(); + const after = new Date().toISOString(); + + expect(result >= before).toBe(true); + expect(result <= after).toBe(true); + }); + + test('sets date when status changes to published and value is empty', () => { + const field = PublishedAt(); + const hooks = (field as DateField).hooks?.beforeChange; + expect(hooks).toHaveLength(1); + + const before = new Date(); + const result = hooks?.[0]({ + siblingData: { _status: 'published' }, + value: undefined, + } as unknown as Parameters< + NonNullable['beforeChange']>[0] + >[0]); + const after = new Date(); + + expect(result).toBeInstanceOf(Date); + expect((result as Date).getTime()).toBeGreaterThanOrEqual(before.getTime()); + expect((result as Date).getTime()).toBeLessThanOrEqual(after.getTime()); + }); + + test('preserves existing value when status is published', () => { + const field = PublishedAt(); + const hooks = (field as DateField).hooks?.beforeChange; + + const existingDate = '2025-01-15T00:00:00.000Z'; + const result = hooks?.[0]({ + siblingData: { _status: 'published' }, + value: existingDate, + } as unknown as Parameters< + NonNullable['beforeChange']>[0] + >[0]); + + expect(result).toBe(existingDate); + }); + + test('returns value as-is when status is not published', () => { + const field = PublishedAt(); + const hooks = (field as DateField).hooks?.beforeChange; + + const result = hooks?.[0]({ + siblingData: { _status: 'draft' }, + value: undefined, + } as unknown as Parameters< + NonNullable['beforeChange']>[0] + >[0]); + + expect(result).toBeUndefined(); + }); + + test('applies overrides to the field', () => { + const field = PublishedAt({ + overrides: { + required: false, + label: 'Date Published', + }, + }); + + expect(field).toMatchObject({ + name: 'publishedAt', + type: 'date', + required: false, + label: 'Date Published', + }); + }); + + test('top-level overrides replace admin when admin is overridden', () => { + const field = PublishedAt({ + overrides: { + admin: { + position: 'sidebar', + description: 'Custom description', + }, + }, + }); + + expect((field as DateField).admin).toMatchObject({ + position: 'sidebar', + description: 'Custom description', + }); + }); + + test('returns correct defaults with no arguments', () => { + const field = PublishedAt(); + + expect(field).toMatchObject({ + name: 'publishedAt', + type: 'date', + required: true, + }); + expect((field as DateField).hooks?.beforeChange).toHaveLength(1); + expect(typeof (field as DateField).defaultValue).toBe('function'); + }); +}); diff --git a/packages/payload-helper/src/common/PublishedAt.ts b/packages/payload-helper/src/common/PublishedAt.ts new file mode 100644 index 00000000..68b68e69 --- /dev/null +++ b/packages/payload-helper/src/common/PublishedAt.ts @@ -0,0 +1,40 @@ +import type { DateField, Field } from 'payload'; + +export type PublishedAtArgs = { + overrides?: Partial; +}; + +/** + * Creates a published at date field with sensible defaults. + * + * Automatically sets the current date as default value and populates + * the field when a document is first published. + * + * @param args - Optional arguments to customise the field. + */ +export const PublishedAt = (args?: PublishedAtArgs): Field => { + return { + name: 'publishedAt', + type: 'date', + required: true, + defaultValue: () => new Date().toISOString(), + admin: { + position: 'sidebar', + date: { + pickerAppearance: 'dayOnly', + }, + ...args?.overrides?.admin, + }, + hooks: { + beforeChange: [ + ({ siblingData, value }) => { + if (siblingData._status === 'published' && !value) { + return new Date(); + } + return value; + }, + ], + }, + ...args?.overrides, + }; +}; diff --git a/packages/payload-helper/src/common/URLField.test.ts b/packages/payload-helper/src/common/URLField.test.ts new file mode 100644 index 00000000..99b14b58 --- /dev/null +++ b/packages/payload-helper/src/common/URLField.test.ts @@ -0,0 +1,81 @@ +import type { Field, FieldHookArgs, TextField, TypeWithID } from 'payload'; +import { describe, expect, test, vi } from 'vitest'; +import { URLField } from './URLField.js'; + +describe('URLField', () => { + test('returns a text field with correct defaults', () => { + const field = URLField({ + generate: () => 'https://example.com', + }); + + expect(field).toMatchObject({ + name: 'url', + label: 'URL', + type: 'text', + virtual: true, + admin: { + readOnly: true, + position: 'sidebar', + }, + }); + }); + + test('calls generate function in afterRead hook', async () => { + const generate = vi.fn(() => 'https://example.com/page'); + + const field = URLField({ generate }); + const hooks = (field as TextField).hooks?.afterRead; + expect(hooks).toHaveLength(1); + + const result = await hooks?.[0]({ draft: false } as unknown as FieldHookArgs); + expect(generate).toHaveBeenCalled(); + expect(result).toBe('https://example.com/page'); + }); + + test('appends draft query parameter when in draft mode', async () => { + const field = URLField({ + generate: () => 'https://example.com/page', + }); + + const hooks = (field as TextField).hooks?.afterRead; + const result = await hooks?.[0]({ draft: true } as unknown as FieldHookArgs); + expect(result).toBe('https://example.com/page?draft=true'); + }); + + test('handles async generate function', async () => { + const field = URLField({ + generate: async () => 'https://example.com/async', + }); + + const hooks = (field as TextField).hooks?.afterRead; + const result = await hooks?.[0]({ draft: false } as unknown as FieldHookArgs); + expect(result).toBe('https://example.com/async'); + }); + + test('applies overrides to the base field', () => { + const field = URLField({ + generate: () => 'https://example.com', + overrides: { + name: 'customUrl', + label: 'Custom URL', + }, + }); + + expect(field).toMatchObject({ + name: 'customUrl', + label: 'Custom URL', + type: 'text', + }); + }); + + test('uses empty object when overrides not provided', () => { + const field = URLField({ + generate: () => 'https://example.com', + }); + + expect(field).toMatchObject({ + name: 'url', + type: 'text', + }); + }); +}); diff --git a/packages/payload-helper/src/common/URLField.ts b/packages/payload-helper/src/common/URLField.ts new file mode 100644 index 00000000..88bd66ac --- /dev/null +++ b/packages/payload-helper/src/common/URLField.ts @@ -0,0 +1,38 @@ +import type { Field, FieldHookArgs, TextField, TypeWithID } from 'payload'; +import { deepMerge } from 'payload'; + +export type URLFieldArgs = { + overrides?: Partial>; + generate: (args: FieldHookArgs) => string | Promise; +}; + +/** + * Creates a virtual URL field with a custom generation function. + * + * @param generate - A function that generates the URL based on the field data. + * @param overrides - Optional overrides to customise the field. + */ +export const URLField = ({ generate, overrides }: URLFieldArgs): Field => { + const baseField: Field = { + name: 'url', + label: 'URL', + type: 'text', + admin: { + readOnly: true, + position: 'sidebar', + }, + virtual: true, + hooks: { + afterRead: [ + async (args: FieldHookArgs) => { + let url = await generate(args); + if (args.draft) { + url += '?draft=true'; + } + return url; + }, + ], + }, + }; + return deepMerge>>(baseField, overrides || {}); +}; diff --git a/packages/payload-helper/src/common/index.ts b/packages/payload-helper/src/common/index.ts index f13e597f..41822517 100644 --- a/packages/payload-helper/src/common/index.ts +++ b/packages/payload-helper/src/common/index.ts @@ -1 +1,5 @@ +export { PublishedAt } from './PublishedAt.js'; +export type { PublishedAtArgs } from './PublishedAt.js'; export { SEOFields } from './SEO.js'; +export { URLField } from './URLField.js'; +export type { URLFieldArgs } from './URLField.js'; diff --git a/packages/payload-helper/src/index.ts b/packages/payload-helper/src/index.ts index 731dd04c..7aa85522 100644 --- a/packages/payload-helper/src/index.ts +++ b/packages/payload-helper/src/index.ts @@ -44,7 +44,9 @@ export { } from './util/index.js'; // Common/Reusable -export { SEOFields } from './common/index.js'; +export { PublishedAt, SEOFields, URLField } from './common/index.js'; +export type { PublishedAtArgs } from './common/index.js'; +export type { URLFieldArgs } from './common/index.js'; // Email Config Helper export { defineEmailConfig } from './email/defineEmailConfig.js'; From 2c77cca3d32e8da2f1e29b24639c5fe581ff5e24 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 11:01:13 +0000 Subject: [PATCH 2/6] style: Moving URLField and PublishedAt to src/fields directory https://claude.ai/code/session_01NwyDXYNHC8fkcbWpmRpBfg --- packages/payload-helper/package.json | 4 ++++ packages/payload-helper/src/common/index.ts | 4 ---- .../src/{common => fields}/PublishedAt.test.ts | 0 .../payload-helper/src/{common => fields}/PublishedAt.ts | 0 .../src/{common => fields}/URLField.test.ts | 0 .../payload-helper/src/{common => fields}/URLField.ts | 0 packages/payload-helper/src/fields/index.ts | 4 ++++ packages/payload-helper/src/index.ts | 8 +++++--- 8 files changed, 13 insertions(+), 7 deletions(-) rename packages/payload-helper/src/{common => fields}/PublishedAt.test.ts (100%) rename packages/payload-helper/src/{common => fields}/PublishedAt.ts (100%) rename packages/payload-helper/src/{common => fields}/URLField.test.ts (100%) rename packages/payload-helper/src/{common => fields}/URLField.ts (100%) create mode 100644 packages/payload-helper/src/fields/index.ts diff --git a/packages/payload-helper/package.json b/packages/payload-helper/package.json index 0e8873d5..c720208b 100644 --- a/packages/payload-helper/package.json +++ b/packages/payload-helper/package.json @@ -43,6 +43,10 @@ "types": "./dist/endpoints/index.d.ts", "import": "./dist/endpoints/index.js" }, + "./fields": { + "types": "./dist/fields/index.d.ts", + "import": "./dist/fields/index.js" + }, "./dist/admin/components/*": { "types": "./dist/admin/components/*.d.ts", "import": "./dist/admin/components/*.js", diff --git a/packages/payload-helper/src/common/index.ts b/packages/payload-helper/src/common/index.ts index 41822517..f13e597f 100644 --- a/packages/payload-helper/src/common/index.ts +++ b/packages/payload-helper/src/common/index.ts @@ -1,5 +1 @@ -export { PublishedAt } from './PublishedAt.js'; -export type { PublishedAtArgs } from './PublishedAt.js'; export { SEOFields } from './SEO.js'; -export { URLField } from './URLField.js'; -export type { URLFieldArgs } from './URLField.js'; diff --git a/packages/payload-helper/src/common/PublishedAt.test.ts b/packages/payload-helper/src/fields/PublishedAt.test.ts similarity index 100% rename from packages/payload-helper/src/common/PublishedAt.test.ts rename to packages/payload-helper/src/fields/PublishedAt.test.ts diff --git a/packages/payload-helper/src/common/PublishedAt.ts b/packages/payload-helper/src/fields/PublishedAt.ts similarity index 100% rename from packages/payload-helper/src/common/PublishedAt.ts rename to packages/payload-helper/src/fields/PublishedAt.ts diff --git a/packages/payload-helper/src/common/URLField.test.ts b/packages/payload-helper/src/fields/URLField.test.ts similarity index 100% rename from packages/payload-helper/src/common/URLField.test.ts rename to packages/payload-helper/src/fields/URLField.test.ts diff --git a/packages/payload-helper/src/common/URLField.ts b/packages/payload-helper/src/fields/URLField.ts similarity index 100% rename from packages/payload-helper/src/common/URLField.ts rename to packages/payload-helper/src/fields/URLField.ts diff --git a/packages/payload-helper/src/fields/index.ts b/packages/payload-helper/src/fields/index.ts new file mode 100644 index 00000000..1a9c0bce --- /dev/null +++ b/packages/payload-helper/src/fields/index.ts @@ -0,0 +1,4 @@ +export { PublishedAt } from './PublishedAt.js'; +export type { PublishedAtArgs } from './PublishedAt.js'; +export { URLField } from './URLField.js'; +export type { URLFieldArgs } from './URLField.js'; diff --git a/packages/payload-helper/src/index.ts b/packages/payload-helper/src/index.ts index 7aa85522..b21631a3 100644 --- a/packages/payload-helper/src/index.ts +++ b/packages/payload-helper/src/index.ts @@ -44,9 +44,11 @@ export { } from './util/index.js'; // Common/Reusable -export { PublishedAt, SEOFields, URLField } from './common/index.js'; -export type { PublishedAtArgs } from './common/index.js'; -export type { URLFieldArgs } from './common/index.js'; +export { SEOFields } from './common/index.js'; + +// Fields +export { PublishedAt, URLField } from './fields/index.js'; +export type { PublishedAtArgs, URLFieldArgs } from './fields/index.js'; // Email Config Helper export { defineEmailConfig } from './email/defineEmailConfig.js'; From 9025c0477316c1701ba24d55714259d693693a7f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 11:13:34 +0000 Subject: [PATCH 3/6] chore: add changeset for fields directory move https://claude.ai/code/session_01NwyDXYNHC8fkcbWpmRpBfg --- .changeset/move-fields-directory.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/move-fields-directory.md diff --git a/.changeset/move-fields-directory.md b/.changeset/move-fields-directory.md new file mode 100644 index 00000000..54959e2e --- /dev/null +++ b/.changeset/move-fields-directory.md @@ -0,0 +1,5 @@ +--- +'@ainsleydev/payload-helper': patch +--- + +Moved URLField and PublishedAt to src/fields and added ./fields export path From a75095c8cf5180ff329475e047b3a2bdd1dd737f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 11:37:57 +0000 Subject: [PATCH 4/6] fix: guard URLField against undefined generate return value When generate() returned undefined and draft mode was active, the hook would produce the string "undefined?draft=true". Now returns undefined early when generate yields no value. https://claude.ai/code/session_01NwyDXYNHC8fkcbWpmRpBfg --- .changeset/move-fields-directory.md | 2 +- packages/payload-helper/src/fields/URLField.test.ts | 7 +++++++ packages/payload-helper/src/fields/URLField.ts | 7 +++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.changeset/move-fields-directory.md b/.changeset/move-fields-directory.md index 54959e2e..5236c093 100644 --- a/.changeset/move-fields-directory.md +++ b/.changeset/move-fields-directory.md @@ -2,4 +2,4 @@ '@ainsleydev/payload-helper': patch --- -Moved URLField and PublishedAt to src/fields and added ./fields export path +Add PublishedAt and URLField field helpers, expose via ./fields export path, and fix URLField crash when generate returns undefined diff --git a/packages/payload-helper/src/fields/URLField.test.ts b/packages/payload-helper/src/fields/URLField.test.ts index 99b14b58..8add605a 100644 --- a/packages/payload-helper/src/fields/URLField.test.ts +++ b/packages/payload-helper/src/fields/URLField.test.ts @@ -68,6 +68,13 @@ describe('URLField', () => { }); }); + test('returns undefined when generate returns undefined', async () => { + const field = URLField({ generate: async () => undefined }); + const hooks = (field as TextField).hooks?.afterRead; + const result = await hooks?.[0]({ draft: true } as unknown as FieldHookArgs); + expect(result).toBeUndefined(); + }); + test('uses empty object when overrides not provided', () => { const field = URLField({ generate: () => 'https://example.com', diff --git a/packages/payload-helper/src/fields/URLField.ts b/packages/payload-helper/src/fields/URLField.ts index 88bd66ac..1b73428f 100644 --- a/packages/payload-helper/src/fields/URLField.ts +++ b/packages/payload-helper/src/fields/URLField.ts @@ -25,9 +25,12 @@ export const URLField = ({ generate, overrides }: URLField hooks: { afterRead: [ async (args: FieldHookArgs) => { - let url = await generate(args); + const url = await generate(args); + if (!url) { + return url; + } if (args.draft) { - url += '?draft=true'; + return `${url}?draft=true`; } return url; }, From 171ab54524e0ebd44fd6fd0cbc368ebfa1cecae8 Mon Sep 17 00:00:00 2001 From: Ainsley Clark <34712954+ainsleyclark@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:54:58 +0000 Subject: [PATCH 5/6] fix: use deepMerge in PublishedAt, fix URLField draft query param, update exports (#416) - PublishedAt: replace spread overrides with deepMerge for proper nested merging - URLField: use URL API to correctly append draft param when URL has existing query string - common/index.ts: re-export PublishedAt and URLField from common entry point - Changeset: reword description to match actual PR scope https://claude.ai/code/session_019KQTSWuJGhcoeP1WmXkaLZ Co-authored-by: Claude --- .changeset/move-fields-directory.md | 2 +- packages/payload-helper/src/common/index.ts | 4 ++++ packages/payload-helper/src/fields/PublishedAt.test.ts | 5 ++++- packages/payload-helper/src/fields/PublishedAt.ts | 6 +++--- packages/payload-helper/src/fields/URLField.test.ts | 10 ++++++++++ packages/payload-helper/src/fields/URLField.ts | 4 +++- 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/.changeset/move-fields-directory.md b/.changeset/move-fields-directory.md index 5236c093..4f188548 100644 --- a/.changeset/move-fields-directory.md +++ b/.changeset/move-fields-directory.md @@ -2,4 +2,4 @@ '@ainsleydev/payload-helper': patch --- -Add PublishedAt and URLField field helpers, expose via ./fields export path, and fix URLField crash when generate returns undefined +Add PublishedAt and URLField field helpers and expose via ./fields export path diff --git a/packages/payload-helper/src/common/index.ts b/packages/payload-helper/src/common/index.ts index f13e597f..95994eaa 100644 --- a/packages/payload-helper/src/common/index.ts +++ b/packages/payload-helper/src/common/index.ts @@ -1 +1,5 @@ export { SEOFields } from './SEO.js'; +export { PublishedAt } from '../fields/PublishedAt.js'; +export type { PublishedAtArgs } from '../fields/PublishedAt.js'; +export { URLField } from '../fields/URLField.js'; +export type { URLFieldArgs } from '../fields/URLField.js'; diff --git a/packages/payload-helper/src/fields/PublishedAt.test.ts b/packages/payload-helper/src/fields/PublishedAt.test.ts index 373b25eb..b3bc59dd 100644 --- a/packages/payload-helper/src/fields/PublishedAt.test.ts +++ b/packages/payload-helper/src/fields/PublishedAt.test.ts @@ -97,7 +97,7 @@ describe('PublishedAt', () => { }); }); - test('top-level overrides replace admin when admin is overridden', () => { + test('deep merges admin overrides with defaults', () => { const field = PublishedAt({ overrides: { admin: { @@ -110,6 +110,9 @@ describe('PublishedAt', () => { expect((field as DateField).admin).toMatchObject({ position: 'sidebar', description: 'Custom description', + date: { + pickerAppearance: 'dayOnly', + }, }); }); diff --git a/packages/payload-helper/src/fields/PublishedAt.ts b/packages/payload-helper/src/fields/PublishedAt.ts index 68b68e69..448694a9 100644 --- a/packages/payload-helper/src/fields/PublishedAt.ts +++ b/packages/payload-helper/src/fields/PublishedAt.ts @@ -1,4 +1,5 @@ import type { DateField, Field } from 'payload'; +import { deepMerge } from 'payload'; export type PublishedAtArgs = { overrides?: Partial; @@ -13,7 +14,7 @@ export type PublishedAtArgs = { * @param args - Optional arguments to customise the field. */ export const PublishedAt = (args?: PublishedAtArgs): Field => { - return { + const baseField: Field = { name: 'publishedAt', type: 'date', required: true, @@ -23,7 +24,6 @@ export const PublishedAt = (args?: PublishedAtArgs): Field => { date: { pickerAppearance: 'dayOnly', }, - ...args?.overrides?.admin, }, hooks: { beforeChange: [ @@ -35,6 +35,6 @@ export const PublishedAt = (args?: PublishedAtArgs): Field => { }, ], }, - ...args?.overrides, }; + return deepMerge>(baseField, args?.overrides || {}); }; diff --git a/packages/payload-helper/src/fields/URLField.test.ts b/packages/payload-helper/src/fields/URLField.test.ts index 8add605a..0d0f6fde 100644 --- a/packages/payload-helper/src/fields/URLField.test.ts +++ b/packages/payload-helper/src/fields/URLField.test.ts @@ -42,6 +42,16 @@ describe('URLField', () => { expect(result).toBe('https://example.com/page?draft=true'); }); + test('appends draft parameter correctly when URL already has query params', async () => { + const field = URLField({ + generate: () => 'https://example.com/page?foo=bar', + }); + + const hooks = (field as TextField).hooks?.afterRead; + const result = await hooks?.[0]({ draft: true } as unknown as FieldHookArgs); + expect(result).toBe('https://example.com/page?foo=bar&draft=true'); + }); + test('handles async generate function', async () => { const field = URLField({ generate: async () => 'https://example.com/async', diff --git a/packages/payload-helper/src/fields/URLField.ts b/packages/payload-helper/src/fields/URLField.ts index 1b73428f..5921d734 100644 --- a/packages/payload-helper/src/fields/URLField.ts +++ b/packages/payload-helper/src/fields/URLField.ts @@ -30,7 +30,9 @@ export const URLField = ({ generate, overrides }: URLField return url; } if (args.draft) { - return `${url}?draft=true`; + const u = new URL(url); + u.searchParams.set('draft', 'true'); + return u.toString(); } return url; }, From 6cb4dd92578e19c1a7d278e11bc1e95737b52994 Mon Sep 17 00:00:00 2001 From: Ainsley Date: Tue, 17 Feb 2026 12:01:32 +0000 Subject: [PATCH 6/6] fix: URL stuff --- .changeset/payload-helper-minor.md | 5 +++++ packages/payload-helper/src/fields/URLField.ts | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 .changeset/payload-helper-minor.md diff --git a/.changeset/payload-helper-minor.md b/.changeset/payload-helper-minor.md new file mode 100644 index 00000000..465441ec --- /dev/null +++ b/.changeset/payload-helper-minor.md @@ -0,0 +1,5 @@ +--- +"@ainsleydev/sveltekit-helper": minor +--- + +Add Payload SEO helper components and tidy metadata handling. diff --git a/packages/payload-helper/src/fields/URLField.ts b/packages/payload-helper/src/fields/URLField.ts index 5921d734..357220d7 100644 --- a/packages/payload-helper/src/fields/URLField.ts +++ b/packages/payload-helper/src/fields/URLField.ts @@ -30,9 +30,15 @@ export const URLField = ({ generate, overrides }: URLField return url; } if (args.draft) { - const u = new URL(url); - u.searchParams.set('draft', 'true'); - return u.toString(); + try { + const u = new URL(url); + u.searchParams.set('draft', 'true'); + return u.toString(); + } catch { + // Relative URL — append manually + const sep = url.includes('?') ? '&' : '?'; + return `${url}${sep}draft=true`; + } } return url; },