npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

payload-plugin-urls

v0.9.5

Published

Payload CMS plugin for URL utilities.

Downloads

498

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 hidden populatedUrl field or pass field-level overrides (for example admin UI tweaks).
  • locales — Optional. When omitted, locales are taken from Payload localization. Set defaultLocale and locales explicitly 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; see PayloadPluginUrlsOptions. 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 for name / overrides under field; 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 related UrlDoc (expanded).

    GetDocumentLinkArgs:

    interface GetDocumentLinkArgs {
      relationTo: string
      value: string | UrlDoc
    }
    • relationTo — Target collection slug.
    • value — Related document; must be an object with populatedUrl / category / slug data, not an unresolved id string.
  • context — Locale, plugin options, optional public baseUrl, and optional urlPrefixStrategy override 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 with URL and usePathname for same-origin and active-route checks.
  • urlslocale, options (your plugin config), optional baseUrl, optional urlPrefixStrategy (same meanings as getDocumentLink).

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; collection is 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 normalized locales.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, or undefined on first save.
  • options — Plugin options determining homepage field and rootPage mappings.

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.