@consilioweb/payload-spellcheck
v0.13.1
Published
Payload CMS spellcheck plugin — LanguageTool + Claude AI fallback, dashboard, sidebar field, auto-check on save
Downloads
129
Maintainers
Readme
[!IMPORTANT]
⚠️ Next.js 16 + Turbopack — Known Issue
If you're using Next.js 16 with Turbopack (default bundler), you may encounter a
createContext is not a functionerror duringnext build. This is a known Payload CMS issue (#15429, #14330) — not specific to this plugin.Workaround — Add this to your admin page (
src/app/(payload)/admin/[[...segments]]/page.tsx):export const dynamic = 'force-dynamic'And ensure all
@consilioweb/*packages are intranspilePackagesin yournext.config.ts:transpilePackages: ['@consilioweb/seo-analyzer', '@consilioweb/admin-nav', /* ...other @consilioweb packages */],✅ Next.js 15 works without any workaround.
About
@consilioweb/payload-spellcheck is a Payload CMS 3 plugin that adds real-time spelling and grammar checking to your admin panel. Powered by LanguageTool with optional Claude AI semantic analysis.
| Feature | Description |
|---------|-------------|
| Dashboard | Full admin view at /admin/spellcheck with bulk scanning |
| Sidebar Field | Real-time spellcheck score + issues in the editor |
| Auto-check | Fire-and-forget hook checks content on every save |
| One-click Fix | Apply corrections directly in Lexical JSON |
| LanguageTool | Grammar, spelling, punctuation via free API |
| Claude AI | Optional semantic analysis (coherence, tone, phrasing) |
| Custom Dictionary | Whitelist tech terms, brand names, proper nouns |
| Dynamic Dictionary | Add/remove words from admin UI, persists in DB |
| Offset-based Fix | Precise corrections using LanguageTool offsets |
| Ignore Issues | Dismiss false positives (persists across reloads) |
| i18n | French and English UI translations |
Table of Contents
- Features
- Installation
- Quick Start
- Configuration
- Admin Views
- Dynamic Dictionary
- API Endpoints
- Engine
- Package Exports
- Uninstall
- Changelog
- License
Features
Dashboard (/admin/spellcheck)
- Tabbed interface — Results tab + Dictionary tab
- Selective scan — Check specific pages or all documents at once
- Checkbox selection — Pick individual documents to scan
- Collection filter — Filter by collection (pages, posts, etc.)
- Sortable table — Sort by score, issues, word count, last checked
- Expandable rows — Click a document to see all issues inline
- Before/After diff — Visual comparison of original vs corrected text
- Multiple suggestions — Dropdown to choose between alternative corrections
- One-click fix — Apply corrections directly (issue removed from UI + DB)
- Ignore button — Dismiss false positives (persists in DB across reloads)
- Add to dictionary — Whitelist a word directly from an issue card
- Summary cards — Total documents, average score, issues count
Sidebar Field
- Score badge — Color-coded score (green/yellow/red) in the editor sidebar
- Issue list — All issues with context, suggestions, and fix buttons
- Manual check — "Vérifier" button for on-demand analysis
- Auto-check — Results loaded automatically from last check
Auto-check on Save
- Non-blocking — Fire-and-forget async (IIFE pattern, does not slow down saves)
- Upsert results — Stores/updates results in
spellcheck-resultscollection - Configurable — Enable/disable via
checkOnSaveoption
LanguageTool Engine
- Free API — No API key required (public LanguageTool API)
- Rate-limited — 3-second delay between requests for bulk scans
- 18K char limit — Automatic text truncation for API compliance
- Smart filtering — Skip premium rules, typography, style-only issues
- Custom dictionary — Whitelist words that shouldn't be flagged
Claude AI Fallback (Optional)
- Semantic analysis — Checks coherence, tone, phrasing, missing words
- Complementary — Does NOT duplicate LanguageTool (no spelling/grammar)
- Cost-efficient — Uses Claude Haiku for fast, cheap analysis
- Opt-in — Disabled by default, enable via
enableAiFallback: true
Dynamic Dictionary
- Admin UI — Manage dictionary from the "Dictionnaire" tab in the dashboard
- Add words — Single word or comma-separated bulk input
- Import — Paste a list of words (one per line or comma-separated)
- Export — Download all dictionary words as a
.txtfile - Search & filter — Find words in the dictionary
- Bulk delete — Select and remove multiple words at once
- Merged sources — Config
customDictionary(defaults) + DB dictionary (dynamic) - 5-min cache — Dictionary loaded from DB with in-memory TTL cache
- Auto-schema — Plugin auto-creates missing DB columns on init (SQLite/Postgres)
Lexical JSON Support
- Recursive extraction — Traverses Lexical AST to extract plain text
- Code block skip — Ignores code blocks (not natural language)
- Offset-based fixes — Precise corrections using LanguageTool offsets (v0.8.0+)
- Legacy fallback — Substring search for backwards compatibility
- Multi-field — Extracts from hero, content, layout blocks, columns
Installation
# npm
npm install @consilioweb/payload-spellcheck
# pnpm
pnpm add @consilioweb/payload-spellcheck
# yarn
yarn add @consilioweb/payload-spellcheck| Peer Dependency | Version |
|----------------|---------|
| payload | ^3.0.0 |
| @payloadcms/next | ^3.0.0 |
| @payloadcms/ui | ^3.0.0 |
| react | ^18.0.0 \|\| ^19.0.0 |
Quick Start
Add the plugin to your Payload config:
// src/plugins/index.ts (or payload.config.ts)
import { spellcheckPlugin } from '@consilioweb/payload-spellcheck'
export default buildConfig({
plugins: [
spellcheckPlugin({
collections: ['pages', 'posts'],
language: 'fr',
}),
],
})Then regenerate the import map:
npx payload generate:importmapThat's it! The plugin automatically:
- Creates
spellcheck-resultsandspellcheck-dictionarycollections (hidden from admin nav) - Registers API endpoints (
validate,fix,bulk,status,dictionary) - Adds a sidebar field to your target collections
- Creates a dashboard view at
/admin/spellcheck(Results + Dictionary tabs) - Adds an
afterChangehook for auto-checking on save - Auto-fixes missing DB columns on init (SQLite/Postgres
push:truecompatibility)
Configuration
spellcheckPlugin({
// Target collections (default: ['pages', 'posts'])
collections: ['pages', 'posts'],
// Rich text field name (default: 'content')
contentField: 'content',
// LanguageTool language (default: 'fr')
language: 'fr',
// Auto-check on save (default: true)
checkOnSave: true,
// Sidebar field in editor (default: true)
addSidebarField: true,
// Dashboard view at /admin/spellcheck (default: true)
addDashboardView: true,
// Base path for API endpoints (default: '/spellcheck')
endpointBasePath: '/spellcheck',
// ── Filtering ──────────────────────────────────────
// LanguageTool rule IDs to skip
skipRules: ['FR_SPELLING_RULE', 'WHITESPACE_RULE'],
// LanguageTool categories to skip
skipCategories: ['TYPOGRAPHY', 'STYLE'],
// Words to never flag as errors
customDictionary: [
'Next.js', 'Payload', 'TypeScript', 'SEO',
'Corrèze', 'Limoges', 'ConsilioWEB',
],
// Minimum score threshold for warnings (default: 80)
warningThreshold: 80,
// ── Claude AI Fallback (optional) ──────────────────
// Enable semantic analysis via Claude (default: false)
enableAiFallback: false,
// Anthropic API key (required if enableAiFallback is true)
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
// ── RBAC (v0.13.0) ────────────────────────────────────
// Custom access control function (default: admin-only)
access: ({ req }) => req.user?.role === 'admin',
// ── Advanced (v0.13.0) ─────────────────────────────────
// Custom package name for component paths (monorepo support)
packageName: '@my-scope/spellcheck',
// Trust x-forwarded-for header for IP-based rate limiting
trustProxy: false,
})Options Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| collections | string[] | ['pages', 'posts'] | Collections à vérifier |
| contentField | string | 'content' | Nom du champ rich text à extraire |
| language | string | 'fr' | Code langue pour LanguageTool |
| checkOnSave | boolean | true | Vérification automatique à la sauvegarde |
| addSidebarField | boolean | true | Ajouter le champ sidebar dans l'éditeur |
| addDashboardView | boolean | true | Ajouter la vue /admin/spellcheck |
| addListColumn | boolean | true | Ajouter la colonne score dans les listes de collection |
| endpointBasePath | string | '/spellcheck' | Chemin de base pour les endpoints API |
| enableAiFallback | boolean | false | Activer l'analyse sémantique Claude AI |
| anthropicApiKey | string | — | Clé API Anthropic pour Claude |
| skipRules | string[] | [] | IDs de règles LanguageTool à ignorer |
| skipCategories | string[] | [] | Catégories LanguageTool à ignorer |
| customDictionary | string[] | [] | Mots à ne jamais signaler comme erreurs |
| languageToolUrl | string | 'https://api.languagetool.org/v2/check' | URL de l'API LanguageTool (pour instances auto-hébergées) |
| warningThreshold | number | 80 | Score en dessous duquel un avertissement est affiché |
| autoFixSchema | boolean | true | Auto-fix missing DB columns on startup |
| access | function | Admin-only | Custom access control function for endpoints (v0.13.0) |
| packageName | string | '@consilioweb/payload-spellcheck' | Custom package name for component paths — useful in monorepos (v0.13.0) |
| trustProxy | boolean | false | Trust x-forwarded-for header for IP-based rate limiting (v0.13.0) |
| rateLimits | object | -- | Rate limiting overrides (see below) |
| timeouts | object | -- | Timeout and limit overrides (see below) |
rateLimits
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| rateLimits.validate | number | 30 | Max requêtes par fenêtre pour /validate |
| rateLimits.fix | number | 20 | Max requêtes par fenêtre pour /fix |
| rateLimits.fixAll | number | 5 | Max requêtes par fenêtre pour /fix-all |
| rateLimits.bulk | number | 3 | Max requêtes par fenêtre pour /bulk |
| rateLimits.dictionary | number | 60 | Max requêtes par fenêtre pour /dictionary |
| rateLimits.windowMs | number | 60000 | Fenêtre de rate limiting en millisecondes |
timeouts
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| timeouts.languageTool | number | 30000 | Timeout de l'API LanguageTool en ms |
| timeouts.claude | number | 60000 | Timeout de l'API Claude en ms |
| timeouts.maxTextLengthLanguageTool | number | 18000 | Longueur max du texte envoyé à LanguageTool (caractères) |
| timeouts.maxTextLengthClaude | number | 8000 | Longueur max du texte envoyé à Claude (caractères) |
| timeouts.bulkRateLimitDelay | number | 3000 | Délai entre les appels API LanguageTool lors d'un scan bulk (ms) |
| timeouts.bulkStaleTimeout | number | 600000 | Timeout pour les jobs bulk stale (ms) |
Admin Views
Dashboard (/admin/spellcheck)
The dashboard provides a complete overview of your content's spelling quality:
- Summary cards — Document count, average score, total issues, error-free count
- Sortable table — Click column headers to sort by score, issues, words, date
- Expandable rows — Click any row to see detailed issues with context and suggestions
- Bulk scan — "Scanner tout" analyzes all published documents sequentially
- One-click fix — Apply a correction directly from the expanded issue view
Sidebar Field
The sidebar field appears in the editor for every target collection:
- Score badge — Color-coded (green ≥95, yellow ≥80, red <80)
- Stats bar — Word count, issue count, last check time
- Issue cards — Each issue shows message, context with highlighted error, suggestion
- Fix button — Applies the suggestion directly in the Lexical JSON
- Ignore button — Removes the issue from the current view
API Endpoints
All endpoints require authentication (Payload admin user).
| Endpoint | Method | Description |
|----------|--------|-------------|
| /api/spellcheck/validate | POST | Check a single document or raw text |
| /api/spellcheck/fix | POST | Apply a correction in Lexical JSON (offset-based) |
| /api/spellcheck/bulk | POST | Scan all documents (sequential, rate-limited) |
| /api/spellcheck/status | GET | Get current bulk scan progress |
| /api/spellcheck/dictionary | GET | List all dictionary words |
| /api/spellcheck/dictionary | POST | Add word(s) to dictionary |
| /api/spellcheck/dictionary | DELETE | Remove word(s) from dictionary |
POST /api/spellcheck/validate
// Check a document by ID
{ "id": "123", "collection": "pages" }
// Check raw text
{ "text": "Ceci est une test.", "language": "fr" }Response:
{
"docId": "123",
"collection": "pages",
"score": 85,
"issueCount": 2,
"wordCount": 450,
"issues": [
{
"ruleId": "GRAMMAR",
"category": "GRAMMAR",
"message": "Le déterminant « une » ne correspond pas...",
"context": "Ceci est une test.",
"original": "une",
"replacements": ["un"],
"source": "languagetool"
}
],
"lastChecked": "2025-02-22T20:30:00.000Z"
}POST /api/spellcheck/fix
{
"id": "123",
"collection": "pages",
"original": "une test",
"replacement": "un test",
"offset": 42,
"length": 8
}
offsetandlengthenable precise offset-based targeting (v0.8.0+). Falls back to substring search if omitted.
GET /api/spellcheck/dictionary
Response:
{ "words": [{ "id": "1", "word": "typescript", "addedBy": { "email": "[email protected]" }, "createdAt": "..." }], "count": 1 }POST /api/spellcheck/dictionary
// Single word
{ "word": "TypeScript" }
// Multiple words
{ "words": ["TypeScript", "Next.js", "Payload"] }DELETE /api/spellcheck/dictionary
// Single
{ "id": "abc123" }
// Multiple
{ "ids": ["abc123", "def456"] }POST /api/spellcheck/bulk
// Scan all configured collections
{}
// Scan a specific collection
{ "collection": "posts" }Engine
Text Extraction
The plugin extracts text from Payload documents by recursively traversing:
- Title — Document title
- Hero —
hero.richText(Lexical JSON) - Content — Main content field (Lexical JSON)
- Layout blocks — Each block's
richTextandcolumns[].richText
Code blocks are automatically skipped (not natural language).
LanguageTool
- API:
POST https://api.languagetool.org/v2/check(free, no auth) - Limit: 18,000 characters per request (auto-truncated)
- Rate: 3-second delay between bulk requests
- Timeout: 30 seconds per request
Filtering
Issues are filtered through multiple layers:
- Premium rules — Skipped (free API only)
- Configured rules —
skipRulesoption - Configured categories —
skipCategoriesoption - Custom dictionary — Case-insensitive word matching
- Single-character — Skipped (often punctuation false positives)
Scoring
Score = max(0, 100 - (issues / words * 1000))
- 100 — No issues
- 90+ — Excellent (green)
- 80+ — Good (yellow)
- <80 — Needs work (red)
Claude AI (Optional)
When enableAiFallback: true, the plugin also sends text to Claude Haiku for:
- Inconsistent tone or register
- Incoherent statements or contradictions
- Awkward phrasing
- Missing words that change meaning
Claude issues are tagged with source: 'claude' and category COHERENCE, TONE, PHRASING, or MISSING_WORD.
Collections
The plugin auto-creates two collections:
| Collection | Slug | Description |
|------------|------|-------------|
| SpellCheck Results | spellcheck-results | Stores check results per document |
| SpellCheck Dictionary | spellcheck-dictionary | Dynamic dictionary (one doc per word) |
Results fields: docId, collection, title, slug, score, issueCount, wordCount, issues (JSON), lastChecked
Dictionary fields: word (text, unique, indexed), addedBy (relationship to users)
Both collections are hidden from the admin nav. The dictionary is managed via the Dashboard's "Dictionnaire" tab or the REST API.
Package Exports
Main Entry (@consilioweb/payload-spellcheck)
// Plugin
export { spellcheckPlugin } from './plugin'
// Types
export type { SpellCheckPluginConfig, SpellCheckIssue, SpellCheckResult } from './types'
// Engine (for programmatic use)
export { extractTextFromLexical, countWords } from './engine/lexicalParser'
export { checkWithLanguageTool } from './engine/languagetool'
export { checkWithClaude } from './engine/claude'
export { filterFalsePositives, calculateScore } from './engine/filters'
// Dictionary cache
export { loadDictionaryWords, invalidateDictionaryCache } from './endpoints/dictionary'
// i18n
export { getTranslations, getScoreLabel } from './i18n'Client Entry (@consilioweb/payload-spellcheck/client)
export { SpellCheckField } from './components/SpellCheckField'
export { SpellCheckDashboard } from './components/SpellCheckDashboard'
export { IssueCard } from './components/IssueCard'Views Entry (@consilioweb/payload-spellcheck/views)
export { SpellCheckView } from './views/SpellCheckView'Requirements
- Node.js >= 18
- Payload CMS 3.x
- React 18.x or 19.x
- Any Payload DB adapter (SQLite, PostgreSQL, MongoDB)
Uninstall
Automatic (recommended)
npx spellcheck-uninstallThis will:
- Remove all
@consilioweb/payload-spellcheckimports and plugin calls from your source files - Drop the
spellcheck_resultstable and indexes from your database - Remove the npm package
- Regenerate the import map
Use
--keep-datato preserve the database table.
Manual
- Remove the plugin from your config
- Run
npx payload generate:importmap - (Optional) Drop the database table:
-- SQLite
DROP INDEX IF EXISTS `spellcheck_results_doc_id_idx`;
DROP INDEX IF EXISTS `spellcheck_results_collection_idx`;
DROP INDEX IF EXISTS `spellcheck_results_last_checked_idx`;
DROP TABLE IF EXISTS `spellcheck_results`;Changelog
v0.13.0
- New: RBAC with configurable
accessfunction in plugin config - New:
packageNameoption for custom package name in component paths - New:
useSpellcheckI18nhook for component localization - New: i18n integration in SpellCheckField and IssueCard components
- New: Client-side score cache (30s TTL) in SpellCheckScoreCell
- New: Collection injection protection on validate, fix, fixAll endpoints
- New: IP spoofing protection with
trustProxyoption - Changed: fixAll calls fix logic directly instead of HTTP self-fetch (eliminates SSRF)
- Changed: URL built from NEXT_PUBLIC_SERVER_URL, not Origin header
- Changed: Score formula alignment between client and server
v0.8.1
- Fix: Corrections now remove the issue from the UI immediately (optimistic update)
- Fix: "Ignorer" persists in DB across page reloads (was local state only)
- Fix: Auto-fix missing DB columns on init (
push:truecompatibility for SQLite/Postgres)
v0.8.0
- New: Dynamic dictionary — manage words from the admin dashboard (add/remove/import/export)
- New:
spellcheck-dictionarycollection (one document per word, merged with config dictionary) - New: Dictionary REST API (GET/POST/DELETE at
/api/spellcheck/dictionary) - New: Offset-based corrections — precise fix targeting using LanguageTool offsets
- New: "Add to dictionary" button on issue cards (+ Dico)
- New: "Ignore" button to dismiss false positives
- New: Dictionary tab in the dashboard with search, bulk delete, import/export
- New: In-memory cache (5-min TTL) for dictionary DB queries
- Changed:
filterFalsePositivesis now async (merges config + DB dictionaries) - Changed: Fix endpoint accepts
offsetandlengthparameters (falls back to substring search)
v0.5.0 — v0.7.0
- Custom dictionary config, contextual multi-word filtering, background scan
- Lexical ghost space fix, extended French dictionary
- Contextual offset, manual edit input, repetition filter
Roadmap
- Bulk scan with AI (Claude) integration
- Export spell check results as CSV/JSON
- Webhook / notification on new issues found
- Per-field spellcheck configuration
- Custom dictionaries per collection
- Grammar rules beyond spelling (style, tone, consistency)
- Auto-correct suggestions with one-click apply
- Integration with external APIs (Grammarly, LanguageTool Cloud)
☕ Support
If this plugin saves you time, consider buying me a coffee!
License
MIT License - see LICENSE for details.
