Surfaces related posts or articles for Payload collections using deterministic similarity algorithms instead of an external AI service. It reads keywords from the Payload search plugin collection or a custom source adapter. It adds an admin sidebar panel, a REST endpoint, a server getRelated function and a React hook. Requires Payload 3.
Related items plugin for Payload CMS
Installation
npm i payload-plugin-related-items3
Stars
0
Forks
2
Open Issues
2 months ago
Last Updated
24
Weekly Downloads
v1.0.5
npm Version
423.8 KB
Unpacked Size
0
Dependencies
Trends appear after 7 days of data. Collecting since Oct 22 — 1 daily snapshot so far.
Trends appear after 7 days of data. Collecting since Oct 22 — 1 daily snapshot so far.
Each point is npm's rolling 7-day download count.
Health Score
Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.
Badge for your README
[](https://payloaddirectory.dev/plugins/1220131707-root)<a href="https://payloaddirectory.dev/plugins/1220131707-root"><img src="https://payloaddirectory.dev/badge/1220131707-root.svg" alt="Payload Directory"></a>README
A Payload CMS plugin that surfaces related content using classical, transparent similarity algorithms — no external AI service required.
Built for editorial sites, docs, knowledge bases, and any Payload project that wants "Related posts", "You might also like…", or "More like this" sections backed by predictable math instead of a black box.
adminField.scorer default (compare algorithms without touching collection config).getRelated() server API, and a headless
useRelatedItems React hook.@payloadcms/plugin-search
out of the box, or any custom data source via the SourceAdapter
interface.pnpm add payload-plugin-related-items
# or
npm install payload-plugin-related-items
^18.20.2 || >=20.9.0pnpm ^9 || ^10 (repo uses pnpm@10)^3.0.0 (peer dependency)payload (^3.0.0)@payloadcms/ui (^3.0.0) for admin sidebar integrationreact (^18 || ^19) and react-dom (^18 || ^19) for the client hook package (payload-plugin-related-items)If you only use the server API (getRelated) and REST endpoint, optional UI peers are not required.
The simplest setup pairs this plugin with @payloadcms/plugin-search, which
handles keyword extraction and gives you one shared index to query.
// payload.config.ts
import { buildConfig } from 'payload'
import { searchPlugin } from '@payloadcms/plugin-search'
import { extractKeywords, payloadRelatedItems } from 'payload-plugin-related-items'
export default buildConfig({
collections: [
/* posts, articles, ... */
],
plugins: [
searchPlugin({
collections: ['posts', 'articles'],
searchOverrides: {
fields: ({ defaultFields }) => [
...defaultFields,
{ name: 'keywords', type: 'text', hasMany: true },
],
},
beforeSync: ({ originalDoc, searchDoc }) => ({
...searchDoc,
keywords: extractKeywords(
[originalDoc.title, originalDoc.excerpt, originalDoc.body].filter(Boolean).join(' '),
),
}),
}),
payloadRelatedItems({
collections: {
posts: {
fields: [{ name: 'keywords', weight: 1 }],
recency: { field: 'publishedAt', halfLifeDays: 60 },
},
articles: {
fields: [{ name: 'keywords', weight: 1 }],
},
},
}),
],
})
This package is authored with Payload's definePlugin API (recommended for
published plugins) and preserves the standard consumer usage:
plugins: [
payloadRelatedItems({
collections: { posts: true },
}),
]
For type-aware plugin registration, the package also augments
payload's RegisteredPlugins map for 'payload-rplugin-elated-items', so users get
typed plugin options when importing the package in TypeScript projects.
That's it. Each configured collection now exposes:
GET /api/related/:collection/:id?limit=5 — JSON response.getRelated({ payload, collection, id }) in server code.useRelatedItems({ collection, id }) in client components (from
payload-plugin-related-items).For Next.js App Router (or any RSC setup), call getRelated() in a server
component and wrap it in <Suspense> so the page shell can stream while
related items resolve.
// components/RelatedItemsWidgetBlock.tsx
import { getPayload } from 'payload'
import config from '@payload-config'
import { getRelated } from 'payload-plugin-related-items'
export default async function RelatedItemsWidgetBlock({
docId,
collection,
headline = 'Polecamy',
limit = 4,
}: {
docId: string | number
collection: string
headline?: string
limit?: number
}) {
const payload = await getPayload({ config })
const items = await getRelated({ payload, collection, id: docId, limit, populate: 1 })
if (!items.length) return null
return (
<section>
<h2>{headline}</h2>
<ul>
{items.map((item) => (
<li key={`${item.collection}:${item.id}`}>
<a href={`/${item.collection}/${item.doc?.slug ?? item.id}`}>
{item.doc?.title ?? item.id}
</a>
</li>
))}
</ul>
</section>
)
}
<Suspense fallback={<div className="h-64 animate-pulse bg-gray-100 rounded-lg" />}>
<RelatedItemsWidgetBlock docId={article.id} collection="articles" limit={4} />
</Suspense>
For client-side fetching (tabs, infinite scroll, scorer toggles), use
useRelatedItems instead. See
API → Populating original docs for
populate / depth options.
| Topic | What's in there |
|---|---|
| Configuration | Full options reference: source, collections, cache, recency, disabling. |
| Scorers | Algorithm comparison + the three layers where you can set the scorer. |
| API | getRelated(), REST endpoint, headless React hook, result shape. |
| Precomputation | Sidecar collection, incremental sync, full rebuilds. |
| Word cloud | On-demand admin keyword cloud + REST endpoint. |
| Source adapter | How keyword storage works + plugging in a non-search-plugin source. |
| Development | Project layout, scripts, releasing, adding a scorer. |
Common repo scripts from package.json:
pnpm dev - runs the local Next.js dev app under dev/pnpm dev:payload - runs Payload CLI with dev/payload.config.tspnpm dev:generate-types - regenerates Payload types for the dev apppnpm dev:generate-importmap - regenerates the Payload import mappnpm build - copies assets and builds JS + declaration files to dist/pnpm lint - runs ESLintpnpm test - runs integration tests (vitest) and e2e tests (playwright)@payloadcms/plugin-search?No, but it's the path of least resistance. The plugin reads from any source
via a SourceAdapter. The default adapter targets the search plugin's
collection because (a) it already gives you a polymorphic relationship back
to the originating doc, and (b) it's where most Payload projects already
extract keywords. See Source adapter to roll
your own.
Once, on the search-plugin collection — not duplicated on each content
collection. Your searchPlugin({ beforeSync }) decides what goes in. See
Source adapter.
bm25 is a strong default. It down-weights generic words via IDF and
length-normalizes, which matters once you have more than a few hundred
documents. For very short keyword lists try dice; for sparse, tidy sets
jaccard is fine. Full guidance in
Scorers.
Yes — that's a first-class concern. Set adminField.scorer as the initial
scorer for the widget (collection default vs BM25 vs Dice, …).
Editors can change it per session via the scorer dropdown in the sidebar.
See Where to set the scorer.
All blended scores are in [0, 1] — higher means more related.
Exact BM25 keyword overlap normalizes to 1 for that query; partial overlaps are below 1.
Recency decay (when configured) can multiply the blended score before filtering.
See Scorers.
With the default search-plugin adapter, the plugin selects common display fields
(title, name, slug, description) onto each source row so RelatedItem.source
can render readable labels without calling populate. For populated originals use
getRelated({ populate: true }) or ?populate=true as usual.
See API.
The LRU cache is per-process. Source-collection writes invalidate the
local cache instantly via afterChange / afterDelete hooks. For
multi-instance deployments where strong cross-process freshness matters,
lower cache.ttlSeconds or disable the cache. For larger corpora, prefer
the precomputed sidecar instead.
With precompute.incremental: true (default when precompute is enabled),
the sidecar is updated on every source-collection write. Run
rebuildRelatedIndex({ payload }) periodically if you want a belt-and-suspenders
guarantee.
Yes — the plugin only uses Payload's collection APIs (find, findByID,
update, create, delete). Whatever Payload supports, this supports.
Reads honour the requesting user's session via PayloadRequest, so
collection-level access control applies. For drafts/locales specifically,
pass an explicit filter in the collection config (e.g. { status: { equals: 'published' } }), or call getRelated({ filter, req }) per call.
Pass populate: true (or { depth: 1 } for relationship resolution) to
getRelated(), the REST endpoint (?populate=true), or the
useRelatedItems hook. Each result then carries the full originating
document as doc, batched server-side to avoid N+1 fetches. See
API → Populating original docs.
An admin-only helper that aggregates keyword frequencies across your source
collection rows on demand and renders them sized by frequency. The renderer
is code-split (React.lazy), so nothing is bundled or computed until an
editor clicks Compute word cloud. See Word cloud
for scale notes, configuration, and the REST endpoint that backs it.
Yes — adminField: false (or adminField: { enabled: false }) keeps the
REST endpoint, hooks, and getRelated() working without injecting any
field into the admin.
No. The default path is in-memory + cache. Precomputation runs inline in the
source collection's hooks (incremental) or as an explicit
rebuildRelatedIndex() call you can schedule any way you like (cron, queue
job, onInit, etc.). No new runtime to operate.
MIT.
This plugin was tested and developed in relation to my upcoming fairytale builder app.

search
Generate fast-to-query search records from your Payload documents
10083.1k/wk45.4kv3
payload typesense
Typesense search for Payload with automatic sync, search endpoints and a React search input
83235/wk32v3
algolia search
Algolia search sync for Payload with draft-aware indexing, automatic text extraction and admin reindex buttons
562/wk15v3
payload plugin algolia
Sync Payload collections to an Algolia search index
5418/wk17v3
menu plugin
Menu management for Payload with nested dropdowns, mega menus, locale support and a public menu endpoint
5156/wk6v3
payload algolia search
Sync Payload collections to Algolia with search and re-index endpoints plus optional result enrichment
39161/wk1v3