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

payloadcms-seo-cli

v0.1.0

Published

A PayloadCMS Content SEO CLI Tool for AI LLM Agents to do SEO

Readme

payloadcms-seo-cli

A Node.js CLI for AI coding agents that work with Payload CMS content. It pulls documents to pretty-printed JSON files, runs SEO gates (keywords, copy-paste duplicates, title and description lengths), and creates or updates documents over REST.

Sponsor

Brainy Builds

payloadcms-seo-cli is sponsored by Brainy Builds. Thank you for supporting the project!

Install

npm install -D payloadcms-seo-cli

The package ships a payloadcms-seo-cli bin and a programmatic API. It requires Node.js >=20.

Rule / Skill auto-install for AI agents

npm install -D payloadcms-seo-cli runs a postinstall hook that installs agent guidance into the consumer project:

  • .cursor/rules/payloadcms-seo-cli.mdc - Cursor rule
  • .claude/skills/payloadcms-seo-cli/SKILL.md - Claude Code skill
  • AGENTS.md - appended between <!-- payloadcms-seo-cli:start --> / <!-- payloadcms-seo-cli:end --> sentinels for Codex, Gemini CLI, and other Agent Skills-compatible tools

To skip auto-install:

npm install -D payloadcms-seo-cli --ignore-scripts

(npm install --ignore-scripts skips lifecycle scripts for the whole install.)

To re-run setup manually:

npx payloadcms-seo-cli setup [flags]

Flags: --cursor, --claude, --agents (select targets; default: all), --force (overwrite even if differs), --silent / --yes (never prompt, for CI / postinstall), --dry-run (print what would be written, write nothing).

Supported IDEs and agents: Cursor, Claude Code, Codex, Gemini CLI, GitHub Copilot, OpenCode, and any tool that follows the Agent Skills open standard (https://agentskills.io/).

Commit the generated .cursor/, .claude/, and AGENTS.md files so the whole team shares the same agent guidance.

setup is a meta-command. It does not load payloadcms-seo-cli.json and does not scaffold that config. It is not one of the SEO commands (pull, keywords, duplicates, meta, check, push, create).

Quick start

  1. Copy payloadcms-seo-cli.example.json to payloadcms-seo-cli.json in the project root. Replace example slugs with your Payload collection and global slugs.
  2. Set Payload REST credentials in .env (see Environment):
PAYLOAD_API_URL=https://localhost:3000/api
PAYLOAD_API_KEY=
  1. Inspect a command:
npx payloadcms-seo-cli pull --help
  1. Pull configured resources, edit the JSON files, run gates, then write back:
npx payloadcms-seo-cli pull --all
npx payloadcms-seo-cli check --files "./content/**/*.json"
npx payloadcms-seo-cli push --files "./content/posts/*.json"

Default config file: payloadcms-seo-cli.json (--config overrides). JSON indent default: tab. Pull depth default: 0.

On disk:

  • Collections: {outputDir}/{collectionSlug}/{id}.json
  • Globals: {outputDir}/globals/{globalSlug}.json

Configuration

Every top-level key of SeoCliConfig. Full generic example: payloadcms-seo-cli.example.json. Do not put API keys in the JSON file.

{
	"payload": {
		"apiUrlEnv": "PAYLOAD_API_URL",
		"apiKeyEnv": "PAYLOAD_API_KEY",
		"authCollection": "users",
		"allowInsecureTls": false,
		"depth": 0,
		"draft": true
	},
	"outputDir": "./content",
	"json": {
		"indent": "tab"
	},
	"fieldsIgnore": [
		"id",
		"createdAt",
		"updatedAt",
		"_status",
		"globalType"
	],
	"keywords": {
		"source": "./keywords.csv",
		"csvPhraseColumn": "phrase",
		"failOnMissing": false
	},
	"duplicates": {
		"minLines": 5,
		"minTokens": 50,
		"threshold": null,
		"mode": "mild"
	},
	"meta": {
		"fields": [
			{
				"paths": ["title", "seo.title", "meta.title"],
				"min": 50,
				"max": 60,
				"required": false
			},
			{
				"paths": ["description", "excerpt", "seo.description", "meta.description"],
				"min": 150,
				"max": 160,
				"required": false
			}
		]
	},
	"resources": {
		"posts": {
			"kind": "collection",
			"pull": true,
			"create": true,
			"push": true,
			"onPush": "published"
		},
		"pages": {
			"kind": "collection",
			"pull": true,
			"create": false,
			"push": true,
			"onPush": "draft"
		},
		"media": {
			"kind": "collection",
			"pull": true,
			"create": false,
			"push": false,
			"onPush": "draft"
		},
		"header": {
			"kind": "global",
			"pull": true,
			"create": false,
			"push": true,
			"onPush": "draft"
		},
		"footer": {
			"kind": "global",
			"pull": true,
			"create": false,
			"push": true,
			"onPush": "draft"
		}
	}
}

| Key | Meaning | |---|---| | payload | REST client: env var names for URL and API key, auth collection slug, TLS, default depth (0) and draft. | | outputDir | Root directory for pulled JSON files. | | json.indent | Pretty-print indent: "tab" (default) or 0, 2, 4. | | fieldsIgnore | Field names skipped by keyword and duplicate scans (id, timestamps, _status, globalType in the example). A resource may add its own fieldsIgnore. | | keywords | Phrase list path. CSV uses csvPhraseColumn (example: phrase). JSON is a list of strings. failOnMissing fails the keywords gate when a phrase is absent. | | duplicates | jscpd clone detection: minLines, minTokens, optional threshold (fail when duplication percent is above this value), mode (mild / weak / strict). | | meta | Title and description length ranges. Defaults match common SERP limits: title 50-60, description 150-160. A resource may override meta. | | resources | Map of Payload slugs. Each entry needs kind (collection or global), pull, create, push, and onPush (draft or published). Missing slugs are refused. create: true is valid on collections only. |

Example resources above: posts (published writes), pages / header / footer (draft writes), media (pull only).

Commands

npx payloadcms-seo-cli <command> [options]

Global options: --help / -h, --version / -v, --config <path> (default: payloadcms-seo-cli.json).

| Command | Purpose | |---|---| | pull | Download Payload documents as pretty-printed JSON files. | | keywords | Scan JSON files for required keyword phrases. | | duplicates | Scan JSON string values for copy-paste clones. | | meta | Check title and description lengths against configured ranges. | | check | Run keywords, duplicates, and meta gates together. | | push | Update existing Payload documents from JSON files. | | create | Create new Payload documents from JSON files. |

setup installs agent rule and skill files. It is not a SEO command. See Rule / Skill auto-install.

Selecting files

The scan commands (keywords, duplicates, meta, check) and the write commands (push, create) read JSON from local files. REST is not involved in selecting them. Three forms exist:

| Command | Positional paths | --file | --files | |---|---|---|---| | keywords, duplicates, meta | yes | no | yes (repeatable) | | check | yes | yes | yes (repeatable) | | push | no | yes | yes (repeatable) | | create | no | yes (required) | no |

  • Positional paths are plain file paths passed after the command name, for example check ./content/posts/abc123.json.
  • --file <path> selects a single JSON file. check, push, and create accept it; create requires it.
  • --files <glob> adds every JSON file matching a glob and is repeatable, for example --files "./content/posts/*.json" --files "./content/globals/*.json". On the scan commands, positional paths and --files globs combine into one file set.

Glob expansion happens inside the CLI, so quote globs (for example "./content/posts/*.json") and the shell passes the pattern through untouched.

# positional path
npx payloadcms-seo-cli check ./content/posts/abc123.json

# single file via --file
npx payloadcms-seo-cli check --file ./content/globals/header.json

# repeatable globs
npx payloadcms-seo-cli check --files "./content/posts/*.json" --files "./content/globals/*.json"

# push uses the same forms
npx payloadcms-seo-cli push --files "./content/posts/*.json"
npx payloadcms-seo-cli push --file ./content/globals/header.json

push reads the same JSON input model: pass --file for one document or --files for many. create always needs exactly one file: --file <path> plus --slug <slug>.

pull

npx payloadcms-seo-cli pull --all
npx payloadcms-seo-cli pull --slug posts --slug pages
npx payloadcms-seo-cli pull --slug posts --id <id>
npx payloadcms-seo-cli pull --slug posts --depth 0 --draft true --output-dir ./content --indent tab

| Flag | Value | Meaning | |---|---|---| | --slug | <slug> | Collection or global slug (repeatable for pull). | | --all | | Pull every resource listed in the config file. | | --id | <id> | Limit pull to one collection document id. | | --depth | <n> | Payload REST depth query override. | | --draft | true\|false | Whether to request draft documents. | | --output-dir | <dir> | Directory for written JSON files. | | --indent | tab\|0\|2\|4 | Pretty-print indent for written JSON files. |

keywords, duplicates, meta, check

Scan local JSON. REST is not used. Files come from positional paths, --file (check only), and repeatable --files globs - see Selecting files.

npx payloadcms-seo-cli keywords ./content/posts/abc123.json --source csv
npx payloadcms-seo-cli keywords --files "./content/posts/*.json" --keywords-source ./topics/ecommerce.csv
npx payloadcms-seo-cli check --files "./content/posts/*.json" --keywords-source ./topics/ai-tools.json
npx payloadcms-seo-cli duplicates --files "./content/**/*.json" --min-lines 5 --min-tokens 50 --mode mild
npx payloadcms-seo-cli meta --files "./content/**/*.json"
npx payloadcms-seo-cli check --file ./content/posts/abc123.json

Shared scan flags:

| Flag | Value | Meaning | |---|---|---| | --files | <glob> | JSON file glob (repeatable). | | --file | <file> | Single JSON file (check only). | | --format | bash\|json | Report format. | | --output | <file> | Write the JSON report to this file. | | --keywords-source | <path> | Keyword list file path override for this run (keywords, check). |

The reports and gates for each scan are documented below: Keyword presence, Duplicates report, and Meta report. check runs all three scans on the same file set and exits 2 when any of their gates fails.

Keyword presence

keywords scans the target files for phrases loaded from keywords.source in the config: a CSV file (with csvPhraseColumn naming the column that holds the phrases) or a JSON file that is a list of strings. --source csv|json overrides the list file format for the run. --keywords-source <path> overrides the phrase file path for the run and is accepted by check too; the config file is left untouched.

The report has two parts:

  • hits - every phrase found in the scanned files. Each hit reports the phrase, the file, the JSON line number where the phrase occurs, the dot-separated JSON field path (title, seo.description, and so on), and the count of matches.
  • absent - the phrases with zero hits in the scanned files.

In bash output the hits are grouped by phrase. Each group is labeled Phrase: (the quoted phrase), Count: (the number of locations where the phrase appears), and Occurrences (file:line, path, count): - one indented line per location with the file, the JSON line number, the dot-separated field path, and the number of matches there. Absent phrases are listed under Absent: with the same Phrase: label:

Hits:
Phrase: "web"
Count: 3
Occurrences (file:line, path, count):
    content/posts/abc123.json:3 title x1
    content/posts/abc123.json:4 slug x1
    content/posts/abc123.json:6 seo.description x2
Absent:
Phrase: "free portfolio website"

Scanning one file shows presence for that file. Scanning many files shows presence per file and across the whole file set. The gate fails (exit 2) when keywords.failOnMissing is true and any phrase is absent.

# one file, positional path
npx payloadcms-seo-cli keywords ./content/posts/abc123.json

# many files - presence per file and across files
npx payloadcms-seo-cli keywords --files "./content/posts/*.json"

# JSON list of phrases instead of CSV
npx payloadcms-seo-cli keywords --files "./content/posts/*.json" --source json

# CSV topic file instead of the config source
npx payloadcms-seo-cli keywords --files "./content/posts/*.json" --keywords-source ./topics/ecommerce.csv

# JSON topic file
npx payloadcms-seo-cli keywords --files "./content/posts/*.json" --keywords-source ./topics/ai-tools.json

# machine-readable report written to a file
npx payloadcms-seo-cli keywords --files "./content/posts/*.json" --format json --output ./keywords-report.json

--output ./keywords-report.json writes the JSON report to that file and, when --format is omitted, also prints JSON to stdout. The report shape (a small sample):

{
	"hits": [
		{
			"phrase": "custom web development",
			"file": "./content/posts/abc123.json",
			"line": 12,
			"path": "title",
			"count": 1
		}
	],
	"absent": [
		"payload cms",
		"web design"
	]
}

Use hits to see which phrases each file already contains, and absent to find the phrases that still need to be worked into the scanned files.

Duplicates report

duplicates scans JSON string values for copy-paste clones. In bash output the clones are grouped by the duplicated phrase. Each group is labeled: Phrase: shows the quoted duplicated text, Count: shows how many times it repeats, and Occurrences (file:lines): lists one line per occurrence with the file and the JSON start and end line range:

Clones:
Phrase: "Professional web design services in Malta"
Count: 2
Occurrences (file:lines):
    content/posts/a.json:4-5
    content/posts/b.json:4-5
Duplication:
12.5%

The JSON report keeps the clone pairs; each side carries the file, the JSON start and end line range, the dot-separated field path, and the snippet field with the duplicated text (truncated with ... when long). The report also carries the duplicationPercent and the applied threshold. The gate fails (exit 2) when duplicationPercent is above threshold; a threshold of null in the config disables the gate.

Config defaults come from duplicates.minLines, duplicates.minTokens, duplicates.threshold, and duplicates.mode. The flags below override them for the run.

| Flag | Value | Meaning | |---|---|---| | --min-lines | <n> | Minimum clone length in lines. | | --min-tokens | <n> | Minimum clone length in tokens. | | --threshold | <n> | Fail the run when duplication percent is above this value. | | --mode | strict\|mild\|weak | jscpd clone detection mode. |

npx payloadcms-seo-cli duplicates --files "./content/posts/*.json" --min-lines 5 --min-tokens 50 --threshold 10 --mode mild

Meta report

meta checks title and description lengths against meta.fields in the config. Each field entry lists dot-separated paths (the first path present in a document is checked), a min and max character range, and required. Defaults match common SERP limits: title 50-60, description 150-160. A resource may override meta with its own fields.

Issues have a kind of below-min, above-max, or missing, and a failsGate flag. A present string below min or above max always fails the gate. A missing field fails the gate only when it is required; an optional missing field is skipped. The run exits 2 when any issue with failsGate: true is present.

npx payloadcms-seo-cli meta --files "./content/**/*.json"
npx payloadcms-seo-cli meta ./content/posts/abc123.json

push

Update existing documents. Collection files PATCH by id. Global files POST by slug.

npx payloadcms-seo-cli push --files "./content/posts/*.json"
npx payloadcms-seo-cli push --file ./content/globals/header.json --dry-run
npx payloadcms-seo-cli push --files "./content/pages/*.json" --status draft

create

Create a new collection document. Globals are a usage error. Requires --file and --slug.

npx payloadcms-seo-cli create --file ./content/posts/new.json --slug posts
npx payloadcms-seo-cli create --file ./content/posts/new.json --slug posts --dry-run

Write flags (push and create):

| Flag | Value | Meaning | |---|---|---| | --files | <glob> | JSON file glob (repeatable). | | --file | <file> | Single JSON file to read. | | --status | draft\|published | Override resources[slug].onPush for this run. | | --force | | Skip the check gate before push or create. | | --dry-run | | Print the intended REST write and do not send it. |

create also takes --slug <slug>.

Topic-specific keyword files

The 10-topic workflow: one phrase file per topic, same content files. --keywords-source <path> picks the phrase file for this run only; keywords.source in the config is left untouched.

# ecommerce topic - CSV phrase file
npx payloadcms-seo-cli keywords --files "./content/posts/*.json" --keywords-source ./topics/ecommerce.csv

# AI tools topic - JSON list of phrases
npx payloadcms-seo-cli keywords --files "./content/posts/*.json" --keywords-source ./topics/ai-tools.json

Both list formats work the same way as keywords.source in the config:

  • CSV: a header row, with keywords.csvPhraseColumn naming the column that holds the phrases. Extra columns (category, intent, volume) are ignored - only the phrase column is read.
  • JSON: an array of strings, one phrase per entry.

Format resolution precedence:

  1. An explicit --source csv|json wins.
  2. Otherwise the file extension of the effective path decides - the --keywords-source path when given, else keywords.source from the config.
  3. If neither decides, the run fails with a usage error. A mismatched explicit --source (for example a CSV file with --source json) is a usage error too; the CLI never silently picks the wrong parser.

check accepts --keywords-source as well, and push / create inherit it through the check gate (unless --force).

Tips for large or clustered keyword files:

  • One phrase per CSV row or JSON entry.
  • Extra CSV columns are ignored; only csvPhraseColumn is read.
  • Keep fieldsIgnore listing boilerplate fields (id, timestamps, _status) so the scans focus on content.
  • Run per topic so the absent list stays meaningful for that topic instead of blending every phrase file together.

Ideas (docs only - not implemented)

Future ideas, none implemented yet:

  • Repeatable --keywords-source (several phrase files in one run).
  • Category / intent filters to split reports by topic.
  • Per-file phrase reports.
  • JSON phrase files with per-phrase metadata.

Reports

keywords, duplicates, meta, and check print a report.

  • --format bash (default for stdout) or --format json
  • --output <file> always writes the JSON report to that file. If --format is omitted when --output is set, stdout format is JSON and stdout is not duplicated after the file write.
npx payloadcms-seo-cli check --files "./content/**/*.json" --format json --output ./seo-report.json

Exit codes: 0 success, 1 usage error, 2 SEO gate failed, 3 REST write failed.

Push policy

  • resources[slug].onPush sets _status (draft or published) for that slug.
  • --status draft|published overrides onPush for the run.
  • push and create run check on the target files first unless --force.
  • --dry-run prints the intended REST write (method, URL, and status) and does not send it.
  • media in the example is pull-only (push: false, create: false).

Workflow recipes

Concrete end-to-end flows. All scans are local and touch no REST endpoint; push and create hit the Payload REST API and run check on the target files first unless --force is used.

# Check one file with all gates
npx payloadcms-seo-cli check --file ./content/posts/abc123.json

# Check a glob of files with all gates
npx payloadcms-seo-cli check --files "./content/posts/*.json"

# Keyword presence across files, with a JSON report file
npx payloadcms-seo-cli keywords --files "./content/posts/*.json" --format json --output ./keywords-report.json

# Meta lengths only, across all local content
npx payloadcms-seo-cli meta --files "./content/**/*.json"

# Duplicates only, with a strict threshold
npx payloadcms-seo-cli duplicates --files "./content/**/*.json" --threshold 5 --mode strict

# Dry-run push - prints the intended REST write (method, URL, status) and sends nothing
npx payloadcms-seo-cli push --files "./content/posts/*.json" --dry-run

# Push a glob of edited documents as published (overrides resources.posts.onPush)
npx payloadcms-seo-cli push --files "./content/posts/*.json" --status published

# Dry-run create - validate a new document before writing
npx payloadcms-seo-cli create --file ./content/posts/new.json --slug posts --dry-run

# Pull a subset of resources
npx payloadcms-seo-cli pull --slug posts --slug pages

# Pull one document
npx payloadcms-seo-cli pull --slug posts --id <id>

# Pull drafts with full relationship depth
npx payloadcms-seo-cli pull --slug posts --draft true --depth 2

Environment

Loaded from .env in the current working directory when that file exists. Already-set process.env keys are not overwritten. Names come from payload.apiUrlEnv and payload.apiKeyEnv (example: PAYLOAD_API_URL, PAYLOAD_API_KEY).

# Payload API URL
PAYLOAD_API_URL=https://localhost:3000/api
# Payload API Key
# A misspelled PALOAD_API_KEY is still read as fallback by the CLI later.
PAYLOAD_API_KEY=

Never commit real API keys. Never put keys in payloadcms-seo-cli.json.

Programmatic API

Named exports from payloadcms-seo-cli. runCli is the same runner the bin uses: it prints help, reports, and errors, and returns an exit code (it does not call process.exit). The pull and scan functions return values and do not set an exit code:

import {
	runCli,
	pullResources,
	scanKeywords,
	scanDuplicates,
	scanMeta,
	scanCheck,
	loadSeoCliRuntime,
} from "payloadcms-seo-cli";

| Export | Role | |---|---| | runCli | Parse argv and dispatch a command. Returns an exit code. | | pullResources | Download configured resources to JSON files. | | scanKeywords | Keyword phrase scan. | | scanDuplicates | jscpd duplicate scan. | | scanMeta | Title and description length scan. | | scanCheck | Run the three scanners together. | | loadSeoCliRuntime | Load and validate config plus Payload env auth. |

Types for reports, config, and CLI options are exported alongside the functions.

Links

License

MIT

Copyright (c) 2026 Leo Marinchenko, Brainy Builds