@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/LICENSE +21 -0
- package/README.md +1201 -0
- package/dist/client.cjs +19286 -0
- package/dist/client.d.cts +133 -0
- package/dist/client.d.ts +133 -0
- package/dist/client.js +19261 -0
- package/dist/index.cjs +11836 -0
- package/dist/index.d.cts +1416 -0
- package/dist/index.d.ts +1416 -0
- package/dist/index.js +11752 -0
- package/dist/views.cjs +216 -0
- package/dist/views.d.cts +67 -0
- package/dist/views.d.ts +67 -0
- package/dist/views.js +206 -0
- package/package.json +122 -0
- package/scripts/uninstall.mjs +282 -0
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¢er=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
|
+
|  |  |
|
|
90
|
+
|
|
91
|
+
| Editor Sidebar | Configuration |
|
|
92
|
+
|:---:|:---:|
|
|
93
|
+
|  |  |
|
|
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§ion=footer" width="100%"/>
|
|
1200
|
+
|
|
1201
|
+
</div>
|