@consilioweb/seo-analyzer
v1.6.0
Published
Payload CMS SEO plugin — 50+ checks, dashboard, Lexical JSON support, Flesch FR/EN readability, i18n
Maintainers
Readme
About
@consilioweb/seo-analyzer — A comprehensive SEO analysis plugin for Payload CMS 3 with 50+ checks, bilingual readability scoring (French & English), native Lexical JSON support, a full admin dashboard suite with auto-locale detection, and meta field labels in 39 languages.
Overview
@consilioweb/seo-analyzer adds a complete SEO toolkit directly into your Payload CMS admin panel. It runs 50+ on-page SEO checks in real time as editors write content, with bilingual support (French & English) — locale-adapted readability formulas (Kandel-Moles FR / Flesch-Kincaid EN), passive voice detection, transition words, and all SEO messages — plus native parsing of Payload's Lexical rich text format.
The plugin provides 9 dedicated admin views, 5 auto-managed collections, 20+ API endpoints, and automatic behaviors like slug-change redirect creation and score history tracking — all configured through a single plugin call. The admin dashboard automatically adapts to the user's Payload locale (FR/EN), and meta field UI labels support 39 languages via Payload's native i18n system.
Screenshots
| SEO Dashboard | Sitemap Audit |
|:---:|:---:|
|
|
|
| Editor Sidebar | Configuration |
|:---:|:---:|
|
|
|
Table of Contents
- Features
- Installation
- Quick Start
- Internationalization (i18n)
- Configuration
- Admin Views
- API Endpoints
- SEO Rules Reference
- Collections
- Fields Added to Collections
- Programmatic Usage
- Page Type Detection
- Package Exports
- Requirements
- Uninstall
- License
Features
SEO Analysis Engine (50+ Checks)
The core analyzer runs 17 rule groups covering every aspect of on-page SEO:
- Title — length (30-60 chars), keyword presence and position, duplicate brand detection, power words, numbers, questions, emotional words
- Meta Description — length (120-160 chars), keyword presence, call-to-action verbs
- URL / Slug — length, format validation, keyword presence, French stop word detection
- Headings — unique H1, keyword in H1/H2, heading hierarchy, H1 vs title differentiation, heading frequency
- Content — word count by page type, keyword in introduction, keyword density (0.5%-2.5%), placeholder detection, thin content, keyword distribution across content tiers, list detection
- Images — alt text coverage (80%+ threshold), keyword in alt, image presence and quantity
- Linking — internal links (3+ recommended), external links, generic anchor detection, empty link detection
- Social — OG image, title truncation on social platforms, description length for Facebook/LinkedIn
- Schema — structured data readiness (title + description + image)
- Readability — Flesch FR score, long sentences, long paragraphs, passive voice, transition words, consecutive same-start sentences, long sections without subheadings
- Quality — duplicate/placeholder content detection, substantial content validation
- Secondary Keywords — presence in title, description, content, and H2/H3 headings (up to 3 secondary keywords)
- Cornerstone — enhanced checks for pillar content (1500+ words, 5+ internal links, mandatory keyword)
- Freshness — content age tracking, review dates, year references, thin content aging penalty
- Technical — canonical URL validation, robots meta directives (noindex/nofollow)
- Accessibility — short anchors, alt text quality, empty headings, duplicate adjacent links, all-caps headings, link density ratio, camera filename detection, alt-heading redundancy
- E-commerce — price detection, product description length, image count, brand in title, price in meta, review readiness, availability status
Bilingual Readability (FR & EN)
Locale-adapted readability analysis with different formulas and thresholds per language:
| Check | French (Kandel-Moles) | English (Flesch-Kincaid) | |-------|----------------------|--------------------------| | Flesch pass | >= 40 | >= 60 | | Flesch warning | >= 25 | >= 40 | | Long sentences | > 25 words | > 20 words | | Passive voice max | 15% | 10% | | Transition words min | 15% | 20% |
French uses the Kandel-Moles coefficients (lower thresholds due to longer words: -tion, -ment, -ité), French passive voice detection (excludes passé composé with être-verbs), and 72 French transition words.
English uses the standard Flesch-Kincaid formula, English passive voice detection (be-verb + past participle), and 65 English transition words.
Native Lexical JSON Support
Natively parses Payload CMS Lexical rich text JSON structures with:
- Recursive text extraction (configurable max depth, default: 50)
- Heading extraction with tag and text
- Link extraction (internal/external) with anchor text
- Image extraction with alt text analysis
- List detection (ordered/unordered) for featured snippet optimization
- Support for nested blocks, columns, and all standard Payload block types
Admin Dashboard Suite (9 Views — FR/EN auto-locale)
All dashboard views automatically switch to the user's Payload admin locale (French or English). No configuration needed — the plugin detects useLocale() from @payloadcms/ui and adapts all labels, messages, dates, and UI strings accordingly.
| View | Path | Description |
|------|------|-------------|
| SEO Dashboard | /admin/seo | Sortable table of all pages/posts with scores, inline editing, bulk actions, filters |
| Sitemap Audit | /admin/sitemap-audit | Orphan pages, weak pages, broken internal links, hub detection, link graph analysis |
| SEO Configuration | /admin/seo-config | Site name, ignored slugs, disabled rules, custom thresholds, sitemap and breadcrumb settings |
| Redirect Manager | /admin/redirects | Full CRUD for 301/302 redirects with CSV import, test tool, and bulk operations |
| Cannibalization | /admin/cannibalization | Detect keyword cannibalization across pages sharing the same focus keyword |
| Performance | /admin/performance | Google Search Console data import (CSV/XLSX), trend charts, position tracking |
| Keyword Research | /admin/keyword-research | Keyword suggestions based on existing content, gap analysis |
| Schema Builder | /admin/schema-builder | Visual JSON-LD schema.org structured data generation |
| Link Graph | /admin/link-graph | Internal link structure visualization with hub and orphan detection |
Editor Sidebar Components
- SeoAnalyzer — Real-time SEO scoring widget in the document editor sidebar with pass/warning/fail indicators, actionable tips, and grouped checks
- Score History Chart — Inline score trend visualization over time
- Content Decay Section — Freshness and aging indicators
- Social Preview — Facebook and Twitter card preview
Automatic Behaviors
- Auto-redirect on slug change — Creates a 301 redirect when a document's slug is modified (with redirect chain detection)
- Score history tracking — Records SEO score snapshots on every document save via afterChange hook
- Cache warm-up — Pre-loads collection data on startup and hourly for instant dashboard response
- SEO Logs (404 monitoring) — Tracks 404 errors with hit count, referrer, and user agent for proactive redirect management
Installation
pnpm add @consilioweb/seo-analyzerOr with npm/yarn:
npm install @consilioweb/seo-analyzer
yarn add @consilioweb/seo-analyzerPeer Dependencies
The plugin requires Payload CMS 3.x. The following peer dependencies are optional but recommended for full admin UI features:
| Package | Version | Required |
|---------|---------|----------|
| payload | ^3.0.0 | Yes |
| @payloadcms/next | ^3.0.0 | Optional (admin views) |
| @payloadcms/ui | ^3.0.0 | Optional (admin UI) |
| react | ^18.0.0 \|\| ^19.0.0 | Optional (admin UI) |
Note: For XLSX import in the Performance view, install
xlsxseparately (pnpm add xlsx). It is loaded dynamically and not required as a peer dependency.
Quick Start
Add the plugin to your payload.config.ts:
import { buildConfig } from 'payload'
import { seoAnalyzerPlugin } from '@consilioweb/seo-analyzer'
export default buildConfig({
// ... your existing config
plugins: [
seoAnalyzerPlugin({
collections: ['pages', 'posts'],
}),
],
})Using alongside
@payloadcms/plugin-seo? The export is namedseoAnalyzerPlugin(notseoPlugin) specifically to avoid naming conflicts with the official Payload SEO plugin. If both plugins target the same collections, a warning will be logged at startup to help you avoid duplicate SEO fields. You can safely use both plugins together — just make sure they target different collections, or accept the overlap if intentional.The legacy import
import { seoPlugin } from '@consilioweb/seo-analyzer'still works for backward compatibility.
That's it. The plugin will automatically:
- Add SEO fields (
focusKeyword,focusKeywords,isCornerstone) and the SeoAnalyzer sidebar widget to the specified collections - Auto-create meta fields (
meta.title,meta.description,meta.image) with SERP preview — unless@payloadcms/plugin-seois already handling them - Create 5 managed collections for score history, performance data, settings, redirects, and 404 logs
- Register 20+ API endpoints under
/api/seo-plugin/ - Add 9 admin views with a collapsible navigation group
- Attach
beforeChange(auto-redirect) andafterChange(score tracking) hooks to target collections and globals - Inject meta field translations (39 languages) into Payload's i18n system
- Start background cache warm-up on server init
Internationalization (i18n)
The plugin has three layers of internationalization:
| Layer | Languages | What it covers | |-------|-----------|----------------| | SEO Analysis Engine | FR, EN | 50+ check messages, tips, readability formulas, linguistic analysis | | Admin Dashboard | FR, EN | All 9 dashboard views, sidebar components, navigation labels (~500 strings) | | Meta Field UI Labels | 39 languages | Field labels, descriptions, and generate buttons in the Payload admin |
1. SEO Analysis Locale
Controls the language of SEO check messages and linguistic analysis via the locale option:
seoAnalyzerPlugin({
collections: ['pages', 'posts'],
locale: 'en', // 'fr' (default) | 'en'
})| Feature | locale: 'fr' (default) | locale: 'en' |
|---------|--------------------------|-----------------|
| SEO messages & tips | French | English |
| Readability formula | Kandel-Moles (FR thresholds) | Flesch-Kincaid (EN thresholds) |
| Passive voice detection | être + participe passé | be-verb + past participle |
| Transition words | 72 French expressions | 65 English expressions |
| Stop words in slug | French stop words | English stop words |
| Action verbs (CTA) | 30 French verbs | 30 English verbs |
| Power words | 29 French words | 30 English words |
| Page type detection | FR slugs (mentions-legales, contact) | EN slugs (privacy-policy, contact-us) |
| Question words (title) | comment, pourquoi, quand... | how, why, when... |
2. Dashboard Auto-Locale (FR/EN)
The admin dashboard automatically adapts to the Payload user's locale — no configuration needed. When the admin switches their UI language in Payload (e.g. via the locale selector), all dashboard labels, messages, dates, and UI strings switch instantly.
This works via useLocale() from @payloadcms/ui. Any locale starting with en maps to English; all others default to French.
Covered components: SEO Dashboard, Sitemap Audit, SEO Config, Redirect Manager, Cannibalization, Performance, Keyword Research, Schema Builder, Link Graph, SeoAnalyzer sidebar, Score History, Content Decay, Social Preview, SERP Preview, Meta fields (title, description, image, overview).
3. Meta Field Labels (39 Languages)
The plugin injects translations for meta field UI labels (title, description, image, overview, SERP preview) into Payload's native i18n system. These are auto-loaded — no configuration needed.
Supported languages: Arabic, Azerbaijani, Bulgarian, Catalan, Czech, German, English, Spanish, Estonian, Farsi, Finnish, French, Hebrew, Croatian, Hungarian, Indonesian, Italian, Japanese, Korean, Malay, Norwegian, Dutch, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Swedish, Thai, Turkish, Ukrainian, Vietnamese, Chinese (Simplified & Traditional), Bengali, Greek, Latvian, Serbian.
Backward Compatibility
The locale option defaults to 'fr' — existing installations are unaffected. All legacy exports (calculateFleschFR, getStopWordsFR, POWER_WORDS_FR, etc.) remain available as aliases.
Programmatic Usage with Locale
import { analyzeSeo } from '@consilioweb/seo-analyzer'
const result = analyzeSeo(input, { locale: 'en' })
// All messages returned in English, EN readability thresholds appliedConfiguration
SeoPluginConfig
seoAnalyzerPlugin({
// Toutes les options sont optionnelles — des valeurs par défaut sont utilisées
collections: ['pages', 'posts'],
globals: [],
locale: 'fr',
tabbedUI: false,
autoCreateMetaFields: true,
uploadsCollection: 'media',
generateTitle: undefined,
generateDescription: undefined,
generateImage: undefined,
generateURL: undefined,
fields: undefined,
localeMapping: undefined,
addDashboardView: true,
addSitemapAuditView: true,
disabledRules: [],
overrideWeights: {},
thresholds: {},
localSeoSlugs: [],
siteName: undefined,
siteUrl: undefined,
endpointBasePath: '/seo-plugin',
trackScoreHistory: true,
redirectsCollection: 'seo-redirects',
knownRoutes: [],
seoLogsSecret: undefined,
interfaceName: undefined,
})| Option | Type | Default | Description |
|--------|------|---------|-------------|
| collections | string[] | ['pages', 'posts'] | Collections auxquelles ajouter les champs SEO et les hooks |
| globals | string[] | [] | Globals auxquels ajouter les champs SEO et les hooks |
| locale | 'fr' \| 'en' | 'fr' | Langue pour les messages SEO, l'analyse de lisibilité et les vérifications linguistiques |
| tabbedUI | boolean | false | Organiser les champs en onglets "Content" + "SEO" |
| autoCreateMetaFields | boolean | true | Créer automatiquement les champs meta (title, description, image) si @payloadcms/plugin-seo n'est pas détecté |
| uploadsCollection | string | 'media' | Slug de la collection pour le champ d'upload meta image |
| generateTitle | function | undefined | Fonction custom pour générer le meta title |
| generateDescription | function | undefined | Fonction custom pour générer la meta description |
| generateImage | function | undefined | Fonction custom pour générer la meta image (retourne un ID media ou URL) |
| generateURL | function | undefined | Fonction custom pour générer l'URL de la page (aperçu SERP) |
| fields | function | undefined | Surcharger les champs meta par défaut : ({ defaultFields }) => Field[] |
| localeMapping | Record<string, 'fr' \| 'en'> | undefined | Mapper les codes locale Payload vers la locale d'analyse (ex: { 'fr-FR': 'fr', 'en-US': 'en' }) |
| addDashboardView | boolean | true | Enregistrer le dashboard SEO et toutes les vues admin |
| addSitemapAuditView | boolean | true | Enregistrer la vue d'audit sitemap |
| disabledRules | RuleGroup[] | [] | Groupes de règles à désactiver entièrement |
| overrideWeights | Partial<Record<RuleGroup, number>> | {} | Surcharger le poids de tous les checks d'un groupe de règles |
| thresholds | SeoThresholds | Voir ci-dessous | Seuils personnalisés pour les vérifications d'analyse |
| localSeoSlugs | string[] | [] | Slugs supplémentaires reconnus comme pages SEO local |
| siteName | string | undefined | Nom du site pour la détection de duplication de marque dans les titres |
| siteUrl | string | undefined | URL de base du site (utilisée pour la validation d'URL canonique, ex: 'https://example.com') |
| endpointBasePath | string | '/seo-plugin' | Préfixe du chemin de base pour tous les endpoints API |
| trackScoreHistory | boolean | true | Activer la collection d'historique des scores et le hook afterChange de suivi |
| redirectsCollection | string | 'seo-redirects' | Slug de la collection de redirections auto-créée |
| knownRoutes | string[] | [] | Routes dynamiques qui ne doivent pas être signalées comme liens cassés |
| seoLogsSecret | string | undefined | Secret partagé pour l'endpoint POST des logs SEO (auth middleware) |
| interfaceName | string | undefined | Nom d'interface TypeScript personnalisé pour le type du groupe meta généré (ex: 'SharedSEO') |
SeoThresholds
Tous les seuils sont optionnels. Les valeurs par défaut sont utilisées si omis.
| Seuil | Type | Default | Description |
|-------|------|---------|-------------|
| titleLengthMin | number | 30 | Longueur minimale du meta title (caractères) |
| titleLengthMax | number | 60 | Longueur maximale du meta title (caractères) |
| metaDescLengthMin | number | 120 | Longueur minimale de la meta description |
| metaDescLengthMax | number | 160 | Longueur maximale de la meta description |
| minWordsGeneric | number | 300 | Nombre minimum de mots pour les pages génériques |
| minWordsPost | number | 800 | Nombre minimum de mots pour les articles de blog |
| keywordDensityMin | number | 0.5 | Densité minimale du mot-clé (%) |
| keywordDensityMax | number | 3 | Densité maximale du mot-clé (%) |
| fleschScorePass | number | 40 | Score Flesch FR seuil de réussite |
| slugMaxLength | number | 75 | Longueur maximale du slug (caractères) |
RuleGroup Values
type RuleGroup =
| 'title'
| 'meta-description'
| 'url'
| 'headings'
| 'content'
| 'images'
| 'linking'
| 'social'
| 'schema'
| 'readability'
| 'quality'
| 'secondary-keywords'
| 'cornerstone'
| 'freshness'
| 'technical'
| 'accessibility'
| 'ecommerce'Advanced Configuration Example
import { seoAnalyzerPlugin } from '@consilioweb/seo-analyzer'
export default buildConfig({
plugins: [
seoAnalyzerPlugin({
collections: ['pages', 'posts', 'products'],
globals: ['header', 'footer'],
locale: 'en',
tabbedUI: true, // Wrap fields in Content + SEO tabs
siteName: 'My Website',
endpointBasePath: '/seo',
knownRoutes: ['blog', 'products', 'categories'],
localSeoSlugs: ['plumber-paris', 'plumber-lyon'],
disabledRules: ['social', 'schema'],
overrideWeights: {
readability: 1,
cornerstone: 5,
},
thresholds: {
titleLengthMax: 65,
minWordsPost: 1000,
fleschScorePass: 35,
},
// Generate functions (called by the "Generate" buttons in meta fields)
generateTitle: ({ doc }) => `${(doc as any).title} | My Website`,
generateDescription: ({ doc }) => `Discover ${(doc as any).title} on My Website.`,
generateURL: ({ doc }) => `https://mywebsite.com/${(doc as any).slug || ''}`,
// Custom meta fields layout
fields: ({ defaultFields }) => [
...defaultFields,
{ name: 'canonicalUrl', type: 'text', label: 'Canonical URL' },
],
// Map Payload locales to analysis language
localeMapping: { 'fr-FR': 'fr', 'en-US': 'en' },
seoLogsSecret: process.env.SEO_LOGS_SECRET,
}),
],
})Admin Views
SEO Dashboard (/admin/seo)
The main dashboard displays a sortable, filterable table of all pages and posts with their SEO scores. Features include:
- Color-coded score badges (excellent/good/ok/poor)
- Sortable columns: score, title, word count, focus keyword, H1, OG image, links, readability
- Quick filters: missing meta, missing H1, low readability
- Inline editing of meta title and description
- Bulk actions: export CSV, mark/unmark cornerstone
- Checkboxes for multi-selection
- Score trend indicators (up/down arrows)
- Multi-keyword display
- Quick links to edit each document
Sitemap Audit (/admin/sitemap-audit)
Analyzes your site's internal structure to identify:
- Orphan pages — pages with no internal links pointing to them
- Weak pages — pages with few incoming links (with anchor text display)
- Broken internal links — links pointing to non-existent pages (with fix suggestions)
- Hub pages — pages with the most outgoing internal links
- One-click 301 redirect creation for broken links
- SEO scores alongside orphan and weak pages
- Hover previews with contextual information
- Export — JSON and CSV download of the full link graph
SEO Configuration (/admin/seo-config)
Centralized settings management:
- Site name (for brand duplicate detection)
- Ignored slugs (excluded from audits)
- Disabled rule groups
- Custom thresholds (title length, word counts, etc.)
- Sitemap configuration (excluded slugs, change frequency, priority overrides)
- Breadcrumb configuration (separator, home label, display options)
Redirect Manager (/admin/redirects)
Full redirect management with:
- CRUD operations for 301/302 redirects
- CSV import for bulk redirect creation
- Redirect test tool (verify where a URL redirects)
- Bulk delete operations
Cannibalization Detection (/admin/cannibalization)
Identifies pages competing for the same keywords by detecting documents that share identical focus keywords.
Performance Tracking (/admin/performance)
Import and visualize Google Search Console data:
- CSV and XLSX file import (supports French GSC headers)
- Click, impression, CTR, and position tracking
- Trend visualization over time
- Per-URL and per-query breakdowns
Keyword Research (/admin/keyword-research)
Keyword analysis based on your existing content:
- Keyword suggestions derived from current pages
- Gap analysis to identify missing keyword coverage
Schema Builder (/admin/schema-builder)
Visual tool for generating JSON-LD structured data (schema.org) markup for your pages.
Link Graph (/admin/link-graph)
Interactive visualization of your site's internal linking structure:
- Node-based graph representation
- Hub and orphan page identification
- Link equity flow analysis
API Endpoints
All endpoints are prefixed with the configured endpointBasePath (default: /seo-plugin). All endpoints require an authenticated admin user unless noted otherwise.
| Method | Path | Description |
|--------|------|-------------|
| GET POST | /validate | Run SEO analysis on a document |
| GET | /check-keyword | Check for keyword duplication across collections |
| GET | /audit | Full site-wide SEO audit |
| GET | /history | Score history data for trend charts |
| GET | /sitemap-audit | Sitemap structure audit |
| GET PATCH | /settings | Read or update SEO settings |
| POST | /suggest-links | Internal link suggestions for a page |
| POST | /create-redirect | Create a single redirect entry |
| GET POST PATCH DELETE | /redirects | Full CRUD for redirect management |
| POST | /ai-generate | AI-powered meta title/description generation |
| GET | /cannibalization | Detect keyword cannibalization |
| POST | /external-links | Check external link status (live HTTP checks with SSRF protection) |
| GET | /sitemap-config | Sitemap configuration data |
| GET POST | /performance | Read or import performance data (CSV/XLSX) |
| GET | /keyword-research | Keyword suggestions and gap analysis |
| GET | /breadcrumb | Breadcrumb configuration and data |
| GET | /link-graph | Internal link graph data |
| GET POST DELETE | /seo-logs | 404 log management (POST supports secret-header auth) |
SEO Rules Reference
Scoring Algorithm
Each check has a weight (1-5) and produces a status (pass, warning, or fail):
- Pass — earns 100% of weight points
- Warning — earns 50% of weight points
- Fail — earns 0 points
Final score = round(earnedPoints / maxPoints * 100)
| Level | Score Range | |-------|-------------| | Excellent | >= 91 | | Good | >= 71 | | OK | >= 41 | | Poor | < 41 |
Complete Check List
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| title-missing | 3 | Critical | Meta title is present |
| title-length | 3 | Critical | Title between 30-60 characters |
| title-keyword | 3 | Critical | Focus keyword in title |
| title-keyword-position | 2 | Important | Keyword in first half of title |
| title-duplicate-brand | 2 | Important | No duplicate brand name |
| title-power-words | 1 | Bonus | Contains power words |
| title-has-number | 1 | Bonus | Contains a number (+36% CTR) |
| title-is-question | 1 | Bonus | Question format (Featured Snippet friendly) |
| title-sentiment | 1 | Bonus | Contains emotional words |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| meta-desc-missing | 3 | Critical | Meta description is present |
| meta-desc-length | 3 | Critical | Length between 120-160 characters |
| meta-desc-keyword | 3 | Critical | Focus keyword in description |
| meta-desc-cta | 2 | Important | Contains action verb or CTA pattern |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| slug-missing | 2 | Important | Slug is defined |
| slug-length | 2 | Important | Slug under 75 characters |
| slug-format | 2 | Important | Lowercase, no special characters |
| slug-keyword | 2 | Important | Focus keyword in slug |
| slug-stopwords | 1 | Bonus | No stop words (FR or EN based on locale) |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| h1-missing / h1-unique | 2 | Important | Exactly one H1 per page |
| h1-keyword | 2 | Important | Keyword in H1 |
| heading-hierarchy | 2 | Important | Proper heading hierarchy (no level skip) |
| h2-keyword | 2 | Important | Keyword in at least one H2 |
| heading-frequency | 1 | Bonus | One subheading every ~300 words |
| h1-title-different | 1 | Important | H1 differs from meta title |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| content-wordcount | 2 | Important | Meets minimum word count by page type |
| content-keyword-intro | 2 | Important | Keyword in first paragraph |
| content-keyword-density | 2-3 | Important/Critical | Density between 0.5%-2.5% |
| content-no-placeholder | 3 | Critical | No lorem ipsum, TODO, or placeholders |
| content-thin | 2 | Important | Not thin content (>100 words) |
| content-keyword-distribution | 2 | Important | Keyword in 2+ of 3 content tiers |
| content-has-lists | 1 | Bonus | Contains ordered/unordered lists |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| images-alt | 2 | Important | Alt text on 80%+ of images |
| images-alt-keyword | 1 | Bonus | Keyword in at least one alt text |
| images-present | 2 | Important | At least one image |
| images-quantity | 1-2 | Bonus/Important | Multiple images for posts |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| linking-internal | 2 | Important | At least one internal link (3+ ideal) |
| linking-external | 1 | Bonus | At least one external link |
| linking-generic-anchors | 2 | Important | No generic anchor text |
| linking-empty | 2 | Important | No empty links |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| social-og-image | 2 | Important | OG/meta image defined |
| social-title-truncation | 1 | Bonus | Title within social platform limits (~65 chars) |
| social-desc-length | 1 | Bonus | Description within Facebook/LinkedIn limits (~155 chars) |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| schema-readiness | 1 | Bonus | Page has enough metadata for JSON-LD generation |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| readability-flesch | 2 | Important | Flesch reading ease (FR: >= 40, EN: >= 60) |
| readability-long-sentences | 2 | Important | Long sentence ratio < 30% (FR: >25 words, EN: >20 words) |
| readability-long-paragraphs | 2 | Important | No paragraphs over 150 words |
| readability-passive | 2 | Important | Passive voice ratio (FR: < 15%, EN: < 10%) |
| readability-transitions | 1 | Bonus | Transition words (FR: 15%+, EN: 20%+) |
| readability-consecutive-starts | 1 | Bonus | No 3+ consecutive sentences with same first word |
| readability-long-sections | 2 | Important | No sections >400 words without subheadings |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| quality-no-duplicate | 3 | Critical | No duplicate or generic content |
| quality-substantial | 3 | Critical | Enough content substance (>50 words fail, >200 warning) |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| secondary-kw-title-* | 1 | Bonus | Secondary keyword in title |
| secondary-kw-desc-* | 1 | Bonus | Secondary keyword in description |
| secondary-kw-content-* | 1 | Bonus | Secondary keyword in content |
| secondary-kw-heading-* | 1 | Bonus | Secondary keyword in H2/H3 |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| cornerstone-wordcount | 4 | Important | 1500+ words for pillar content |
| cornerstone-internal-links | 4 | Important | 5+ internal links |
| cornerstone-focus-keyword | 5 | Critical | Focus keyword is defined |
| cornerstone-meta-description | 5 | Critical | Meta description is present and optimized |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| freshness-age | 1-3 | Bonus/Important | Content updated within 6/12 months |
| freshness-reviewed | 2 | Bonus | Content reviewed within 6 months |
| freshness-year-ref | 2 | Important | Current year referenced in content |
| freshness-thin-aging | 3 | Important | Thin + old content penalty |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| canonical-* | 2 | Important | Canonical URL is valid and correctly set |
| robots-noindex | 2-3 | Important/Critical | Noindex directive detection |
| robots-nofollow | 2 | Important | Nofollow directive detection |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| a11y-short-anchors | 2 | Important | No links with text under 3 characters |
| a11y-alt-quality | 2 | Important | No generic or filename-based alt texts |
| a11y-empty-headings | 3 | Critical | No empty heading tags |
| a11y-duplicate-links | 1 | Bonus | No adjacent duplicate links |
| a11y-all-caps | 1 | Bonus | No all-caps headings |
| a11y-link-density | 2 | Important | Link text ratio under 30% of content |
| a11y-image-filename | 2 | Important | No camera default filenames in alt |
| a11y-alt-duplicates-context | 1 | Bonus | Alt text differs from adjacent headings |
| Check ID | Weight | Category | Description |
|----------|--------|----------|-------------|
| product-price-mentioned | 2 | Important | Price visible in content |
| product-short-description | 2 | Important | Description >= 100 words |
| product-has-images | 3 | Critical | At least 2 product images |
| product-title-includes-brand | 1 | Bonus | Brand/keyword in meta title |
| product-meta-includes-price | 1 | Bonus | Price in meta description |
| product-review-readiness | 1 | Bonus | Review/rating content detected |
| product-availability | 2 | Important | Availability status mentioned |
Collections
The plugin automatically creates and manages these collections (all hidden from admin nav, managed via plugin views):
| Collection | Slug | Description |
|------------|------|-------------|
| SEO Score History | seo-score-history | Score snapshots per document (ID, collection, score, level, word count, keyword, checks summary, date) |
| SEO Performance | seo-performance | Search Console data (URL, query, clicks, impressions, CTR, position, date, source) |
| SEO Settings | seo-settings | Site-wide config (site name, ignored slugs, disabled rules, thresholds, sitemap config, breadcrumb config) |
| SEO Redirects | seo-redirects | 301/302 redirect rules (from, to, type). Slug is configurable via redirectsCollection |
| SEO Logs | seo-logs | 404 error tracking (URL, type, hit count, last seen, referrer, user agent, ignored flag) |
Fields Added to Collections
The plugin adds the following fields to each target collection specified in collections:
SEO Analyzer Fields (sidebar)
| Field | Type | Location | Description |
|-------|------|----------|-------------|
| isCornerstone | checkbox | Sidebar | Marks the document as pillar/cornerstone content (triggers enhanced checks) |
| focusKeyword | text | Sidebar | Primary SEO focus keyword for analysis |
| seoAnalyzer | ui | Sidebar | Real-time SEO analysis widget with score, checks, and actionable tips |
| focusKeywords | array (max 3) | Collapsible group | Secondary focus keywords for additional coverage |
Meta Fields (auto-created)
When @payloadcms/plugin-seo is not detected on a collection, the plugin auto-creates a meta field group with generate buttons and SERP preview. Set autoCreateMetaFields: false to disable.
| Field | Type | Description |
|-------|------|-------------|
| meta._overview | ui | Completeness indicator (0/3 to 3/3 — title, description, image) |
| meta.title | text | Meta title with character counter (30-60), progress bar, and "Generate" button |
| meta.description | textarea | Meta description with character counter (120-160) and "Generate" button |
| meta.image | upload | Meta/OG image with status indicator and optional "Generate" button |
| meta._preview | ui | Google SERP preview (desktop + mobile toggle, Google 2025 styling) |
Compatibility with
@payloadcms/plugin-seo: If the official plugin is already adding meta fields to a collection, our plugin detects this and skips auto-creation. Both plugins can safely coexist.
Programmatic Usage
The analyzer can be used independently of the Payload plugin system:
import { analyzeSeo } from '@consilioweb/seo-analyzer'
import type { SeoInput, SeoConfig } from '@consilioweb/seo-analyzer'
const input: SeoInput = {
metaTitle: 'My Page Title - Brand',
metaDescription: 'A comprehensive description of my page for search engines...',
slug: 'my-page',
focusKeyword: 'my keyword',
heroTitle: 'Welcome to My Page',
heroRichText: { /* Lexical JSON root node */ },
blocks: [ /* Payload layout blocks */ ],
content: { /* Lexical JSON for posts */ },
isPost: false,
isProduct: false,
isCornerstone: false,
updatedAt: '2025-06-01T00:00:00Z',
}
const config: SeoConfig = {
siteName: 'Brand',
localSeoSlugs: ['paris', 'lyon'],
disabledRules: ['social'],
thresholds: { minWordsPost: 1000 },
}
const result = analyzeSeo(input, config)
// {
// score: 78,
// level: 'good',
// checks: [
// { id: 'title-length', status: 'pass', message: '...', weight: 3, ... },
// { id: 'content-wordcount', status: 'warning', message: '...', weight: 2, ... },
// ...
// ]
// }Exported Helpers
The package re-exports utility functions for advanced use cases:
import {
// Lexical JSON parsing
extractTextFromLexical,
extractHeadingsFromLexical,
extractLinksFromLexical,
extractImagesFromLexical,
extractLinkUrlsFromLexical,
extractListsFromLexical,
checkImagesInBlocks,
// Text analysis (bilingual — pass locale: 'fr' | 'en')
countWords,
countSentences, // countSentences(text, locale?)
countSyllablesFR, // French syllable counter
countSyllablesEN, // English syllable counter
calculateFlesch, // calculateFlesch(text, locale) — Kandel-Moles (FR) or Flesch-Kincaid (EN)
calculateFleschFR, // Legacy alias for calculateFlesch(text, 'fr')
detectPassiveVoice, // detectPassiveVoice(sentence, locale?)
hasTransitionWord, // hasTransitionWord(sentence, locale?)
checkHeadingHierarchy,
countLongSections,
// Keyword utilities
normalizeForComparison,
slugifyKeyword,
keywordMatchesText,
countKeywordOccurrences,
// Page type detection (bilingual)
detectPageType, // detectPageType(slug, collection?, extra?, locale?)
// Bilingual constant accessors — pass locale: 'fr' | 'en'
getStopWords, // getStopWords(locale)
getActionVerbs, // getActionVerbs(locale)
getPowerWords, // getPowerWords(locale)
getGenericAnchors, // getGenericAnchors(locale)
getLegalSlugs, // getLegalSlugs(locale)
getUtilitySlugs, // getUtilitySlugs(locale)
getEvergreenSlugs, // getEvergreenSlugs(locale)
getStopWordCompounds, // getStopWordCompounds(locale)
// Legacy aliases (backward compat)
getStopWordsFR, // = getStopWords('fr')
getActionVerbsFR, // = getActionVerbs('fr')
POWER_WORDS_FR, // = POWER_WORDS.fr
isStopWordInCompoundExpression,
// Locale-specific thresholds
FLESCH_THRESHOLDS, // { fr: { pass: 40, warn: 25 }, en: { pass: 60, warn: 40 } }
READABILITY_THRESHOLDS, // { fr: { longSentenceWords: 25, ... }, en: { longSentenceWords: 20, ... } }
// Constants (thresholds, limits)
TITLE_LENGTH_MIN, // 30
TITLE_LENGTH_MAX, // 60
META_DESC_LENGTH_MIN, // 120
META_DESC_LENGTH_MAX, // 160
MIN_WORDS_POST, // 800
MIN_WORDS_GENERIC, // 300
SCORE_EXCELLENT, // 91
SCORE_GOOD, // 71
SCORE_OK, // 41
// ... and more
} from '@consilioweb/seo-analyzer'Page Type Detection
The analyzer automatically adapts thresholds and check severity based on the detected page type:
| Page Type | Detection Logic (FR) | Detection Logic (EN) | Adapted Behavior |
|-----------|---------------------|----------------------|------------------|
| blog | isPost: true | isPost: true | Higher word count threshold (800 words) |
| home | Slug is home or empty | Slug is home or empty | Standard checks |
| contact | contact | contact, contact-us, get-in-touch | Relaxed: images optional, external links optional, freshness lenient |
| form | formulaire, devis, inscription | quote, signup, register, apply | Relaxed: word count min 150, images optional |
| legal | mentions-legales, cgv, politique-de-confidentialite | privacy-policy, terms, tos, gdpr, cookies | Relaxed: word count min 200, images optional, freshness 24 months |
| local-seo | Matches configured localSeoSlugs | Matches configured localSeoSlugs | Standard checks with local SEO context |
| service | service, prestation | services, our-services | Standard checks |
| resource | ressource, guide, tutoriel | resources, guide, tutorial | Standard checks |
| agency | agence, a-propos, equipe | about, about-us, team | Standard checks |
| generic | Default fallback | Default fallback | Standard checks (300 words min) |
Note: Page type detection checks both FR and EN slug patterns regardless of locale, so a French site with an
aboutslug will still be correctly detected.
Package Exports
The package provides three entry points for different use contexts:
// Main entry — plugin, analyzer, types, helpers, constants
import {
seoAnalyzerPlugin, analyzeSeo, seoFields, metaFields,
resolveAnalysisLocale, fetchAllDocs, createGenerateHandler,
} from '@consilioweb/seo-analyzer'
import type { GenerateFnArgs, MetaFieldsConfig } from '@consilioweb/seo-analyzer'
// Client components — React components for Payload admin UI
import {
SeoAnalyzerField,
SeoNavLink,
ScoreHistoryChart,
ContentDecaySection,
SeoSocialPreview,
// Meta field components (used internally, also available for custom layouts)
MetaTitleField,
MetaDescriptionField,
MetaImageField,
OverviewField,
SerpPreview,
} from '@consilioweb/seo-analyzer/client'
// Server views — admin views wrapped in DefaultTemplate
import {
SeoView,
SitemapAuditView,
SeoConfigView,
RedirectManagerView,
CannibalizationView,
PerformanceView,
KeywordResearchView,
SchemaBuilderView,
LinkGraphView,
} from '@consilioweb/seo-analyzer/views'Requirements
- Node.js >= 18
- Payload CMS 3.x
- React 18.x or 19.x (for admin UI components)
- Database: Any Payload-supported adapter (SQLite, PostgreSQL, MongoDB)
Uninstall
One command handles everything — code cleanup, package removal, and importmap regeneration:
npx seo-analyzer-uninstallThe script automatically:
- Scans
src/and removes allimportstatements andseoAnalyzerPlugin()/seoPlugin()calls - Runs
pnpm remove @consilioweb/seo-analyzer(detects your package manager) - Regenerates the Payload importmap
No manual editing needed.
What happens to your data?
Your data is safe. The plugin uses Payload's standard API (payload.find, payload.create, etc.) with zero raw SQL queries — it is fully database-agnostic and works identically with SQLite, PostgreSQL, and MongoDB.
When you remove the plugin:
| What | Status | Action needed |
|------|--------|---------------|
| Plugin collections (seo-score-history, seo-performance, seo-settings, seo-redirects, seo-logs) | Tables/documents remain in DB | Delete manually if you want to reclaim space |
| Fields added to your collections (focusKeyword, focusKeywords, isCornerstone) | Data remains in DB | Columns/fields are ignored by Payload but stay in storage |
| Admin views & API endpoints | Removed automatically | No action needed |
| Hooks (auto-redirect, score tracking) | Removed automatically | No action needed |
Full cleanup (optional)
If you want to remove all plugin data from your database:
SQLite:
DROP TABLE IF EXISTS seo_score_history;
DROP TABLE IF EXISTS seo_performance;
DROP TABLE IF EXISTS seo_settings;
DROP TABLE IF EXISTS seo_redirects;
DROP TABLE IF EXISTS seo_logs;PostgreSQL:
DROP TABLE IF EXISTS "seo-score-history" CASCADE;
DROP TABLE IF EXISTS "seo-performance" CASCADE;
DROP TABLE IF EXISTS "seo-settings" CASCADE;
DROP TABLE IF EXISTS "seo-redirects" CASCADE;
DROP TABLE IF EXISTS "seo-logs" CASCADE;MongoDB:
db.getCollection('seo-score-history').drop()
db.getCollection('seo-performance').drop()
db.getCollection('seo-settings').drop()
db.getCollection('seo-redirects').drop()
db.getCollection('seo-logs').drop()Note: The plugin never drops tables or deletes data automatically. This is by design — your SEO history and redirects are valuable data that should only be removed intentionally.
License
Author
Made with passion by ConsilioWEB
