@consilioweb/payload-seo-analyzer 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1201 @@
1
+ <!-- Header Banner -->
2
+ <div align="center">
3
+
4
+ <a href="https://git.io/typing-svg">
5
+ <img src="https://readme-typing-svg.demolab.com?font=Fira+Code&weight=700&size=32&duration=3000&pause=1000&color=3B82F6&center=true&vCenter=true&width=700&lines=%40consilioweb%2Fseo-analyzer;Payload+CMS+SEO+Plugin;50%2B+Checks+%7C+39+Languages;Admin+Dashboard+Suite+%7C+FR+%26+EN" alt="Typing SVG" />
6
+ </a>
7
+
8
+ <br><br>
9
+
10
+ <!-- Badges -->
11
+ <a href="https://www.npmjs.com/package/@consilioweb/payload-seo-analyzer"><img src="https://img.shields.io/npm/v/@consilioweb/payload-seo-analyzer?style=for-the-badge&logo=npm&logoColor=white&color=CB3837" alt="npm version"></a>
12
+ <a href="https://www.npmjs.com/package/@consilioweb/payload-seo-analyzer"><img src="https://img.shields.io/npm/dw/@consilioweb/payload-seo-analyzer?style=for-the-badge&logo=npm&logoColor=white&color=CB3837" alt="npm downloads"></a>
13
+ <img src="https://img.shields.io/badge/Payload%20CMS-3.x-0F172A?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTIgMkw0IDdWMTdMMTIgMjJMMjAgMTdWN0wxMiAyWiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=&logoColor=white" alt="Payload CMS 3">
14
+ <img src="https://img.shields.io/badge/SEO-50%2B%20Checks-10B981?style=for-the-badge" alt="50+ Checks">
15
+ <img src="https://img.shields.io/badge/i18n-39%20Languages-3B82F6?style=for-the-badge" alt="i18n 39 Languages">
16
+ <a href="https://github.com/pOwn3d/payload-seo-analyzer/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-7C3AED?style=for-the-badge" alt="MIT License"></a>
17
+ <img src="https://img.shields.io/badge/TypeScript-5.x-3178C6?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript">
18
+ <a href="https://github.com/pOwn3d/payload-seo-analyzer/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/pOwn3d/payload-seo-analyzer/ci.yml?branch=main&style=for-the-badge&logo=github-actions&logoColor=white" alt="CI"></a>
19
+ <a href="https://github.com/pOwn3d/payload-seo-analyzer"><img src="https://img.shields.io/github/stars/pOwn3d/payload-seo-analyzer?style=for-the-badge&logo=github&color=181717" alt="GitHub stars"></a>
20
+
21
+ </div>
22
+
23
+ <p align="center">
24
+ <a href="https://buymeacoffee.com/pown3d">
25
+ <img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-☕-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black" alt="Buy me a coffee" />
26
+ </a>
27
+ </p>
28
+
29
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
30
+
31
+ > [!IMPORTANT]
32
+ > ## ⚠️ Next.js 16 + Turbopack — Known Issue
33
+ >
34
+ > If you're using **Next.js 16** with Turbopack (default bundler), you may encounter a `createContext is not a function` error during `next build`. This is a **known Payload CMS issue** ([#15429](https://github.com/payloadcms/payload/issues/15429), [#14330](https://github.com/payloadcms/payload/discussions/14330)) — not specific to this plugin.
35
+ >
36
+ > **Workaround** — Add this to your admin page (`src/app/(payload)/admin/[[...segments]]/page.tsx`):
37
+ > ```ts
38
+ > export const dynamic = 'force-dynamic'
39
+ > ```
40
+ >
41
+ > And ensure all `@consilioweb/*` packages are in `transpilePackages` in your `next.config.ts`:
42
+ > ```ts
43
+ > transpilePackages: ['@consilioweb/payload-seo-analyzer', '@consilioweb/admin-nav', /* ...other @consilioweb packages */],
44
+ > ```
45
+ >
46
+ > ✅ **Next.js 15** works without any workaround.
47
+
48
+ ## About
49
+
50
+ > **@consilioweb/payload-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.
51
+
52
+ <table>
53
+ <tr>
54
+ <td align="center" width="25%">
55
+ <img src="https://img.icons8.com/color/96/seo.png" width="50"/><br>
56
+ <b>50+ SEO Checks</b><br>
57
+ <sub>17 rule groups</sub>
58
+ </td>
59
+ <td align="center" width="25%">
60
+ <img src="https://img.icons8.com/color/96/dashboard-layout.png" width="50"/><br>
61
+ <b>9 Admin Views</b><br>
62
+ <sub>Full dashboard suite</sub>
63
+ </td>
64
+ <td align="center" width="25%">
65
+ <img src="https://img.icons8.com/color/96/translation.png" width="50"/><br>
66
+ <b>i18n 39 Languages</b><br>
67
+ <sub>Meta fields UI + FR/EN dashboard</sub>
68
+ </td>
69
+ <td align="center" width="25%">
70
+ <img src="https://img.icons8.com/color/96/api-settings.png" width="50"/><br>
71
+ <b>20+ Endpoints</b><br>
72
+ <sub>REST API</sub>
73
+ </td>
74
+ </tr>
75
+ </table>
76
+
77
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
78
+
79
+ ## Overview
80
+
81
+ `@consilioweb/payload-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.
82
+
83
+ 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.
84
+
85
+ ### Screenshots
86
+
87
+ | SEO Dashboard | Sitemap Audit |
88
+ |:---:|:---:|
89
+ | ![SEO Dashboard](https://raw.githubusercontent.com/pOwn3d/payload-seo-analyzer/main/docs/screenshots/seo-dashboard.png) | ![Sitemap Audit](https://raw.githubusercontent.com/pOwn3d/payload-seo-analyzer/main/docs/screenshots/sitemap-audit.png) |
90
+
91
+ | Editor Sidebar | Configuration |
92
+ |:---:|:---:|
93
+ | ![Editor Sidebar](https://raw.githubusercontent.com/pOwn3d/payload-seo-analyzer/main/docs/screenshots/editor-sidebar.png) | ![Configuration](https://raw.githubusercontent.com/pOwn3d/payload-seo-analyzer/main/docs/screenshots/seo-config.png) |
94
+
95
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
96
+
97
+ ## Table of Contents
98
+
99
+ - [Features](#features)
100
+ - [Installation](#installation)
101
+ - [Quick Start](#quick-start)
102
+ - [Internationalization (i18n)](#internationalization-i18n)
103
+ - [Configuration](#configuration)
104
+ - [Admin Views](#admin-views)
105
+ - [API Endpoints](#api-endpoints)
106
+ - [SEO Rules Reference](#seo-rules-reference)
107
+ - [Collections](#collections)
108
+ - [Fields Added to Collections](#fields-added-to-collections)
109
+ - [Programmatic Usage](#programmatic-usage)
110
+ - [Page Type Detection](#page-type-detection)
111
+ - [Package Exports](#package-exports)
112
+ - [Requirements](#requirements)
113
+ - [Uninstall](#uninstall)
114
+ - [License](#license)
115
+
116
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
117
+
118
+ ## Features
119
+
120
+ ### SEO Analysis Engine (50+ Checks)
121
+
122
+ The core analyzer runs **17 rule groups** covering every aspect of on-page SEO:
123
+
124
+ - **Title** — length (30-60 chars), keyword presence and position, duplicate brand detection, power words, numbers, questions, emotional words
125
+ - **Meta Description** — length (120-160 chars), keyword presence, call-to-action verbs
126
+ - **URL / Slug** — length, format validation, keyword presence, French stop word detection
127
+ - **Headings** — unique H1, keyword in H1/H2, heading hierarchy, H1 vs title differentiation, heading frequency
128
+ - **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
129
+ - **Images** — alt text coverage (80%+ threshold), keyword in alt, image presence and quantity
130
+ - **Linking** — internal links (3+ recommended), external links, generic anchor detection, empty link detection
131
+ - **Social** — OG image, title truncation on social platforms, description length for Facebook/LinkedIn
132
+ - **Schema** — structured data readiness (title + description + image)
133
+ - **Readability** — Flesch FR score, long sentences, long paragraphs, passive voice, transition words, consecutive same-start sentences, long sections without subheadings
134
+ - **Quality** — duplicate/placeholder content detection, substantial content validation
135
+ - **Secondary Keywords** — presence in title, description, content, and H2/H3 headings (up to 3 secondary keywords)
136
+ - **Cornerstone** — enhanced checks for pillar content (1500+ words, 5+ internal links, mandatory keyword)
137
+ - **Freshness** — content age tracking, review dates, year references, thin content aging penalty
138
+ - **Technical** — canonical URL validation, robots meta directives (noindex/nofollow)
139
+ - **Accessibility** — short anchors, alt text quality, empty headings, duplicate adjacent links, all-caps headings, link density ratio, camera filename detection, alt-heading redundancy
140
+ - **E-commerce** — price detection, product description length, image count, brand in title, price in meta, review readiness, availability status
141
+
142
+ ### Bilingual Readability (FR & EN)
143
+
144
+ Locale-adapted readability analysis with different formulas and thresholds per language:
145
+
146
+ | Check | French (Kandel-Moles) | English (Flesch-Kincaid) |
147
+ |-------|----------------------|--------------------------|
148
+ | Flesch pass | >= 40 | >= 60 |
149
+ | Flesch warning | >= 25 | >= 40 |
150
+ | Long sentences | > 25 words | > 20 words |
151
+ | Passive voice max | 15% | 10% |
152
+ | Transition words min | 15% | 20% |
153
+
154
+ **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.
155
+
156
+ **English** uses the standard Flesch-Kincaid formula, English passive voice detection (be-verb + past participle), and 65 English transition words.
157
+
158
+ ### Native Lexical JSON Support
159
+
160
+ Natively parses Payload CMS Lexical rich text JSON structures with:
161
+
162
+ - Recursive text extraction (configurable max depth, default: 50)
163
+ - Heading extraction with tag and text
164
+ - Link extraction (internal/external) with anchor text
165
+ - Image extraction with alt text analysis
166
+ - List detection (ordered/unordered) for featured snippet optimization
167
+ - Support for nested blocks, columns, and all standard Payload block types
168
+
169
+ ### Admin Dashboard Suite (9 Views — FR/EN auto-locale)
170
+
171
+ 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.
172
+
173
+ | View | Path | Description |
174
+ |------|------|-------------|
175
+ | **SEO Dashboard** | `/admin/seo` | Sortable table of all pages/posts with scores, inline editing, bulk actions, filters |
176
+ | **Sitemap Audit** | `/admin/sitemap-audit` | Orphan pages, weak pages, broken internal links, hub detection, link graph analysis |
177
+ | **SEO Configuration** | `/admin/seo-config` | Site name, ignored slugs, disabled rules, custom thresholds, sitemap and breadcrumb settings |
178
+ | **Redirect Manager** | `/admin/redirects` | Full CRUD for 301/302 redirects with CSV import, test tool, and bulk operations |
179
+ | **Cannibalization** | `/admin/cannibalization` | Detect keyword cannibalization across pages sharing the same focus keyword |
180
+ | **Performance** | `/admin/performance` | Google Search Console data import (CSV/XLSX), trend charts, position tracking |
181
+ | **Keyword Research** | `/admin/keyword-research` | Keyword suggestions based on existing content, gap analysis |
182
+ | **Schema Builder** | `/admin/schema-builder` | Visual JSON-LD schema.org structured data generation |
183
+ | **Link Graph** | `/admin/link-graph` | Internal link structure visualization with hub and orphan detection |
184
+
185
+ ### Editor Sidebar Components
186
+
187
+ - **SeoAnalyzer** — Real-time SEO scoring widget in the document editor sidebar with pass/warning/fail indicators, actionable tips, and grouped checks
188
+ - **Score History Chart** — Inline score trend visualization over time
189
+ - **Content Decay Section** — Freshness and aging indicators
190
+ - **Social Preview** — Facebook and Twitter card preview
191
+
192
+ ### Automatic Behaviors
193
+
194
+ - **Auto-redirect on slug change** — Creates a 301 redirect when a document's slug is modified (with redirect chain detection)
195
+ - **Score history tracking** — Records SEO score snapshots on every document save via afterChange hook
196
+ - **Cache warm-up** — Pre-loads collection data on startup and hourly for instant dashboard response
197
+ - **SEO Logs (404 monitoring)** — Tracks 404 errors with hit count, referrer, and user agent for proactive redirect management
198
+
199
+ ### New in v1.7.0
200
+
201
+ - **Granular feature flags** — Disable collections, endpoints, or views independently via `features` config
202
+ - **robots.txt generation** — Dynamic robots.txt endpoint with admin management
203
+ - **XML sitemap generation** — Dynamic sitemap.xml endpoint built from your collections
204
+ - **Custom dashboard translations** — Extend or override dashboard labels via `customTranslations` config or `registerDashboardTranslations()` API
205
+ - **RBAC on destructive endpoints** — Admin role check on all write/delete operations
206
+ - **Security hardening** — SSRF DNS rebinding protection, collection injection protection, timing-safe secret comparison, LRU cache eviction
207
+ - **Shared helpers** — `extractDocContent`, `parseJsonBody`, `fetchAllDocs`, `loadMergedConfig`, `metaGeneration` reduce code duplication across endpoints
208
+
209
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
210
+
211
+ ## Installation
212
+
213
+ ```bash
214
+ pnpm add @consilioweb/payload-seo-analyzer
215
+ ```
216
+
217
+ Or with npm/yarn:
218
+
219
+ ```bash
220
+ npm install @consilioweb/payload-seo-analyzer
221
+ yarn add @consilioweb/payload-seo-analyzer
222
+ ```
223
+
224
+ ### Peer Dependencies
225
+
226
+ The plugin requires Payload CMS 3.x. The following peer dependencies are optional but recommended for full admin UI features:
227
+
228
+ | Package | Version | Required |
229
+ |---------|---------|----------|
230
+ | `payload` | `^3.0.0` | **Yes** |
231
+ | `@payloadcms/next` | `^3.0.0` | Optional (admin views) |
232
+ | `@payloadcms/ui` | `^3.0.0` | Optional (admin UI) |
233
+ | `react` | `^18.0.0 \|\| ^19.0.0` | Optional (admin UI) |
234
+
235
+ > **Note:** For XLSX import in the Performance view, install `xlsx` separately (`pnpm add xlsx`). It is loaded dynamically and not required as a peer dependency.
236
+
237
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
238
+
239
+ ## Quick Start
240
+
241
+ Add the plugin to your `payload.config.ts`:
242
+
243
+ ```ts
244
+ import { buildConfig } from 'payload'
245
+ import { seoAnalyzerPlugin } from '@consilioweb/payload-seo-analyzer'
246
+
247
+ export default buildConfig({
248
+ // ... your existing config
249
+ plugins: [
250
+ seoAnalyzerPlugin({
251
+ collections: ['pages', 'posts'],
252
+ }),
253
+ ],
254
+ })
255
+ ```
256
+
257
+ > **Using alongside `@payloadcms/plugin-seo`?** The export is named `seoAnalyzerPlugin` (not `seoPlugin`) 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.
258
+ >
259
+ > The legacy import `import { seoPlugin } from '@consilioweb/payload-seo-analyzer'` still works for backward compatibility.
260
+
261
+ That's it. The plugin will automatically:
262
+
263
+ 1. Add SEO fields (`focusKeyword`, `focusKeywords`, `isCornerstone`) and the SeoAnalyzer sidebar widget to the specified collections
264
+ 2. Auto-create meta fields (`meta.title`, `meta.description`, `meta.image`) with SERP preview — unless `@payloadcms/plugin-seo` is already handling them
265
+ 3. Create 5 managed collections for score history, performance data, settings, redirects, and 404 logs
266
+ 4. Register 20+ API endpoints under `/api/seo-plugin/`
267
+ 5. Add 9 admin views with a collapsible navigation group
268
+ 6. Attach `beforeChange` (auto-redirect) and `afterChange` (score tracking) hooks to target collections and globals
269
+ 7. Inject meta field translations (39 languages) into Payload's i18n system
270
+ 8. Start background cache warm-up on server init
271
+
272
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
273
+
274
+ ## Internationalization (i18n)
275
+
276
+ The plugin has **three layers of internationalization**:
277
+
278
+ | Layer | Languages | What it covers |
279
+ |-------|-----------|----------------|
280
+ | **SEO Analysis Engine** | FR, EN | 50+ check messages, tips, readability formulas, linguistic analysis |
281
+ | **Admin Dashboard** | FR, EN | All 9 dashboard views, sidebar components, navigation labels (~500 strings) |
282
+ | **Meta Field UI Labels** | 39 languages | Field labels, descriptions, and generate buttons in the Payload admin |
283
+
284
+ ### 1. SEO Analysis Locale
285
+
286
+ Controls the language of SEO check messages and linguistic analysis via the `locale` option:
287
+
288
+ ```ts
289
+ seoAnalyzerPlugin({
290
+ collections: ['pages', 'posts'],
291
+ locale: 'en', // 'fr' (default) | 'en'
292
+ })
293
+ ```
294
+
295
+ | Feature | `locale: 'fr'` (default) | `locale: 'en'` |
296
+ |---------|--------------------------|-----------------|
297
+ | SEO messages & tips | French | English |
298
+ | Readability formula | Kandel-Moles (FR thresholds) | Flesch-Kincaid (EN thresholds) |
299
+ | Passive voice detection | être + participe passé | be-verb + past participle |
300
+ | Transition words | 72 French expressions | 65 English expressions |
301
+ | Stop words in slug | French stop words | English stop words |
302
+ | Action verbs (CTA) | 30 French verbs | 30 English verbs |
303
+ | Power words | 29 French words | 30 English words |
304
+ | Page type detection | FR slugs (`mentions-legales`, `contact`) | EN slugs (`privacy-policy`, `contact-us`) |
305
+ | Question words (title) | `comment`, `pourquoi`, `quand`... | `how`, `why`, `when`... |
306
+
307
+ ### 2. Dashboard Auto-Locale (FR/EN)
308
+
309
+ 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.
310
+
311
+ This works via `useLocale()` from `@payloadcms/ui`. Any locale starting with `en` maps to English; all others default to French.
312
+
313
+ **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).
314
+
315
+ ### 3. Meta Field Labels (39 Languages)
316
+
317
+ 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.
318
+
319
+ 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.
320
+
321
+ ### Backward Compatibility
322
+
323
+ The `locale` option defaults to `'fr'` — existing installations are unaffected. All legacy exports (`calculateFleschFR`, `getStopWordsFR`, `POWER_WORDS_FR`, etc.) remain available as aliases.
324
+
325
+ ### Programmatic Usage with Locale
326
+
327
+ ```ts
328
+ import { analyzeSeo } from '@consilioweb/payload-seo-analyzer'
329
+
330
+ const result = analyzeSeo(input, { locale: 'en' })
331
+ // All messages returned in English, EN readability thresholds applied
332
+ ```
333
+
334
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
335
+
336
+ ## Configuration
337
+
338
+ ### `SeoPluginConfig`
339
+
340
+ ```ts
341
+ seoAnalyzerPlugin({
342
+ // All options are optional — defaults are used if omitted
343
+ collections: ['pages', 'posts'],
344
+ globals: [],
345
+ locale: 'fr',
346
+ tabbedUI: false,
347
+ autoCreateMetaFields: true,
348
+ uploadsCollection: 'media',
349
+ generateTitle: undefined,
350
+ generateDescription: undefined,
351
+ generateImage: undefined,
352
+ generateURL: undefined,
353
+ fields: undefined,
354
+ localeMapping: undefined,
355
+ addDashboardView: true,
356
+ addSitemapAuditView: true,
357
+ disabledRules: [],
358
+ overrideWeights: {},
359
+ thresholds: {},
360
+ localSeoSlugs: [],
361
+ siteName: undefined,
362
+ siteUrl: undefined,
363
+ endpointBasePath: '/seo-plugin',
364
+ trackScoreHistory: true,
365
+ redirectsCollection: 'seo-redirects',
366
+ knownRoutes: [],
367
+ seoLogsSecret: undefined,
368
+ interfaceName: undefined,
369
+
370
+ // v1.7.0 — Feature flags
371
+ features: {
372
+ collections: true, // Auto-create managed collections
373
+ endpoints: true, // Register API endpoints
374
+ views: true, // Register admin views
375
+ robotsTxt: false, // Enable robots.txt generation endpoint
376
+ xmlSitemap: false, // Enable XML sitemap generation endpoint
377
+ },
378
+
379
+ // v1.7.0 — Custom dashboard translations
380
+ customTranslations: {
381
+ en: { 'seo:myKey': 'My custom label' },
382
+ fr: { 'seo:myKey': 'Mon label custom' },
383
+ },
384
+ })
385
+ ```
386
+
387
+ | Option | Type | Default | Description |
388
+ |--------|------|---------|-------------|
389
+ | `collections` | `string[]` | `['pages', 'posts']` | Collections auxquelles ajouter les champs SEO et les hooks |
390
+ | `globals` | `string[]` | `[]` | Globals auxquels ajouter les champs SEO et les hooks |
391
+ | `locale` | `'fr' \| 'en'` | `'fr'` | Langue pour les messages SEO, l'analyse de lisibilité et les vérifications linguistiques |
392
+ | `tabbedUI` | `boolean` | `false` | Organiser les champs en onglets "Content" + "SEO" |
393
+ | `autoCreateMetaFields` | `boolean` | `true` | Créer automatiquement les champs meta (title, description, image) si `@payloadcms/plugin-seo` n'est pas détecté |
394
+ | `uploadsCollection` | `string` | `'media'` | Slug de la collection pour le champ d'upload meta image |
395
+ | `generateTitle` | `function` | `undefined` | Fonction custom pour générer le meta title |
396
+ | `generateDescription` | `function` | `undefined` | Fonction custom pour générer la meta description |
397
+ | `generateImage` | `function` | `undefined` | Fonction custom pour générer la meta image (retourne un ID media ou URL) |
398
+ | `generateURL` | `function` | `undefined` | Fonction custom pour générer l'URL de la page (aperçu SERP) |
399
+ | `fields` | `function` | `undefined` | Surcharger les champs meta par défaut : `({ defaultFields }) => Field[]` |
400
+ | `localeMapping` | `Record<string, 'fr' \| 'en'>` | `undefined` | Mapper les codes locale Payload vers la locale d'analyse (ex: `{ 'fr-FR': 'fr', 'en-US': 'en' }`) |
401
+ | `addDashboardView` | `boolean` | `true` | Enregistrer le dashboard SEO et toutes les vues admin |
402
+ | `addSitemapAuditView` | `boolean` | `true` | Enregistrer la vue d'audit sitemap |
403
+ | `disabledRules` | `RuleGroup[]` | `[]` | Groupes de règles à désactiver entièrement |
404
+ | `overrideWeights` | `Partial<Record<RuleGroup, number>>` | `{}` | Surcharger le poids de tous les checks d'un groupe de règles |
405
+ | `thresholds` | `SeoThresholds` | Voir ci-dessous | Seuils personnalisés pour les vérifications d'analyse |
406
+ | `localSeoSlugs` | `string[]` | `[]` | Slugs supplémentaires reconnus comme pages SEO local |
407
+ | `siteName` | `string` | `undefined` | Nom du site pour la détection de duplication de marque dans les titres |
408
+ | `siteUrl` | `string` | `undefined` | URL de base du site (utilisée pour la validation d'URL canonique, ex: `'https://example.com'`) |
409
+ | `endpointBasePath` | `string` | `'/seo-plugin'` | Préfixe du chemin de base pour tous les endpoints API |
410
+ | `trackScoreHistory` | `boolean` | `true` | Activer la collection d'historique des scores et le hook afterChange de suivi |
411
+ | `redirectsCollection` | `string` | `'seo-redirects'` | Slug de la collection de redirections auto-créée |
412
+ | `knownRoutes` | `string[]` | `[]` | Routes dynamiques qui ne doivent pas être signalées comme liens cassés |
413
+ | `seoLogsSecret` | `string` | `undefined` | Secret partagé pour l'endpoint POST des logs SEO (auth middleware) |
414
+ | `interfaceName` | `string` | `undefined` | Custom TypeScript interface name for the generated meta group type (e.g. `'SharedSEO'`) |
415
+ | `features` | `object` | All `true` | Granular feature flags to disable collections, endpoints, or views (see below) |
416
+ | `features.collections` | `boolean` | `true` | Auto-create managed collections (score history, redirects, settings, SEO logs, performance) |
417
+ | `features.endpoints` | `boolean` | `true` | Register API endpoints under the base path |
418
+ | `features.views` | `boolean` | `true` | Register admin dashboard views |
419
+ | `features.robotsTxt` | `boolean` | `false` | Enable dynamic robots.txt generation endpoint (`GET /api/seo-plugin/robots.txt`) |
420
+ | `features.xmlSitemap` | `boolean` | `false` | Enable dynamic XML sitemap generation endpoint (`GET /api/seo-plugin/sitemap.xml`) |
421
+ | `customTranslations` | `Record<string, Record<string, string>>` | `undefined` | Custom dashboard translations keyed by locale (merged at startup) |
422
+
423
+ ### `SeoThresholds`
424
+
425
+ Tous les seuils sont optionnels. Les valeurs par défaut sont utilisées si omis.
426
+
427
+ | Seuil | Type | Default | Description |
428
+ |-------|------|---------|-------------|
429
+ | `titleLengthMin` | `number` | `30` | Longueur minimale du meta title (caractères) |
430
+ | `titleLengthMax` | `number` | `60` | Longueur maximale du meta title (caractères) |
431
+ | `metaDescLengthMin` | `number` | `120` | Longueur minimale de la meta description |
432
+ | `metaDescLengthMax` | `number` | `160` | Longueur maximale de la meta description |
433
+ | `minWordsGeneric` | `number` | `300` | Nombre minimum de mots pour les pages génériques |
434
+ | `minWordsPost` | `number` | `800` | Nombre minimum de mots pour les articles de blog |
435
+ | `keywordDensityMin` | `number` | `0.5` | Densité minimale du mot-clé (%) |
436
+ | `keywordDensityMax` | `number` | `3` | Densité maximale du mot-clé (%) |
437
+ | `fleschScorePass` | `number` | `40` | Score Flesch FR seuil de réussite |
438
+ | `slugMaxLength` | `number` | `75` | Longueur maximale du slug (caractères) |
439
+
440
+ ### `RuleGroup` Values
441
+
442
+ ```ts
443
+ type RuleGroup =
444
+ | 'title'
445
+ | 'meta-description'
446
+ | 'url'
447
+ | 'headings'
448
+ | 'content'
449
+ | 'images'
450
+ | 'linking'
451
+ | 'social'
452
+ | 'schema'
453
+ | 'readability'
454
+ | 'quality'
455
+ | 'secondary-keywords'
456
+ | 'cornerstone'
457
+ | 'freshness'
458
+ | 'technical'
459
+ | 'accessibility'
460
+ | 'ecommerce'
461
+ ```
462
+
463
+ ### Advanced Configuration Example
464
+
465
+ ```ts
466
+ import { seoAnalyzerPlugin } from '@consilioweb/payload-seo-analyzer'
467
+
468
+ export default buildConfig({
469
+ plugins: [
470
+ seoAnalyzerPlugin({
471
+ collections: ['pages', 'posts', 'products'],
472
+ globals: ['header', 'footer'],
473
+ locale: 'en',
474
+ tabbedUI: true, // Wrap fields in Content + SEO tabs
475
+ siteName: 'My Website',
476
+ endpointBasePath: '/seo',
477
+ knownRoutes: ['blog', 'products', 'categories'],
478
+ localSeoSlugs: ['plumber-paris', 'plumber-lyon'],
479
+ disabledRules: ['social', 'schema'],
480
+ overrideWeights: {
481
+ readability: 1,
482
+ cornerstone: 5,
483
+ },
484
+ thresholds: {
485
+ titleLengthMax: 65,
486
+ minWordsPost: 1000,
487
+ fleschScorePass: 35,
488
+ },
489
+ // Generate functions (called by the "Generate" buttons in meta fields)
490
+ generateTitle: ({ doc }) => `${(doc as any).title} | My Website`,
491
+ generateDescription: ({ doc }) => `Discover ${(doc as any).title} on My Website.`,
492
+ generateURL: ({ doc }) => `https://mywebsite.com/${(doc as any).slug || ''}`,
493
+ // Custom meta fields layout
494
+ fields: ({ defaultFields }) => [
495
+ ...defaultFields,
496
+ { name: 'canonicalUrl', type: 'text', label: 'Canonical URL' },
497
+ ],
498
+ // Map Payload locales to analysis language
499
+ localeMapping: { 'fr-FR': 'fr', 'en-US': 'en' },
500
+ seoLogsSecret: process.env.SEO_LOGS_SECRET,
501
+ }),
502
+ ],
503
+ })
504
+ ```
505
+
506
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
507
+
508
+ ## Admin Views
509
+
510
+ ### SEO Dashboard (`/admin/seo`)
511
+
512
+ The main dashboard displays a sortable, filterable table of all pages and posts with their SEO scores. Features include:
513
+
514
+ - Color-coded score badges (excellent/good/ok/poor)
515
+ - Sortable columns: score, title, word count, focus keyword, H1, OG image, links, readability
516
+ - Quick filters: missing meta, missing H1, low readability
517
+ - Inline editing of meta title and description
518
+ - Bulk actions: export CSV, mark/unmark cornerstone
519
+ - Checkboxes for multi-selection
520
+ - Score trend indicators (up/down arrows)
521
+ - Multi-keyword display
522
+ - Quick links to edit each document
523
+
524
+ ### Sitemap Audit (`/admin/sitemap-audit`)
525
+
526
+ Analyzes your site's internal structure to identify:
527
+
528
+ - **Orphan pages** — pages with no internal links pointing to them
529
+ - **Weak pages** — pages with few incoming links (with anchor text display)
530
+ - **Broken internal links** — links pointing to non-existent pages (with fix suggestions)
531
+ - **Hub pages** — pages with the most outgoing internal links
532
+ - **One-click 301 redirect creation** for broken links
533
+ - **SEO scores** alongside orphan and weak pages
534
+ - **Hover previews** with contextual information
535
+ - **Export** — JSON and CSV download of the full link graph
536
+
537
+ ### SEO Configuration (`/admin/seo-config`)
538
+
539
+ Centralized settings management:
540
+
541
+ - Site name (for brand duplicate detection)
542
+ - Ignored slugs (excluded from audits)
543
+ - Disabled rule groups
544
+ - Custom thresholds (title length, word counts, etc.)
545
+ - Sitemap configuration (excluded slugs, change frequency, priority overrides)
546
+ - Breadcrumb configuration (separator, home label, display options)
547
+
548
+ ### Redirect Manager (`/admin/redirects`)
549
+
550
+ Full redirect management with:
551
+
552
+ - CRUD operations for 301/302 redirects
553
+ - CSV import for bulk redirect creation
554
+ - Redirect test tool (verify where a URL redirects)
555
+ - Bulk delete operations
556
+
557
+ ### Cannibalization Detection (`/admin/cannibalization`)
558
+
559
+ Identifies pages competing for the same keywords by detecting documents that share identical focus keywords.
560
+
561
+ ### Performance Tracking (`/admin/performance`)
562
+
563
+ Import and visualize Google Search Console data:
564
+
565
+ - CSV and XLSX file import (supports French GSC headers)
566
+ - Click, impression, CTR, and position tracking
567
+ - Trend visualization over time
568
+ - Per-URL and per-query breakdowns
569
+
570
+ ### Keyword Research (`/admin/keyword-research`)
571
+
572
+ Keyword analysis based on your existing content:
573
+
574
+ - Keyword suggestions derived from current pages
575
+ - Gap analysis to identify missing keyword coverage
576
+
577
+ ### Schema Builder (`/admin/schema-builder`)
578
+
579
+ Visual tool for generating JSON-LD structured data (schema.org) markup for your pages.
580
+
581
+ ### Link Graph (`/admin/link-graph`)
582
+
583
+ Interactive visualization of your site's internal linking structure:
584
+
585
+ - Node-based graph representation
586
+ - Hub and orphan page identification
587
+ - Link equity flow analysis
588
+
589
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
590
+
591
+ ## API Endpoints
592
+
593
+ All endpoints are prefixed with the configured `endpointBasePath` (default: `/seo-plugin`). All endpoints require an authenticated admin user unless noted otherwise.
594
+
595
+ | Method | Path | Description |
596
+ |--------|------|-------------|
597
+ | `GET` `POST` | `/validate` | Run SEO analysis on a document |
598
+ | `GET` | `/check-keyword` | Check for keyword duplication across collections |
599
+ | `GET` | `/audit` | Full site-wide SEO audit |
600
+ | `GET` | `/history` | Score history data for trend charts |
601
+ | `GET` | `/sitemap-audit` | Sitemap structure audit |
602
+ | `GET` `PATCH` | `/settings` | Read or update SEO settings |
603
+ | `POST` | `/suggest-links` | Internal link suggestions for a page |
604
+ | `POST` | `/create-redirect` | Create a single redirect entry |
605
+ | `GET` `POST` `PATCH` `DELETE` | `/redirects` | Full CRUD for redirect management |
606
+ | `POST` | `/ai-generate` | AI-powered meta title/description generation |
607
+ | `GET` | `/cannibalization` | Detect keyword cannibalization |
608
+ | `POST` | `/external-links` | Check external link status (live HTTP checks with SSRF protection) |
609
+ | `GET` | `/sitemap-config` | Sitemap configuration data |
610
+ | `GET` `POST` | `/performance` | Read or import performance data (CSV/XLSX) |
611
+ | `GET` | `/keyword-research` | Keyword suggestions and gap analysis |
612
+ | `GET` | `/breadcrumb` | Breadcrumb configuration and data |
613
+ | `GET` | `/link-graph` | Internal link graph data |
614
+ | `GET` `POST` `DELETE` | `/seo-logs` | 404 log management (POST supports secret-header auth) |
615
+
616
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
617
+
618
+ ## SEO Rules Reference
619
+
620
+ ### Scoring Algorithm
621
+
622
+ Each check has a **weight** (1-5) and produces a **status** (`pass`, `warning`, or `fail`):
623
+
624
+ - **Pass** — earns 100% of weight points
625
+ - **Warning** — earns 50% of weight points
626
+ - **Fail** — earns 0 points
627
+
628
+ **Final score** = `round(earnedPoints / maxPoints * 100)`
629
+
630
+ | Level | Score Range |
631
+ |-------|-------------|
632
+ | Excellent | >= 91 |
633
+ | Good | >= 71 |
634
+ | OK | >= 41 |
635
+ | Poor | < 41 |
636
+
637
+ ### Complete Check List
638
+
639
+ <details>
640
+ <summary><strong>Title (9 checks)</strong></summary>
641
+
642
+ | Check ID | Weight | Category | Description |
643
+ |----------|--------|----------|-------------|
644
+ | `title-missing` | 3 | Critical | Meta title is present |
645
+ | `title-length` | 3 | Critical | Title between 30-60 characters |
646
+ | `title-keyword` | 3 | Critical | Focus keyword in title |
647
+ | `title-keyword-position` | 2 | Important | Keyword in first half of title |
648
+ | `title-duplicate-brand` | 2 | Important | No duplicate brand name |
649
+ | `title-power-words` | 1 | Bonus | Contains power words |
650
+ | `title-has-number` | 1 | Bonus | Contains a number (+36% CTR) |
651
+ | `title-is-question` | 1 | Bonus | Question format (Featured Snippet friendly) |
652
+ | `title-sentiment` | 1 | Bonus | Contains emotional words |
653
+
654
+ </details>
655
+
656
+ <details>
657
+ <summary><strong>Meta Description (4 checks)</strong></summary>
658
+
659
+ | Check ID | Weight | Category | Description |
660
+ |----------|--------|----------|-------------|
661
+ | `meta-desc-missing` | 3 | Critical | Meta description is present |
662
+ | `meta-desc-length` | 3 | Critical | Length between 120-160 characters |
663
+ | `meta-desc-keyword` | 3 | Critical | Focus keyword in description |
664
+ | `meta-desc-cta` | 2 | Important | Contains action verb or CTA pattern |
665
+
666
+ </details>
667
+
668
+ <details>
669
+ <summary><strong>URL / Slug (5 checks)</strong></summary>
670
+
671
+ | Check ID | Weight | Category | Description |
672
+ |----------|--------|----------|-------------|
673
+ | `slug-missing` | 2 | Important | Slug is defined |
674
+ | `slug-length` | 2 | Important | Slug under 75 characters |
675
+ | `slug-format` | 2 | Important | Lowercase, no special characters |
676
+ | `slug-keyword` | 2 | Important | Focus keyword in slug |
677
+ | `slug-stopwords` | 1 | Bonus | No stop words (FR or EN based on locale) |
678
+
679
+ </details>
680
+
681
+ <details>
682
+ <summary><strong>Headings (6 checks)</strong></summary>
683
+
684
+ | Check ID | Weight | Category | Description |
685
+ |----------|--------|----------|-------------|
686
+ | `h1-missing` / `h1-unique` | 2 | Important | Exactly one H1 per page |
687
+ | `h1-keyword` | 2 | Important | Keyword in H1 |
688
+ | `heading-hierarchy` | 2 | Important | Proper heading hierarchy (no level skip) |
689
+ | `h2-keyword` | 2 | Important | Keyword in at least one H2 |
690
+ | `heading-frequency` | 1 | Bonus | One subheading every ~300 words |
691
+ | `h1-title-different` | 1 | Important | H1 differs from meta title |
692
+
693
+ </details>
694
+
695
+ <details>
696
+ <summary><strong>Content (7 checks)</strong></summary>
697
+
698
+ | Check ID | Weight | Category | Description |
699
+ |----------|--------|----------|-------------|
700
+ | `content-wordcount` | 2 | Important | Meets minimum word count by page type |
701
+ | `content-keyword-intro` | 2 | Important | Keyword in first paragraph |
702
+ | `content-keyword-density` | 2-3 | Important/Critical | Density between 0.5%-2.5% |
703
+ | `content-no-placeholder` | 3 | Critical | No lorem ipsum, TODO, or placeholders |
704
+ | `content-thin` | 2 | Important | Not thin content (>100 words) |
705
+ | `content-keyword-distribution` | 2 | Important | Keyword in 2+ of 3 content tiers |
706
+ | `content-has-lists` | 1 | Bonus | Contains ordered/unordered lists |
707
+
708
+ </details>
709
+
710
+ <details>
711
+ <summary><strong>Images (4 checks)</strong></summary>
712
+
713
+ | Check ID | Weight | Category | Description |
714
+ |----------|--------|----------|-------------|
715
+ | `images-alt` | 2 | Important | Alt text on 80%+ of images |
716
+ | `images-alt-keyword` | 1 | Bonus | Keyword in at least one alt text |
717
+ | `images-present` | 2 | Important | At least one image |
718
+ | `images-quantity` | 1-2 | Bonus/Important | Multiple images for posts |
719
+
720
+ </details>
721
+
722
+ <details>
723
+ <summary><strong>Linking (4 checks)</strong></summary>
724
+
725
+ | Check ID | Weight | Category | Description |
726
+ |----------|--------|----------|-------------|
727
+ | `linking-internal` | 2 | Important | At least one internal link (3+ ideal) |
728
+ | `linking-external` | 1 | Bonus | At least one external link |
729
+ | `linking-generic-anchors` | 2 | Important | No generic anchor text |
730
+ | `linking-empty` | 2 | Important | No empty links |
731
+
732
+ </details>
733
+
734
+ <details>
735
+ <summary><strong>Social (3 checks)</strong></summary>
736
+
737
+ | Check ID | Weight | Category | Description |
738
+ |----------|--------|----------|-------------|
739
+ | `social-og-image` | 2 | Important | OG/meta image defined |
740
+ | `social-title-truncation` | 1 | Bonus | Title within social platform limits (~65 chars) |
741
+ | `social-desc-length` | 1 | Bonus | Description within Facebook/LinkedIn limits (~155 chars) |
742
+
743
+ </details>
744
+
745
+ <details>
746
+ <summary><strong>Schema (1 check)</strong></summary>
747
+
748
+ | Check ID | Weight | Category | Description |
749
+ |----------|--------|----------|-------------|
750
+ | `schema-readiness` | 1 | Bonus | Page has enough metadata for JSON-LD generation |
751
+
752
+ </details>
753
+
754
+ <details>
755
+ <summary><strong>Readability (7 checks)</strong></summary>
756
+
757
+ | Check ID | Weight | Category | Description |
758
+ |----------|--------|----------|-------------|
759
+ | `readability-flesch` | 2 | Important | Flesch reading ease (FR: >= 40, EN: >= 60) |
760
+ | `readability-long-sentences` | 2 | Important | Long sentence ratio < 30% (FR: >25 words, EN: >20 words) |
761
+ | `readability-long-paragraphs` | 2 | Important | No paragraphs over 150 words |
762
+ | `readability-passive` | 2 | Important | Passive voice ratio (FR: < 15%, EN: < 10%) |
763
+ | `readability-transitions` | 1 | Bonus | Transition words (FR: 15%+, EN: 20%+) |
764
+ | `readability-consecutive-starts` | 1 | Bonus | No 3+ consecutive sentences with same first word |
765
+ | `readability-long-sections` | 2 | Important | No sections >400 words without subheadings |
766
+
767
+ </details>
768
+
769
+ <details>
770
+ <summary><strong>Quality (2 checks)</strong></summary>
771
+
772
+ | Check ID | Weight | Category | Description |
773
+ |----------|--------|----------|-------------|
774
+ | `quality-no-duplicate` | 3 | Critical | No duplicate or generic content |
775
+ | `quality-substantial` | 3 | Critical | Enough content substance (>50 words fail, >200 warning) |
776
+
777
+ </details>
778
+
779
+ <details>
780
+ <summary><strong>Secondary Keywords (4 checks per keyword, up to 3 keywords)</strong></summary>
781
+
782
+ | Check ID | Weight | Category | Description |
783
+ |----------|--------|----------|-------------|
784
+ | `secondary-kw-title-*` | 1 | Bonus | Secondary keyword in title |
785
+ | `secondary-kw-desc-*` | 1 | Bonus | Secondary keyword in description |
786
+ | `secondary-kw-content-*` | 1 | Bonus | Secondary keyword in content |
787
+ | `secondary-kw-heading-*` | 1 | Bonus | Secondary keyword in H2/H3 |
788
+
789
+ </details>
790
+
791
+ <details>
792
+ <summary><strong>Cornerstone (4 checks, only when isCornerstone is true)</strong></summary>
793
+
794
+ | Check ID | Weight | Category | Description |
795
+ |----------|--------|----------|-------------|
796
+ | `cornerstone-wordcount` | 4 | Important | 1500+ words for pillar content |
797
+ | `cornerstone-internal-links` | 4 | Important | 5+ internal links |
798
+ | `cornerstone-focus-keyword` | 5 | Critical | Focus keyword is defined |
799
+ | `cornerstone-meta-description` | 5 | Critical | Meta description is present and optimized |
800
+
801
+ </details>
802
+
803
+ <details>
804
+ <summary><strong>Freshness (4 checks)</strong></summary>
805
+
806
+ | Check ID | Weight | Category | Description |
807
+ |----------|--------|----------|-------------|
808
+ | `freshness-age` | 1-3 | Bonus/Important | Content updated within 6/12 months |
809
+ | `freshness-reviewed` | 2 | Bonus | Content reviewed within 6 months |
810
+ | `freshness-year-ref` | 2 | Important | Current year referenced in content |
811
+ | `freshness-thin-aging` | 3 | Important | Thin + old content penalty |
812
+
813
+ </details>
814
+
815
+ <details>
816
+ <summary><strong>Technical (3 checks)</strong></summary>
817
+
818
+ | Check ID | Weight | Category | Description |
819
+ |----------|--------|----------|-------------|
820
+ | `canonical-*` | 2 | Important | Canonical URL is valid and correctly set |
821
+ | `robots-noindex` | 2-3 | Important/Critical | Noindex directive detection |
822
+ | `robots-nofollow` | 2 | Important | Nofollow directive detection |
823
+
824
+ </details>
825
+
826
+ <details>
827
+ <summary><strong>Accessibility (8 checks)</strong></summary>
828
+
829
+ | Check ID | Weight | Category | Description |
830
+ |----------|--------|----------|-------------|
831
+ | `a11y-short-anchors` | 2 | Important | No links with text under 3 characters |
832
+ | `a11y-alt-quality` | 2 | Important | No generic or filename-based alt texts |
833
+ | `a11y-empty-headings` | 3 | Critical | No empty heading tags |
834
+ | `a11y-duplicate-links` | 1 | Bonus | No adjacent duplicate links |
835
+ | `a11y-all-caps` | 1 | Bonus | No all-caps headings |
836
+ | `a11y-link-density` | 2 | Important | Link text ratio under 30% of content |
837
+ | `a11y-image-filename` | 2 | Important | No camera default filenames in alt |
838
+ | `a11y-alt-duplicates-context` | 1 | Bonus | Alt text differs from adjacent headings |
839
+
840
+ </details>
841
+
842
+ <details>
843
+ <summary><strong>E-commerce (7 checks, only when isProduct is true)</strong></summary>
844
+
845
+ | Check ID | Weight | Category | Description |
846
+ |----------|--------|----------|-------------|
847
+ | `product-price-mentioned` | 2 | Important | Price visible in content |
848
+ | `product-short-description` | 2 | Important | Description >= 100 words |
849
+ | `product-has-images` | 3 | Critical | At least 2 product images |
850
+ | `product-title-includes-brand` | 1 | Bonus | Brand/keyword in meta title |
851
+ | `product-meta-includes-price` | 1 | Bonus | Price in meta description |
852
+ | `product-review-readiness` | 1 | Bonus | Review/rating content detected |
853
+ | `product-availability` | 2 | Important | Availability status mentioned |
854
+
855
+ </details>
856
+
857
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
858
+
859
+ ## Collections
860
+
861
+ The plugin automatically creates and manages these collections (all hidden from admin nav, managed via plugin views):
862
+
863
+ | Collection | Slug | Description |
864
+ |------------|------|-------------|
865
+ | **SEO Score History** | `seo-score-history` | Score snapshots per document (ID, collection, score, level, word count, keyword, checks summary, date) |
866
+ | **SEO Performance** | `seo-performance` | Search Console data (URL, query, clicks, impressions, CTR, position, date, source) |
867
+ | **SEO Settings** | `seo-settings` | Site-wide config (site name, ignored slugs, disabled rules, thresholds, sitemap config, breadcrumb config) |
868
+ | **SEO Redirects** | `seo-redirects` | 301/302 redirect rules (from, to, type). Slug is configurable via `redirectsCollection` |
869
+ | **SEO Logs** | `seo-logs` | 404 error tracking (URL, type, hit count, last seen, referrer, user agent, ignored flag) |
870
+
871
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
872
+
873
+ ## Fields Added to Collections
874
+
875
+ The plugin adds the following fields to each target collection specified in `collections`:
876
+
877
+ ### SEO Analyzer Fields (sidebar)
878
+
879
+ | Field | Type | Location | Description |
880
+ |-------|------|----------|-------------|
881
+ | `isCornerstone` | `checkbox` | Sidebar | Marks the document as pillar/cornerstone content (triggers enhanced checks) |
882
+ | `focusKeyword` | `text` | Sidebar | Primary SEO focus keyword for analysis |
883
+ | `seoAnalyzer` | `ui` | Sidebar | Real-time SEO analysis widget with score, checks, and actionable tips |
884
+ | `focusKeywords` | `array` (max 3) | Collapsible group | Secondary focus keywords for additional coverage |
885
+
886
+ ### Meta Fields (auto-created)
887
+
888
+ 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.
889
+
890
+ | Field | Type | Description |
891
+ |-------|------|-------------|
892
+ | `meta._overview` | `ui` | Completeness indicator (0/3 to 3/3 — title, description, image) |
893
+ | `meta.title` | `text` | Meta title with character counter (30-60), progress bar, and "Generate" button |
894
+ | `meta.description` | `textarea` | Meta description with character counter (120-160) and "Generate" button |
895
+ | `meta.image` | `upload` | Meta/OG image with status indicator and optional "Generate" button |
896
+ | `meta._preview` | `ui` | Google SERP preview (desktop + mobile toggle, Google 2025 styling) |
897
+
898
+ > **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.
899
+
900
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
901
+
902
+ ## Programmatic Usage
903
+
904
+ The analyzer can be used independently of the Payload plugin system:
905
+
906
+ ```ts
907
+ import { analyzeSeo } from '@consilioweb/payload-seo-analyzer'
908
+ import type { SeoInput, SeoConfig } from '@consilioweb/payload-seo-analyzer'
909
+
910
+ const input: SeoInput = {
911
+ metaTitle: 'My Page Title - Brand',
912
+ metaDescription: 'A comprehensive description of my page for search engines...',
913
+ slug: 'my-page',
914
+ focusKeyword: 'my keyword',
915
+ heroTitle: 'Welcome to My Page',
916
+ heroRichText: { /* Lexical JSON root node */ },
917
+ blocks: [ /* Payload layout blocks */ ],
918
+ content: { /* Lexical JSON for posts */ },
919
+ isPost: false,
920
+ isProduct: false,
921
+ isCornerstone: false,
922
+ updatedAt: '2025-06-01T00:00:00Z',
923
+ }
924
+
925
+ const config: SeoConfig = {
926
+ siteName: 'Brand',
927
+ localSeoSlugs: ['paris', 'lyon'],
928
+ disabledRules: ['social'],
929
+ thresholds: { minWordsPost: 1000 },
930
+ }
931
+
932
+ const result = analyzeSeo(input, config)
933
+ // {
934
+ // score: 78,
935
+ // level: 'good',
936
+ // checks: [
937
+ // { id: 'title-length', status: 'pass', message: '...', weight: 3, ... },
938
+ // { id: 'content-wordcount', status: 'warning', message: '...', weight: 2, ... },
939
+ // ...
940
+ // ]
941
+ // }
942
+ ```
943
+
944
+ ### Exported Helpers
945
+
946
+ The package re-exports utility functions for advanced use cases:
947
+
948
+ ```ts
949
+ import {
950
+ // Lexical JSON parsing
951
+ extractTextFromLexical,
952
+ extractHeadingsFromLexical,
953
+ extractLinksFromLexical,
954
+ extractImagesFromLexical,
955
+ extractLinkUrlsFromLexical,
956
+ extractListsFromLexical,
957
+ checkImagesInBlocks,
958
+
959
+ // Text analysis (bilingual — pass locale: 'fr' | 'en')
960
+ countWords,
961
+ countSentences, // countSentences(text, locale?)
962
+ countSyllablesFR, // French syllable counter
963
+ countSyllablesEN, // English syllable counter
964
+ calculateFlesch, // calculateFlesch(text, locale) — Kandel-Moles (FR) or Flesch-Kincaid (EN)
965
+ calculateFleschFR, // Legacy alias for calculateFlesch(text, 'fr')
966
+ detectPassiveVoice, // detectPassiveVoice(sentence, locale?)
967
+ hasTransitionWord, // hasTransitionWord(sentence, locale?)
968
+ checkHeadingHierarchy,
969
+ countLongSections,
970
+
971
+ // Keyword utilities
972
+ normalizeForComparison,
973
+ slugifyKeyword,
974
+ keywordMatchesText,
975
+ countKeywordOccurrences,
976
+
977
+ // Page type detection (bilingual)
978
+ detectPageType, // detectPageType(slug, collection?, extra?, locale?)
979
+
980
+ // Bilingual constant accessors — pass locale: 'fr' | 'en'
981
+ getStopWords, // getStopWords(locale)
982
+ getActionVerbs, // getActionVerbs(locale)
983
+ getPowerWords, // getPowerWords(locale)
984
+ getGenericAnchors, // getGenericAnchors(locale)
985
+ getLegalSlugs, // getLegalSlugs(locale)
986
+ getUtilitySlugs, // getUtilitySlugs(locale)
987
+ getEvergreenSlugs, // getEvergreenSlugs(locale)
988
+ getStopWordCompounds, // getStopWordCompounds(locale)
989
+
990
+ // Legacy aliases (backward compat)
991
+ getStopWordsFR, // = getStopWords('fr')
992
+ getActionVerbsFR, // = getActionVerbs('fr')
993
+ POWER_WORDS_FR, // = POWER_WORDS.fr
994
+ isStopWordInCompoundExpression,
995
+
996
+ // Locale-specific thresholds
997
+ FLESCH_THRESHOLDS, // { fr: { pass: 40, warn: 25 }, en: { pass: 60, warn: 40 } }
998
+ READABILITY_THRESHOLDS, // { fr: { longSentenceWords: 25, ... }, en: { longSentenceWords: 20, ... } }
999
+
1000
+ // Constants (thresholds, limits)
1001
+ TITLE_LENGTH_MIN, // 30
1002
+ TITLE_LENGTH_MAX, // 60
1003
+ META_DESC_LENGTH_MIN, // 120
1004
+ META_DESC_LENGTH_MAX, // 160
1005
+ MIN_WORDS_POST, // 800
1006
+ MIN_WORDS_GENERIC, // 300
1007
+ SCORE_EXCELLENT, // 91
1008
+ SCORE_GOOD, // 71
1009
+ SCORE_OK, // 41
1010
+ // ... and more
1011
+ } from '@consilioweb/payload-seo-analyzer'
1012
+ ```
1013
+
1014
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
1015
+
1016
+ ## Page Type Detection
1017
+
1018
+ The analyzer automatically adapts thresholds and check severity based on the detected page type:
1019
+
1020
+ | Page Type | Detection Logic (FR) | Detection Logic (EN) | Adapted Behavior |
1021
+ |-----------|---------------------|----------------------|------------------|
1022
+ | `blog` | `isPost: true` | `isPost: true` | Higher word count threshold (800 words) |
1023
+ | `home` | Slug is `home` or empty | Slug is `home` or empty | Standard checks |
1024
+ | `contact` | `contact` | `contact`, `contact-us`, `get-in-touch` | Relaxed: images optional, external links optional, freshness lenient |
1025
+ | `form` | `formulaire`, `devis`, `inscription` | `quote`, `signup`, `register`, `apply` | Relaxed: word count min 150, images optional |
1026
+ | `legal` | `mentions-legales`, `cgv`, `politique-de-confidentialite` | `privacy-policy`, `terms`, `tos`, `gdpr`, `cookies` | Relaxed: word count min 200, images optional, freshness 24 months |
1027
+ | `local-seo` | Matches configured `localSeoSlugs` | Matches configured `localSeoSlugs` | Standard checks with local SEO context |
1028
+ | `service` | `service`, `prestation` | `services`, `our-services` | Standard checks |
1029
+ | `resource` | `ressource`, `guide`, `tutoriel` | `resources`, `guide`, `tutorial` | Standard checks |
1030
+ | `agency` | `agence`, `a-propos`, `equipe` | `about`, `about-us`, `team` | Standard checks |
1031
+ | `generic` | Default fallback | Default fallback | Standard checks (300 words min) |
1032
+
1033
+ > **Note:** Page type detection checks both FR and EN slug patterns regardless of locale, so a French site with an `about` slug will still be correctly detected.
1034
+
1035
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
1036
+
1037
+ ## Package Exports
1038
+
1039
+ The package provides three entry points for different use contexts:
1040
+
1041
+ ```ts
1042
+ // Main entry — plugin, analyzer, types, helpers, constants
1043
+ import {
1044
+ seoAnalyzerPlugin, analyzeSeo, seoFields, metaFields,
1045
+ resolveAnalysisLocale, fetchAllDocs, createGenerateHandler,
1046
+ } from '@consilioweb/payload-seo-analyzer'
1047
+ import type { GenerateFnArgs, MetaFieldsConfig } from '@consilioweb/payload-seo-analyzer'
1048
+
1049
+ // Client components — React components for Payload admin UI
1050
+ import {
1051
+ SeoAnalyzerField,
1052
+ SeoNavLink,
1053
+ ScoreHistoryChart,
1054
+ ContentDecaySection,
1055
+ SeoSocialPreview,
1056
+ // Meta field components (used internally, also available for custom layouts)
1057
+ MetaTitleField,
1058
+ MetaDescriptionField,
1059
+ MetaImageField,
1060
+ OverviewField,
1061
+ SerpPreview,
1062
+ } from '@consilioweb/payload-seo-analyzer/client'
1063
+
1064
+ // Server views — admin views wrapped in DefaultTemplate
1065
+ import {
1066
+ SeoView,
1067
+ SitemapAuditView,
1068
+ SeoConfigView,
1069
+ RedirectManagerView,
1070
+ CannibalizationView,
1071
+ PerformanceView,
1072
+ KeywordResearchView,
1073
+ SchemaBuilderView,
1074
+ LinkGraphView,
1075
+ } from '@consilioweb/payload-seo-analyzer/views'
1076
+ ```
1077
+
1078
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
1079
+
1080
+ ## Requirements
1081
+
1082
+ - **Node.js** >= 18
1083
+ - **Payload CMS** 3.x
1084
+ - **React** 18.x or 19.x (for admin UI components)
1085
+ - **Database**: Any Payload-supported adapter (SQLite, PostgreSQL, MongoDB)
1086
+
1087
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
1088
+
1089
+ ## Uninstall
1090
+
1091
+ One command handles everything — code cleanup, package removal, and importmap regeneration:
1092
+
1093
+ ```bash
1094
+ npx seo-analyzer-uninstall
1095
+ ```
1096
+
1097
+ The script automatically:
1098
+ 1. Scans `src/` and removes all `import` statements and `seoAnalyzerPlugin()` / `seoPlugin()` calls
1099
+ 2. Runs `pnpm remove @consilioweb/payload-seo-analyzer` (detects your package manager)
1100
+ 3. Regenerates the Payload importmap
1101
+
1102
+ No manual editing needed.
1103
+
1104
+ ### What happens to your data?
1105
+
1106
+ **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.
1107
+
1108
+ When you remove the plugin:
1109
+
1110
+ | What | Status | Action needed |
1111
+ |------|--------|---------------|
1112
+ | 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 |
1113
+ | Fields added to your collections (`focusKeyword`, `focusKeywords`, `isCornerstone`) | **Data remains in DB** | Columns/fields are ignored by Payload but stay in storage |
1114
+ | Admin views & API endpoints | **Removed automatically** | No action needed |
1115
+ | Hooks (auto-redirect, score tracking) | **Removed automatically** | No action needed |
1116
+
1117
+ ### Full cleanup (optional)
1118
+
1119
+ If you want to remove all plugin data from your database:
1120
+
1121
+ **SQLite:**
1122
+ ```sql
1123
+ DROP TABLE IF EXISTS seo_score_history;
1124
+ DROP TABLE IF EXISTS seo_performance;
1125
+ DROP TABLE IF EXISTS seo_settings;
1126
+ DROP TABLE IF EXISTS seo_redirects;
1127
+ DROP TABLE IF EXISTS seo_logs;
1128
+ ```
1129
+
1130
+ **PostgreSQL:**
1131
+ ```sql
1132
+ DROP TABLE IF EXISTS "seo-score-history" CASCADE;
1133
+ DROP TABLE IF EXISTS "seo-performance" CASCADE;
1134
+ DROP TABLE IF EXISTS "seo-settings" CASCADE;
1135
+ DROP TABLE IF EXISTS "seo-redirects" CASCADE;
1136
+ DROP TABLE IF EXISTS "seo-logs" CASCADE;
1137
+ ```
1138
+
1139
+ **MongoDB:**
1140
+ ```js
1141
+ db.getCollection('seo-score-history').drop()
1142
+ db.getCollection('seo-performance').drop()
1143
+ db.getCollection('seo-settings').drop()
1144
+ db.getCollection('seo-redirects').drop()
1145
+ db.getCollection('seo-logs').drop()
1146
+ ```
1147
+
1148
+ > **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.
1149
+
1150
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
1151
+
1152
+ ## Roadmap
1153
+
1154
+ - Google Search Console API integration (OAuth2 + automatic import)
1155
+ - Core Web Vitals monitoring (LCP, FID, CLS)
1156
+ - Hreflang / multi-locale validation
1157
+ - SERP position tracking & competitor analysis
1158
+ - Content freshness alerts & auto-notifications
1159
+ - Structured data validation against schema.org
1160
+ - Per-block SEO scoring (feedback per Payload block)
1161
+ - Multi-language analysis rules (beyond FR/EN)
1162
+ - Bulk auto-fix for common SEO issues
1163
+ - AI-powered content optimization suggestions
1164
+
1165
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
1166
+
1167
+ ## ☕ Support
1168
+
1169
+ If this plugin saves you time, consider buying me a coffee!
1170
+
1171
+ <a href="https://buymeacoffee.com/pown3d">
1172
+ <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="217" />
1173
+ </a>
1174
+
1175
+ ## License
1176
+
1177
+ [MIT](LICENSE)
1178
+
1179
+ <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="line">
1180
+
1181
+ <div align="center">
1182
+
1183
+ ### Author
1184
+
1185
+ **Made with passion by [ConsilioWEB](https://consilioweb.fr)**
1186
+
1187
+ <a href="https://www.linkedin.com/in/christophe-lopez/">
1188
+ <img src="https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white" alt="LinkedIn">
1189
+ </a>
1190
+ <a href="https://github.com/pOwn3d">
1191
+ <img src="https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white" alt="GitHub">
1192
+ </a>
1193
+ <a href="https://consilioweb.fr">
1194
+ <img src="https://img.shields.io/badge/Website-consilioweb.fr-3B82F6?style=for-the-badge&logo=google-chrome&logoColor=white" alt="Website">
1195
+ </a>
1196
+
1197
+ <br><br>
1198
+
1199
+ <img src="https://capsule-render.vercel.app/api?type=waving&color=gradient&customColorList=6,11,20&height=100&section=footer" width="100%"/>
1200
+
1201
+ </div>