payload-plugin-urls
v0.9.5
Published
Payload CMS plugin for URL utilities.
Downloads
498
Maintainers
Readme
payload-plugin-urls
Payload CMS plugin for localized document URLs, root page prefixes, and categorized collection paths.
Usage
import { urlsPlugin } from "payload-plugin-urls"
export default buildConfig({
plugins: [
urlsPlugin({
collections: {
pages: {
breadcrumbs: true,
},
posts: {
rootPage: "postsPage",
prefixStrategy: "category",
category: {
collection: "post-categories",
field: "categories",
},
},
recipes: {
rootPage: "recipesPage",
prefixStrategy: "rootPage",
category: {
collection: "recipe-categories",
field: "categories",
},
},
},
rootPages: {
fields: {
homePage: {
isHomepage: true,
relationTo: "pages",
overrides: {
label: "Homepage",
admin: {
description: "Select the page used as /",
width: "100%",
},
},
},
postsPage: {
relationTo: "pages",
overrides: {
label: "Blog index",
required: true,
},
},
recipesPage: {
relationTo: "recipes",
},
},
},
overrides: {
label: "Site structure",
hooks: {
afterChange: [
async () => {
/* runs after the plugin's root-pages URL hook */
},
],
},
},
}),
],
})The plugin adds a managed root-pages global. Define every root page relationship in
rootPages.fields. Mark one field with isHomepage: true so that document resolves to / for the
default locale and /<locale> for other locales. Each field can set relationTo and overrides,
which are merged into the generated relationship field. Collection rootPage values such as
recipesPage define URL prefixes for documents in that collection.
Root-pages global (overrides)
overrides is an optional Partial<GlobalConfig> merged into the generated root-pages global
after the plugin's defaults. Use it for label, access, extra hooks, admin,
and other global options. Hook arrays are concatenated: the plugin's hooks run first, then
yours. The fields key in overrides is ignored so the plugin keeps control of the
relationship fields; customize those per field with rootPages.fields[fieldName].overrides instead.
Other options
field— Rename the hiddenpopulatedUrlfield or pass field-leveloverrides(for exampleadminUI tweaks).locales— Optional. When omitted, locales are taken from Payloadlocalization. SetdefaultLocaleandlocalesexplicitly to override that, or when using exported helpers outside the normal plugin lifecycle.
Categorized collections can choose how their prefix is built:
prefixStrategy: "rootPage"produces paths like/blog/hello-world.prefixStrategy: "category"produces paths like/blog/news/hello-world.
Each configured collection receives a hidden localized populatedUrl field and a beforeChange
hook that recalculates it. The plugin also installs an update-urls workflow and queues URL updates
when page, category, or root page relationships change. By default it then calls
payload.jobs.runByID on that job so the workflow runs in the same request (set
delayJobsRun: true to enqueue only and rely on your job runner or cron). Bulk
writes from the workflow use req.context.disableRevalidate (with disablePopulateUrl and
disableUrlUpdates) so cache plugins such as payload-plugin-cache skip per-row invalidation;
revalidate tags like sitemap yourself after large migrations if needed.
Exports
The package default export is the same function as urlsPlugin. Import runtime helpers and
TypeScript types from payload-plugin-urls. Import the Next.js App Router client CmsLink from the
payload-plugin-urls/next submodule (optional peer
dependency on next and React).
urlsPlugin
Registers the root-pages global, URL hooks and fields on configured collections, and the
update-urls job workflow.
interface PayloadPluginUrls {
(options?: PayloadPluginUrlsOptions): PayloadPlugin
}options— Plugin configuration; seePayloadPluginUrlsOptions. When omitted, the plugin is a no-op and returns the Payload config unchanged.
populatedUrlField
Builds the hidden, localized populatedUrl text field definition (same shape the plugin injects
into collections). Use it if you register the field manually.
interface PopulatedUrlField {
(options?: PayloadPluginUrlsOptions): Field
}options— Same plugin options object used forname/overridesunderfield; defaults to{ collections: {} }if omitted.
resolvePopulatedUrl
Async helper that computes the public URL path for a document using your plugin options, locale,
merged document data, and the root-pages global snapshot.
interface ResolvePopulatedUrl {
(args: ResolvePopulatedUrlArgs): Promise<string | undefined>
}ResolvePopulatedUrlArgs:
interface ResolvePopulatedUrlArgs {
/** Collection slug configured under `options.collections`. */
collection: string
/** Incoming payload data for the document (merged with `originalDoc` internally). */
data: Partial<UrlDoc>
/** Locale code to resolve for. */
locale: Locale
/** Full plugin options including `collections` and `locales`. */
options: PayloadPluginUrlsOptions
/** Prior revision used to fill missing fields when resolving. */
originalDoc?: Partial<UrlDoc>
/** Optional Payload instance for loading related docs when needed. */
payload?: PayloadLike
/** Loaded `root-pages` global document for this locale. */
rootPages: RootPagesDoc
}getDocumentLink
Builds a site path from a populated relationship. Reads populatedUrl from the related document
when present; otherwise derives segments from plugin options and document shape. Throws if
reference.value is a plain id string instead of an expanded doc.
interface GetDocumentLink {
(reference: GetDocumentLinkArgs, context: GetDocumentLinkContext): string
}reference— Which collection the relationship targets and the relatedUrlDoc(expanded).GetDocumentLinkArgs:interface GetDocumentLinkArgs { relationTo: string value: string | UrlDoc }relationTo— Target collection slug.value— Related document; must be an object withpopulatedUrl/ category / slug data, not an unresolved id string.
context— Locale, plugin options, optional publicbaseUrl, and optionalurlPrefixStrategyoverride when building category paths.GetDocumentLinkContext:interface GetDocumentLinkContext { baseUrl?: string locale: Locale options: PayloadPluginUrlsOptions urlPrefixStrategy?: UrlPrefixStrategy }
Next.js exports (payload-plugin-urls/next)
Client CmsLink for Payload-style link fields: resolves internal paths with
getDocumentLink, highlights the active destination with usePathname, handles
same-document #hash targets with configurable scroll offset (hashScrollYOffset), resets scroll
near the viewport top on client navigations (unless opening a new tab or modifier-click), and
renders disabled links as plain span elements instead of anchors.
Requires optional peer dependencies next, react, and react-dom.
"use client"
import type { ReactNode } from "react"
import type { PayloadPluginUrlsOptions, UrlDoc } from "payload-plugin-urls"
import { CmsLink } from "payload-plugin-urls/next"
const urlPluginOptions = {} satisfies PayloadPluginUrlsOptions
interface CmsRelationship {
relationTo: string
value: UrlDoc | string
}
export function NavLinkDemo(props: {
locale: string
siteUrl: string
type?: string | null
url?: string | null
reference?: CmsRelationship | null
newTab?: boolean | null
/** Resolve localized labels yourself; pass the result as children. */
children: ReactNode
}) {
const { locale, siteUrl, type, url, reference, newTab, children } = props
return (
<CmsLink
siteUrl={siteUrl}
urls={{ locale, options: urlPluginOptions }}
className="text-primary underline"
type={type ?? undefined}
url={url ?? undefined}
reference={reference && typeof reference.value === "object" ? reference : undefined}
newTab={newTab ?? undefined}
>
{children}
</CmsLink>
)
}siteUrl— Public site base (https://example.com-style origin is enough); used withURLandusePathnamefor same-origin and active-route checks.urls—locale,options(your plugin config), optionalbaseUrl, optionalurlPrefixStrategy(same meanings asgetDocumentLink).
getDocumentLinkBySlugs
Joins URL segments with an optional locale or baseUrl prefix. Lower-level helper used when you
already know slug segments.
interface GetDocumentLinkBySlugs {
(slugs: string[], context: GetDocumentLinkBySlugsContext): string
}GetDocumentLinkBySlugsContext:
interface GetDocumentLinkBySlugsContext {
/** When set, prefixed before segments (can include origin or path prefix). */
baseUrl?: string
/** Declares which collection the path belongs to (for consistency with call sites). */
collection: string
locale: Locale
options: PayloadPluginUrlsOptions
}slugs— Path segments in order (for example category path + page slug).context— Locale and options for default-locale handling;collectionis part of the public signature for naming consistency with internal routing.
withLocalePrefix
Prefixes path with /<locale> when locale is not the configured default locale.
interface WithLocalePrefix {
(path: string, locale: Locale, options: PayloadPluginUrlsOptions): string
}path— Path starting with/(trailing slash normalized away in the result).locale— Active locale.options— Plugin options (uses normalizedlocales.defaultLocale).
withoutTrailingSlash
Normalizes a path to start with / and removes a trailing slash.
interface WithoutTrailingSlash {
(path: string): string
}path— Raw path string.
getRootPageChangeSources
Returns job input sources describing which collections and homepage pages need URL refreshes after the root-pages global changes. Used internally and available for custom workflows.
interface GetRootPageChangeSources {
(
current: RootPagesDoc,
previous: RootPagesDoc | undefined,
options: PayloadPluginUrlsOptions,
): UrlUpdateSource[]
}current— New root-pages field values (ids or embedded docs).previous— Previous root-pages snapshot, orundefinedon first save.options— Plugin options determining homepage field androotPagemappings.
hasUrlPathChanged
Compares populatedUrl on two document-like values when both exist; otherwise compares slug.
Handy in hooks to detect URL-relevant edits.
interface HasUrlPathChanged {
(doc: unknown, previousDoc: unknown): boolean
}doc— Current document or partial.previousDoc— Prior revision for comparison.
Exported types
References to GlobalConfig, Field, LabelFunction, StaticLabel, and PayloadPlugin use the
same names as in payload. The following
interfaces are exported from payload-plugin-urls for use in your codebase.
PayloadPluginUrlsOptions
interface PayloadPluginUrlsOptions {
/** Collections that participate in URL generation and how each behaves. */
collections: Record<string, UrlCollectionOptions>
/** Applied to the generated `root-pages` global only; see [Root-pages global](#root-pages-global-overrides). */
overrides?: Partial<GlobalConfig>
/** Customize the hidden URL field name or field config. */
field?: {
name?: string
overrides?: Record<string, unknown>
}
/** Override inferred localization or use helpers outside Payload. */
locales?: {
defaultLocale?: Locale
locales?: Locale[]
}
/** Root global slug, label, and relationship fields. */
rootPages?: {
slug?: string
label?: LabelFunction | StaticLabel
fields?: Record<string, RootPageFieldOptions>
}
/**
* When `true`, only queue `update-urls`; when omitted or `false`, run the job immediately after
* queue (default).
*/
delayJobsRun?: boolean
}UrlCollectionOptions
interface UrlCollectionOptions {
/** Use breadcrumb trail for path (typical for page trees). */
breadcrumbs?: boolean
/** How category segments appear in the path. */
prefixStrategy?: UrlPrefixStrategy
/** Key in `rootPages.fields` whose page defines this collection's URL prefix. */
rootPage?: string
category?: {
collection: string
field?: string
}
routeCollection?: string
}RootPageFieldOptions
interface RootPageFieldOptions {
/** Marks the field whose target is the locale root `/`. */
isHomepage?: boolean
relationTo?: string
overrides?: Partial<Field>
}UrlDoc, RootPagesDoc, Breadcrumb
interface Breadcrumb {
url?: string | null
}
interface UrlDoc {
id?: string | number | null
slug?: string | null
populatedUrl?: string | null
breadcrumbs?: Breadcrumb[] | null
categories?: unknown
_status?: "draft" | "published" | null
[key: string]: unknown
}
interface RootPagesDoc {
id?: string | null
[field: string]: string | UrlDoc | null | undefined
}UrlPrefixStrategy
type UrlPrefixStrategy = "category" | "rootPage"UrlUpdateSource
type UrlUpdateSource =
| {
collection: string
id?: string | null
type: "category" | "collection" | "page"
}
| {
current: RootPagesDoc
previous?: RootPagesDoc
type: "rootPages"
}Aliases from Payload
PayloadPlugin, PayloadPluginConfig, CollectionConfigLike, GlobalConfigLike, FieldLike,
WorkflowLike, PayloadLike, Hook, and HookArgs match the names exported from this package and
follow Payload’s definitions. Locale is string.
