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

@justanarthur/payload-plugin-seo

v4.1.0

Published

SEO plugin for Payload CMS — adds meta title, description, image and preview fields.

Readme

@justanarthur/payload-plugin-seo

SEO plugin for Payload CMS — adds a meta group field (title, description, keywords, image, Open Graph, Twitter Card, robots, noindex) and a /plugin-seo/generate HTTP endpoint for AI-assisted meta generation.

Install

pnpm add @justanarthur/payload-plugin-seo

Quick start

// payload.config.ts
import { seoPlugin } from '@justanarthur/payload-plugin-seo'

export default buildConfig({
  plugins: [
    seoPlugin({
      collections: ['pages', 'posts'],
      uploadsCollection: 'media',
      generateSEO: async ({ doc }) => {
        // call your LLM of choice; return any subset of the SEOMeta shape
        return { title: doc.title, description: doc.excerpt }
      }
    })
  ]
})

The plugin adds a meta group field to every collection in collections. Editors can click Generate in the meta panel to invoke generateSEO (or the OpenAI fallback if openaiApiKey is set).

Auto-generate meta on save

Set autoGenerate on the plugin to fill empty meta fields automatically — no editor click required.

seoPlugin({
  collections: ['pages'],
  autoGenerate: {
    mode: 'onCreateOrUpdate',           // fires on every save
    onlyFillEmpty: true,                // never overwrite editor-set values
    deriveFrom: 'allScalars'            // heuristic: read every scalar field
  }
})

mode

| Value | Behavior | |---|---| | 'onCreateOrUpdate' (default) | Hook fires on every save — create and update. Editor-set meta is preserved because onlyFillEmpty: true is the default. | | 'onCreate' | Only the first save of a new doc. Subsequent saves never auto-fill. | | 'onUpdate' | Only subsequent saves (not the first). Useful for refreshing meta when source content changes. | | 'off' | Hook installed but does nothing. Same as omitting the option. |

deriveFrom

Two shapes:

// (default) — heuristic: extract every scalar from the doc, apply name-based
// mapping rules. Field named `title` → meta.title, `description`/`excerpt`/
// `summary` → meta.description, image-like relation → meta.image, etc.
deriveFrom: 'allScalars'

// Explicit per-slot source field map. Read `data[sourceField]` verbatim
// and copy to the named meta slot. Useful when the collection has many
// text fields and the heuristic would pick wrong.
deriveFrom: {
  title: 'pageTitle',
  description: 'summary',
  image: 'heroImage',
  keywords: 'tags'
}

onlyFillEmpty

Default true. Only meta fields that are currently empty get filled — the editor's existing values are preserved across saves. Set to false to overwrite editor-set meta on every save (rarely what you want).

timeoutMs

Default 8000. Hard cap on the LLM generator pass when generateSEO or openaiApiKey is set. On timeout, the heuristic result is kept and the save proceeds.

What gets filled automatically

In 'allScalars' mode the heuristic maps:

  • meta.title ← a field named title (or first non-empty text field)
  • meta.description ← a field named description / excerpt / summary (or first 155 chars of any richText body, or first 155 chars of any text scalar)
  • meta.image ← an upload / relationship field whose name contains image / cover / hero (or the first image-like relation in the doc)
  • meta.keywords ← a field named keywords, or top 8 unique tokens from the union of all text in the doc (stopword-filtered)

After the heuristic pass, meta.title is mirrored to ogTitle and twitterTitle, meta.description to ogDescription and twitterDescription, meta.image to ogImage and twitterImage — but only into empty secondary slots.

The pass then calls generateSEO / OpenAI (if configured) to fill any remaining empty slots. Generator failures or timeouts never block the save.

Options reference

| Option | Type | Default | Notes | |---|---|---|---| | collections | CollectionSlug[] | — | Collections to add the meta group to. | | globals | GlobalSlug[] | — | Globals to add the meta group to. | | fields | (args) => Field[] | — | Override the default meta field. | | generateSEO | GenerateSEO | — | User-supplied generation function. | | openaiApiKey | string | — | OpenAI key for the built-in fallback. | | tabbedUI | boolean | false | Group fields into Content / SEO tabs. | | uploadsCollection | CollectionSlug | — | Slug for image-relation fields. | | interfaceName | string | — | TS interface name for the meta group. | | autoGenerate | AutoGenerateConfig \| false | undefined | Auto-fill empty meta on save. See above. |

Behaviour notes

  • The plugin only writes into empty secondary slots (ogTitle, twitterTitle, etc.) — your editor-set values are never overwritten.
  • The built-in OpenAI fallback (when only openaiApiKey is set) uses gpt-4o-mini and runs on every save where the heuristic pass leaves a slot empty.
  • Generator failures / timeouts never block the save — the heuristic result stands and the doc publishes normally.
  • tabbedUI is purely cosmetic; it splits the editor screen into Content / SEO tabs without touching the stored shape.

Subpath imports

Advanced — for hosts that want individual pieces without the plugin runtime:

| Subpath | What's there | |---|---| | @justanarthur/payload-plugin-seo (root) | seoPlugin | | @justanarthur/payload-plugin-seo/types | shared TS types (SEOPluginConfig, SEOMeta, etc.) | | @justanarthur/payload-plugin-seo/fields | metaField factory, used when you want the field without registering the plugin | | @justanarthur/payload-plugin-seo/fields-components | the client-side admin UI components | | @justanarthur/payload-plugin-seo/globals | createMetadataGlobal (the SEO metadata global the lib's createWWWConfig registers) | | @justanarthur/payload-plugin-seo/site-defaults | createSiteDefaults — read the SEO global for the current request | | @justanarthur/payload-plugin-seo/next-metadata | generateMeta (used by @justanarthur/payload-www/render-pages) | | @justanarthur/payload-plugin-seo/root-jsonld | Organization / WebSite JSON-LD helpers | | @justanarthur/payload-plugin-seo/opengraph-image | generateOpenGraphImage (Next.js ImageResponse helper) | | @justanarthur/payload-plugin-seo/client | client-side bits (already lazy-loaded by @justanarthur/payload-www) |

Licence

MIT