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

@jhb.software/payload-pages-plugin

v0.8.0

Published

Payload CMS plugin that adds essential fields for hierarchical page structure to collections.

Readme

JHB Software - Payload Pages Plugin

NPM Version

The Payload Pages plugin simplifies website building by adding essential fields to your collections. These fields enable hierarchical page structures and dynamic URL management.

Setup

First, add the plugin to your payload config. The generatePageURL function is required and must provide a function that returns the full URL to the frontend page.

import { payloadPagesPlugin } from '@jhb.software/payload-pages-plugin'

// Add to plugins array
plugins: [
  payloadPagesPlugin({
    // Example generatePageURL function:
    generatePageURL: ({ path, preview }) =>
      path && process.env.NEXT_PUBLIC_FRONTEND_URL
        ? `${process.env.NEXT_PUBLIC_FRONTEND_URL}${preview ? '/preview' : ''}${path}`
        : null,
  }),
]

Next, create a page collections using the PageCollectionConfig type. This type extends Payload's CollectionConfig type with a page field that contains configurations for the page collection. The page field must be specified as follows:

  • parent.collection: The slug of the collection that will be used as the parent of the current collection.
  • parent.name: The name of the field on the parent collection that will be used to relate to the current collection.
  • isRootCollection: Whether the collection is the root collection (collection which contains the root page). If true, the parent field is optional. Defaults to false.
  • parent.sharedDocument (optional, defaults to false): If true, the parent document will be shared between all documents in the collection.
  • breadcrumbs.labelField (optional, defaults to admin.useAsTitle): The name of the field that will be used to label the document in the breadcrumb.
  • slug.fallbackField (optional, defaults to title): The name of the field that will be used as the fallback for the slug.

Here is an example of the page collection config of the root collection:

import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin'

const Pages: PageCollectionConfig = {
  slug: 'pages',
  admin: {
    useAsTitle: 'title',
  },
  page: {
    parent: {
      collection: 'pages',
      name: 'parent',
    },
    isRootCollection: true,
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    // other fields
  ],
}

Then additional collections can be created. Documents in these collections will be nested under documents in the root collection.

import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin'

const Posts: PageCollectionConfig = {
  slug: 'posts',
  page: {
    parent: {
      collection: 'pages',
      name: 'parent',
      sharedDocument: true,
    },
  },
  fields: [
    // your fields
  ],
}

The plugin also includes a RedirectsCollectionConfig type that can be used to create a redirects collection. This type extends Payload's CollectionConfig type with a redirects field that contains configurations for the redirects collection.

import { RedirectsCollectionConfig } from '@jhb.software/payload-pages-plugin'

const Redirects: RedirectsCollectionConfig = {
  slug: 'redirects',
  admin: {
    defaultColumns: ['sourcePath', 'destinationPath', 'permanent', 'createdAt'],
    listSearchableFields: ['sourcePath', 'destinationPath'],
  },
  redirects: {},
  fields: [
    // the fields are added by the plugin automatically
  ],
}

SEO Plugin Integration

To integrate with the official Payload SEO plugin, store the generatePageURL function you defined for the pages plugin in a variable outside of the Payload config and pass it to the generateURL option of the SEO plugin. If your collections are localized, also add the alternatePathsField which is exported by the plugin to the fields option of the SEO plugin.

import { alternatePathsField, payloadPagesPlugin } from '@jhb.software/payload-pages-plugin'
import { seoPlugin } from '@payloadcms/plugin-seo'

// Example generatePageURL function:
const generatePageURL = ({
  path,
  preview,
}: {
  path: string | null
  preview: boolean
}): string | null => {
  return path && process.env.NEXT_PUBLIC_FRONTEND_URL
    ? `${process.env.NEXT_PUBLIC_FRONTEND_URL}${preview ? '/preview' : ''}${path}`
    : null
}

export default buildConfig({
  // ...
  plugins: [
    payloadPagesPlugin({
      generatePageURL,
    }),
    seoPlugin({
      generateURL: ({ doc }) => generatePageURL({ path: doc.path, preview: false }),
      // If your collections are localized, also add the alternatePathsField
      fields: ({ defaultFields }) => [...defaultFields, alternatePathsField()],
    }),
  ],
})

Multi-tenant support

⚠️ Warning: The multi-tenant support is currently experimental and may change in the future.

The plugin supports multi-tenant setups via the official Multi-tenant plugin.

By default the plugin adds a unique constraint to the slug field of all page collections. In a multi-tenant setup you can disable this constraint by setting the unique field to false in the page collection config. To ensure uniqueness for a tenant to now have pages with multiple slugs, you can create a compound unique index.

Example:

export const Pages: PageCollectionConfig = {
  slug: 'pages',
  page: {
    slug: {
      // Disable the slug uniqueness because of the multi-tenant setup (see indexes below)
      unique: false,
    },
  },
  indexes: [
    {
      fields: ['slug', 'tenant'],
      unique: true,
    },
  ],
  fields: [
    /* your fields */
  ],
}

Some features (e.g. the parent and isRootPage fields) internally fetch documents from the database. To ensure only documents from the current tenant are fetched, you need to pass the baseFilter function to the plugin config. It receives the current request object and should return a Where object which will be added to the query. For the validation of the redirects, you need to pass the redirectValidationFilter function to the plugin config. It receives the current request object and the document object and should return a Where object which will be added to the query.

To generate the URL based on the tenant the page belongs to, pass an async function to the generatePageURL option of the plugin config. It receives the current request object and document data so you could for example fetch the tenant from the database and use its website URL.

Example:

import { payloadPagesPlugin } from '@jhb.software/payload-pages-plugin'
import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities'

export default buildConfig({
  // ...
  plugins: [
    payloadPagesPlugin({
      generatePageURL: async ({ path, preview, data, req }) => {
        if (data.tenant && typeof data.tenant === 'string') {
          const tenant = await req.payload.findByID({
            collection: 'tenants',
            id: data.tenant,
            select: {
              websiteUrl: true,
            },
            req,
          })

          if (tenant && 'websiteUrl' in tenant && tenant.websiteUrl) {
            return `${tenant.websiteUrl}${preview ? '/preview' : ''}${path}`
          }
        }

        return null
      },
      baseFilter: ({ req }) => {
        const tenant = getTenantFromCookie(req.headers, req.payload.db.defaultIDType)

        return { tenant: { equals: tenant } }
      },
      redirectValidationFilter: ({ doc }) => {
        return { tenant: { equals: doc.tenant } }
      },
    }),
  ],
})

Parent Deletion Prevention

The plugin automatically prevents the deletion of parent documents that are referenced by child documents, protecting your data integrity and preventing orphaned references. This feature is enabled by default but can be disabled by setting the preventParentDeletion plugin config option to false if needed.

Resolving Deletion Conflicts

To delete a parent document that has child references, you have two options:

  1. Reassign child documents: Update the child documents to reference a different parent
  2. Remove child documents: Delete the child documents first, then delete the parent

Payload Select API

When using the Payload Select API, the plugin automatically extends the selection to include all virtual fields if any of them are selected. This ensures that virtual fields can be generated correctly. For example, when querying for a page and selecting only the path field, the plugin will also select the slug, parent and title fields as theses fields are required to generate the virtual path field.

Therefore it is highly recommended to specify the defaultPopulate property on all of your page collections.

About this plugin

This plugin streamlines website development with Payload CMS by providing enhanced document nesting capabilities. While the official Nested Docs plugin only supports nesting within a single collection, this plugin enables nesting documents across multiple collections. Another major difference is that this plugin uses virtual fields for the paths and breadcrumbs, ensuring these computed values stay automatically synchronized with your content structure.

Roadmap

⚠️ Warning: This plugin is actively evolving and may undergo significant changes. While it is functional, please thoroughly test before using in production environments.

Have a suggestion for the plugin? Any feedback is welcome!

Contributing

We welcome contributions! Please open an issue to report bugs or suggest improvements, or submit a pull request with your changes.