{"_id":"@consilioweb/payload-spellcheck","_rev":"8-91867e70f254d8478a37696fd57f25ca","name":"@consilioweb/payload-spellcheck","dist-tags":{"latest":"0.18.0"},"versions":{"0.13.1":{"name":"@consilioweb/payload-spellcheck","version":"0.13.1","keywords":["spellcheck","spelling","grammar","languagetool","claude-ai","payload","payload-cms","payload-plugin","payload-cms-3","lexical","richtext","i18n","french","english","readability","consistency","bulk-scan","auto-fix","dictionary","typescript","nextjs"],"author":{"url":"https://consilioweb.fr","name":"ConsilioWEB","email":"contact@consilioweb.fr"},"license":"MIT","_id":"@consilioweb/payload-spellcheck@0.13.1","maintainers":[{"name":"pown3d","email":"contact@consilioweb.fr"}],"homepage":"https://github.com/pOwn3d/payload-spellcheck#readme","bugs":{"url":"https://github.com/pOwn3d/payload-spellcheck/issues"},"bin":{"spellcheck-install":"scripts/install.mjs","spellcheck-uninstall":"scripts/uninstall.mjs"},"dist":{"shasum":"6e7ad3019d5a6be8625f3a1f6725de3c5147954e","tarball":"https://registry.npmjs.org/@consilioweb/payload-spellcheck/-/payload-spellcheck-0.13.1.tgz","fileCount":46,"integrity":"sha512-gQTTpzlzUcMNbPmjx2ByuUQVJ5vr/41RMxxCeqgx6Euq7Z9zsC3Rjl0W60RClS4Gb4MaALU7YeQFYFUNy0oCZQ==","signatures":[{"sig":"MEQCIFZnRYpfmv0Q9B3fZAAN/v1fuW3v3cV5KgHpN09MlB22AiBq5ByXODhCchmUAjLRkNXEpntFutOdUbGbjinmxA9VxQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":405596},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./views":{"import":{"types":"./dist/views.d.ts","default":"./dist/views.js"},"require":{"types":"./dist/views.d.cts","default":"./dist/views.cjs"}},"./client":{"import":{"types":"./dist/client.d.ts","default":"./dist/client.js"},"require":{"types":"./dist/client.d.cts","default":"./dist/client.cjs"}}},"gitHead":"dac52c062ec7214fe2cc25e8e372f7ffd04d4434","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"pnpm build"},"_npmUser":{"name":"pown3d","email":"contact@consilioweb.fr"},"deprecated":"SECURITY: the peer range admitted a Payload vulnerable to pre-authentication account takeover and SQL injection. Upgrade to 0.17.0 — see https://github.com/pOwn3d/payload-spellcheck/blob/main/CHANGELOG.md","repository":{"url":"git+https://github.com/pOwn3d/payload-spellcheck.git","type":"git"},"_npmVersion":"10.9.4","description":"Payload CMS spellcheck plugin — LanguageTool + Claude AI fallback, dashboard, sidebar field, auto-check on save","directories":{},"_nodeVersion":"22.22.1","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/react":"^19.0.0"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","payload":"^3.0.0","@payloadcms/ui":"^3.0.0","@payloadcms/next":"^3.0.0"},"peerDependenciesMeta":{"react":{"optional":true},"@payloadcms/ui":{"optional":true},"@payloadcms/next":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/payload-spellcheck_0.13.1_1776236000333_0.6279486950023232","host":"s3://npm-registry-packages-npm-production"}},"0.14.0":{"name":"@consilioweb/payload-spellcheck","version":"0.14.0","keywords":["spellcheck","spelling","grammar","languagetool","claude-ai","payload","payload-cms","payload-plugin","payload-cms-3","lexical","richtext","i18n","french","english","readability","consistency","bulk-scan","auto-fix","dictionary","typescript","nextjs"],"author":{"url":"https://consilioweb.fr","name":"ConsilioWEB","email":"contact@consilioweb.fr"},"license":"MIT","_id":"@consilioweb/payload-spellcheck@0.14.0","maintainers":[{"name":"pown3d","email":"contact@consilioweb.fr"}],"homepage":"https://github.com/pOwn3d/payload-spellcheck#readme","bugs":{"url":"https://github.com/pOwn3d/payload-spellcheck/issues"},"bin":{"spellcheck-install":"scripts/install.mjs","spellcheck-uninstall":"scripts/uninstall.mjs"},"dist":{"shasum":"97c13461a0fa492496573f103e46ea3f5c30c3a0","tarball":"https://registry.npmjs.org/@consilioweb/payload-spellcheck/-/payload-spellcheck-0.14.0.tgz","fileCount":46,"integrity":"sha512-Stj6HXz5rwgrACNEvmhUzxeojnZNp/KrL2O+mCWohVCMmXOhUp1hP5M9a1gDldVBa9FijtLU/omMJ+ia/QJqWg==","signatures":[{"sig":"MEYCIQCDuhus8Bt1gGQaT1FM3/6rab38obl7v8Bj+4qaDWow9QIhAKvnPCxkx7+bOTeBGRJ24iloXWamRY1U3V7cyVWW8WQ9","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":406786},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./views":{"import":{"types":"./dist/views.d.ts","default":"./dist/views.js"},"require":{"types":"./dist/views.d.cts","default":"./dist/views.cjs"}},"./client":{"import":{"types":"./dist/client.d.ts","default":"./dist/client.js"},"require":{"types":"./dist/client.d.cts","default":"./dist/client.cjs"}}},"gitHead":"bb6c861023ef3776dd6f217f25ccea38726e9054","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"pnpm build"},"_npmUser":{"name":"pown3d","email":"contact@consilioweb.fr"},"deprecated":"SECURITY: the peer range admitted a Payload vulnerable to pre-authentication account takeover and SQL injection. Upgrade to 0.17.0 — see https://github.com/pOwn3d/payload-spellcheck/blob/main/CHANGELOG.md","repository":{"url":"git+https://github.com/pOwn3d/payload-spellcheck.git","type":"git"},"_npmVersion":"11.18.0","description":"Payload CMS spellcheck plugin — LanguageTool + Claude AI fallback, dashboard, sidebar field, auto-check on save","directories":{},"_nodeVersion":"26.6.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/react":"^19.0.0"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","payload":"^3.0.0","@payloadcms/ui":"^3.0.0","@payloadcms/next":"^3.0.0"},"peerDependenciesMeta":{"react":{"optional":true},"@payloadcms/ui":{"optional":true},"@payloadcms/next":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/payload-spellcheck_0.14.0_1786220054208_0.8625215106022002","host":"s3://npm-registry-packages-npm-production"}},"0.15.0":{"name":"@consilioweb/payload-spellcheck","version":"0.15.0","keywords":["spellcheck","spelling","grammar","languagetool","claude-ai","payload","payload-cms","payload-plugin","payload-cms-3","lexical","richtext","i18n","french","english","readability","consistency","bulk-scan","auto-fix","dictionary","typescript","nextjs"],"author":"ConsilioWEB <contact@consilioweb.fr> (https://consilioweb.fr)","license":"MIT","_id":"@consilioweb/payload-spellcheck@0.15.0","maintainers":[{"name":"pown3d","email":"contact@consilioweb.fr"}],"homepage":"https://github.com/pOwn3d/payload-spellcheck#readme","bugs":{"url":"https://github.com/pOwn3d/payload-spellcheck/issues"},"bin":{"spellcheck-install":"./scripts/install.mjs","spellcheck-uninstall":"./scripts/uninstall.mjs"},"dist":{"shasum":"6ad9dcadb1fa40dc8d74750623267ec6770441f0","tarball":"https://registry.npmjs.org/@consilioweb/payload-spellcheck/-/payload-spellcheck-0.15.0.tgz","fileCount":46,"integrity":"sha512-u3GwVUXIEwHNMXT6DhYxOP7HqIvUvhEp3lBuX8A+vukMlfTFSIew+69oVW2zlA4EjgwL6mtMft/2fnkMI4emXw==","signatures":[{"sig":"MEQCIHXU8tvLWCvHJdYd21rZn8jaOQfg1Q3aSAFUnjizUpzwAiAwroXfpucXzIUxCn7T6tU9LiSmjw52IF1pBcKvkxZRvQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIDc+luQV+sjuZ3rlCMHZf/g2WPeGx/Z1sHOTu3vNM57NAiBdljKpeXGpxoUuJujj58/C1ChoaR/2ad5VdoLlGILqgg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@consilioweb%2fpayload-spellcheck@0.15.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":447713},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./views":{"import":{"types":"./dist/views.d.ts","default":"./dist/views.js"},"require":{"types":"./dist/views.d.cts","default":"./dist/views.cjs"}},"./client":{"import":{"types":"./dist/client.d.ts","default":"./dist/client.js"},"require":{"types":"./dist/client.d.cts","default":"./dist/client.cjs"}}},"scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"pown3d","email":"contact@consilioweb.fr"},"deprecated":"SECURITY: the peer range admitted a Payload vulnerable to pre-authentication account takeover and SQL injection. Upgrade to 0.17.0 — see https://github.com/pOwn3d/payload-spellcheck/blob/main/CHANGELOG.md","repository":{"url":"git+https://github.com/pOwn3d/payload-spellcheck.git","type":"git"},"description":"Payload CMS spellcheck plugin — LanguageTool + Claude AI fallback, dashboard, sidebar field, auto-check on save","directories":{},"_nodeVersion":"22.23.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/react":"^19.0.0"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","payload":"^3.0.0","@payloadcms/ui":"^3.0.0","@payloadcms/next":"^3.0.0"},"peerDependenciesMeta":{"react":{"optional":true},"@payloadcms/ui":{"optional":true},"@payloadcms/next":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/payload-spellcheck_0.15.0_1788788612559_0.7742610503030358","host":"s3://npm-registry-packages-npm-production"}},"0.16.0":{"name":"@consilioweb/payload-spellcheck","version":"0.16.0","keywords":["spellcheck","spelling","grammar","languagetool","claude-ai","payload","payload-cms","payload-plugin","payload-cms-3","lexical","richtext","i18n","french","english","readability","consistency","bulk-scan","auto-fix","dictionary","typescript","nextjs"],"author":"ConsilioWEB <contact@consilioweb.fr> (https://consilioweb.fr)","license":"MIT","_id":"@consilioweb/payload-spellcheck@0.16.0","maintainers":[{"name":"pown3d","email":"contact@consilioweb.fr"}],"homepage":"https://github.com/pOwn3d/payload-spellcheck#readme","bugs":{"url":"https://github.com/pOwn3d/payload-spellcheck/issues"},"bin":{"spellcheck-install":"./scripts/install.mjs","spellcheck-uninstall":"./scripts/uninstall.mjs"},"dist":{"shasum":"cc4513fd95a134fea4d794e5670570e0402118fd","tarball":"https://registry.npmjs.org/@consilioweb/payload-spellcheck/-/payload-spellcheck-0.16.0.tgz","fileCount":46,"integrity":"sha512-dSOQqLsXJbC1yhcPjoDT/M/YIr/vjiTy+ScleaOCo9Pv5Avkq0tvpMBLsQBUn8fa2qwQp2OOkMee9+kvEtoyXw==","signatures":[{"sig":"MEQCIAOCbrZQqSKglfReVFBJxlENg8IHSdxgHjijRZwsyfK1AiBUlF5wenZJ3dwNciVUroPyeRA+PeILv6/YA1KkhcfdZw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCmnfrc1GrFwZoTsEn2pY3FPU0Mr3azCG6G0VSMeL1mZQIgKg0S0fCvZ5mqeiYIUdtuDYQS6CTTP1ZO4p4dcaOS9I0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@consilioweb%2fpayload-spellcheck@0.16.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":464171},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./views":{"import":{"types":"./dist/views.d.ts","default":"./dist/views.js"},"require":{"types":"./dist/views.d.cts","default":"./dist/views.cjs"}},"./client":{"import":{"types":"./dist/client.d.ts","default":"./dist/client.js"},"require":{"types":"./dist/client.d.cts","default":"./dist/client.cjs"}}},"scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"pown3d","email":"contact@consilioweb.fr"},"deprecated":"SECURITY: the peer range admitted a Payload vulnerable to pre-authentication account takeover and SQL injection. Upgrade to 0.17.0 — see https://github.com/pOwn3d/payload-spellcheck/blob/main/CHANGELOG.md","repository":{"url":"git+https://github.com/pOwn3d/payload-spellcheck.git","type":"git"},"description":"Payload CMS spellcheck plugin — LanguageTool + Claude AI fallback, dashboard, sidebar field, auto-check on save","directories":{},"_nodeVersion":"22.23.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/react":"^19.0.0"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","payload":"^3.0.0","@payloadcms/ui":"^3.0.0","@payloadcms/next":"^3.0.0"},"peerDependenciesMeta":{"react":{"optional":true},"@payloadcms/ui":{"optional":true},"@payloadcms/next":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/payload-spellcheck_0.16.0_1788850838598_0.6170299334865652","host":"s3://npm-registry-packages-npm-production"}},"0.17.0":{"name":"@consilioweb/payload-spellcheck","version":"0.17.0","keywords":["spellcheck","spelling","grammar","languagetool","claude-ai","payload","payload-cms","payload-plugin","payload-cms-3","lexical","richtext","i18n","french","english","readability","consistency","bulk-scan","auto-fix","dictionary","typescript","nextjs"],"author":"ConsilioWEB <contact@consilioweb.fr> (https://consilioweb.fr)","license":"MIT","_id":"@consilioweb/payload-spellcheck@0.17.0","maintainers":[{"name":"pown3d","email":"contact@consilioweb.fr"}],"homepage":"https://github.com/pOwn3d/payload-spellcheck#readme","bugs":{"url":"https://github.com/pOwn3d/payload-spellcheck/issues"},"bin":{"spellcheck-install":"./scripts/install.mjs","spellcheck-uninstall":"./scripts/uninstall.mjs"},"dist":{"shasum":"80370d5463a2664d1b76e06c5dd864f0895a6eb5","tarball":"https://registry.npmjs.org/@consilioweb/payload-spellcheck/-/payload-spellcheck-0.17.0.tgz","fileCount":46,"integrity":"sha512-QBB7yzf0C+HejkNIX2qtKv2hpb3M4o7cdWJRW45+jx16tzogMDVYHdnBOXRZLECJTEuSBa+PCjTBiDLBkQd+RA==","signatures":[{"sig":"MEUCIQDYqPQnbYvqTAmzLpMwdBcqT/9oYez6nKnWChc2/8bEwwIgclLf7FdEe3ildmoUR0dMjgRcjRZiua2ItC3S7rNFnSg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCID9LIwq+JxOPbo5G+WQLPGr0PaBKmjMRQKA2XAQ+6amPAiEAxUWrabciSMsB2hR0yhTSE69/E/xA9oO2VWedS04yraA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@consilioweb%2fpayload-spellcheck@0.17.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":465011},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./views":{"import":{"types":"./dist/views.d.ts","default":"./dist/views.js"},"require":{"types":"./dist/views.d.cts","default":"./dist/views.cjs"}},"./client":{"import":{"types":"./dist/client.d.ts","default":"./dist/client.js"},"require":{"types":"./dist/client.d.cts","default":"./dist/client.cjs"}}},"scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"pown3d","email":"contact@consilioweb.fr"},"repository":{"url":"git+https://github.com/pOwn3d/payload-spellcheck.git","type":"git"},"description":"Payload CMS spellcheck plugin — LanguageTool + Claude AI fallback, dashboard, sidebar field, auto-check on save","directories":{},"_nodeVersion":"22.23.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vitest":"^3.2.7","typescript":"^5.9.3","@types/react":"^19.2.18"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","payload":"^3.79.1","@payloadcms/ui":"^3.79.1","@payloadcms/next":"^3.79.1"},"peerDependenciesMeta":{"react":{"optional":true},"@payloadcms/ui":{"optional":true},"@payloadcms/next":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/payload-spellcheck_0.17.0_1788856248859_0.024917664688059338","host":"s3://npm-registry-packages-npm-production"}},"0.18.0":{"_id":"@consilioweb/payload-spellcheck@0.18.0","bin":{"spellcheck-install":"./scripts/install.mjs","spellcheck-uninstall":"./scripts/uninstall.mjs"},"bugs":{"url":"https://github.com/pOwn3d/payload-spellcheck/issues"},"dist":{"shasum":"3c98837943dc436a6b7812a8246fedee363b4a45","tarball":"https://registry.npmjs.org/@consilioweb/payload-spellcheck/-/payload-spellcheck-0.18.0.tgz","fileCount":50,"integrity":"sha512-Tl72pvvLn5Zg9CAzXARu22NtU9wsZ7yzpiEUYAfUr8hl15GaKlVikcqIGFxvuK/hvFg3fJsSR1LYm88KAs9QgQ==","signatures":[{"sig":"MEUCIF/VbfN4vKpy2+6S76iRflRzJtGatWiTY1sHFidcLBWjAiEAoc/b29IYdTzm3ofEoGoqcvc+8qjL0ajBrd1erevBv3Q=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCnPlkjF1TVbZi2NerOW9ofYtLJbA3wKnbH0+lyap8eDQIgFUh/0ytk18zteJL9QzlJxkM0PfX5dQaMBOkBxoKToeE="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@consilioweb%2fpayload-spellcheck@0.18.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":504103},"main":"./dist/index.cjs","name":"@consilioweb/payload-spellcheck","type":"module","types":"./dist/index.d.ts","author":"ConsilioWEB <contact@consilioweb.fr> (https://consilioweb.fr)","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./views":{"import":{"types":"./dist/views.d.ts","default":"./dist/views.js"},"require":{"types":"./dist/views.d.cts","default":"./dist/views.cjs"}},"./client":{"import":{"types":"./dist/client.d.ts","default":"./dist/client.js"},"require":{"types":"./dist/client.d.cts","default":"./dist/client.cjs"}}},"license":"MIT","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"version":"0.18.0","_npmUser":{"name":"pown3d","email":"contact@consilioweb.fr"},"homepage":"https://github.com/pOwn3d/payload-spellcheck#readme","keywords":["spellcheck","spelling","grammar","languagetool","claude-ai","payload","payload-cms","payload-plugin","payload-cms-3","lexical","richtext","i18n","french","english","readability","consistency","bulk-scan","auto-fix","dictionary","typescript","nextjs"],"repository":{"url":"git+https://github.com/pOwn3d/payload-spellcheck.git","type":"git"},"description":"Payload CMS spellcheck plugin — LanguageTool + Claude AI fallback, dashboard, sidebar field, auto-check on save","directories":{},"maintainers":[{"name":"pown3d","email":"contact@consilioweb.fr"}],"_nodeVersion":"22.23.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vitest":"^3.2.7","typescript":"^5.9.3","@types/react":"^19.2.18"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","payload":"^3.79.1","@payloadcms/ui":"^3.79.1","@payloadcms/next":"^3.79.1"},"peerDependenciesMeta":{"react":{"optional":true},"@payloadcms/ui":{"optional":true},"@payloadcms/next":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/payload-spellcheck_0.18.0_1788862741620_0.7234661066343826"}}},"time":{"created":"2026-04-15T06:53:20.238Z","modified":"2026-09-08T10:19:02.096Z","0.13.1":"2026-04-15T06:53:20.471Z","0.14.0":"2026-08-08T20:14:14.355Z","0.15.0":"2026-09-07T13:43:32.670Z","0.16.0":"2026-09-08T07:00:38.689Z","0.17.0":"2026-09-08T08:30:48.983Z","0.18.0":"2026-09-08T10:19:01.704Z"},"bugs":{"url":"https://github.com/pOwn3d/payload-spellcheck/issues"},"author":"ConsilioWEB <contact@consilioweb.fr> (https://consilioweb.fr)","license":"MIT","homepage":"https://github.com/pOwn3d/payload-spellcheck#readme","keywords":["spellcheck","spelling","grammar","languagetool","claude-ai","payload","payload-cms","payload-plugin","payload-cms-3","lexical","richtext","i18n","french","english","readability","consistency","bulk-scan","auto-fix","dictionary","typescript","nextjs"],"repository":{"url":"git+https://github.com/pOwn3d/payload-spellcheck.git","type":"git"},"description":"Payload CMS spellcheck plugin — LanguageTool + Claude AI fallback, dashboard, sidebar field, auto-check on save","maintainers":[{"name":"pown3d","email":"contact@consilioweb.fr"}],"readme":"# @consilioweb/payload-spellcheck\n\n> Spelling, grammar and readability checking inside the Payload CMS 3 admin panel, powered by LanguageTool with an optional Claude semantic pass.\n\n[![npm](https://img.shields.io/npm/v/@consilioweb/payload-spellcheck.svg)](https://www.npmjs.com/package/@consilioweb/payload-spellcheck)\n[![license](https://img.shields.io/npm/l/@consilioweb/payload-spellcheck.svg)](LICENSE)\n[![Payload CMS](https://img.shields.io/badge/Payload%20CMS-3.x-blue.svg)](https://payloadcms.com)\n\n> [!IMPORTANT]\n> **Next.js 16 + Turbopack — known issue.** With **Next.js 16** and Turbopack (the default bundler),\n> `next build` may fail with `createContext is not a function`. This is a\n> [known Payload CMS issue](https://github.com/payloadcms/payload/issues/15429)\n> ([discussion](https://github.com/payloadcms/payload/discussions/14330)) — not specific to this plugin.\n>\n> **Workaround** — in your admin page (`src/app/(payload)/admin/[[...segments]]/page.tsx`):\n> ```ts\n> export const dynamic = 'force-dynamic'\n> ```\n>\n> And list every `@consilioweb/*` package in `transpilePackages` in `next.config.ts`:\n> ```ts\n> transpilePackages: ['@consilioweb/payload-spellcheck', /* ...other @consilioweb packages */],\n> ```\n>\n> Next.js 15 works without any workaround.\n\n## About\n\n`@consilioweb/payload-spellcheck` checks the editorial content of your Payload documents where it is\nwritten, instead of after publication: a score and an issue list in the editor sidebar, a dashboard\nthat scans every document of the configured collections, and a one-click fix that rewrites the\nLexical JSON in place at the exact offset LanguageTool reported.\n\nSpelling and grammar come from [LanguageTool](https://languagetool.org/) — the free public API needs\nno key, and a self-hosted instance is a one-line option. Claude can be enabled on top for what a\ngrammar checker cannot see (tone, coherence, missing words); it never replaces LanguageTool. False\npositives are absorbed by a dictionary that lives half in the config and half in the database, so an\neditor can whitelist a brand name without a deploy.\n\nFull release history: [CHANGELOG.md](CHANGELOG.md).\n\n## Table of Contents\n\n- [Features](#features)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Configuration](#configuration)\n- [Engine](#engine)\n- [API Endpoints](#api-endpoints)\n- [Collections](#collections)\n- [Database and upgrades](#database-and-upgrades)\n- [Upgrading](#upgrading)\n- [Package Exports](#package-exports)\n- [Requirements](#requirements)\n- [Uninstall](#uninstall)\n- [Migration from `@consilioweb/spellcheck`](#migration-from-consiliowebspellcheck)\n- [Support](#support)\n- [License](#license)\n\n## Features\n\n- **Dashboard** at `/admin/spellcheck` — sortable results table (score, issues, word count,\n  readability, last check), expandable rows, per-issue fix / ignore / add-to-dictionary, bulk scan\n  with live progress, and a dictionary tab.\n- **Sidebar field** in the editor of every target collection — score badge, readability badge, issue\n  cards with one-click fix, manual correction, ignore, add to dictionary.\n- **Score column** in collection list views (labelled `Ortho`), removable with `addListColumn: false`.\n- **Auto-check on save** — non-blocking `afterChange` hook. It skips autosave ticks and the plugin's\n  own writes, so typing does not flood the LanguageTool API.\n- **LanguageTool engine** — spelling, grammar and punctuation through the free public API (no key\n  required), or any self-hosted instance via `languageToolUrl`.\n- **Claude semantic pass (opt-in)** — coherence, tone, phrasing and missing words, *in addition to*\n  LanguageTool. A Claude failure degrades the result; it never fails the request.\n- **Readability analysis** — Flesch-Kincaid (English) / Kandel-Moles (French), stored with every\n  result and displayed in the dashboard and the sidebar.\n- **Consistency check** — mixed variants of the same term in a document (`TypeScript` vs\n  `typescript`, `e-commerce` vs `ecommerce`). Computed, stored on the result and returned by\n  `/validate`; no dedicated UI yet.\n- **Two-source dictionary** — static `customDictionary` from the config, merged with a\n  `spellcheck-dictionary` collection editors manage from the dashboard (add, import, export, search,\n  bulk delete), cached in memory for 5 minutes.\n- **Offset-based fixes** — a correction applied at the reported offset (or at the offset a drift\n  search recovered) is re-read from the mutated document and refused rather than persisted when the\n  replacement did not land where it should. The last-resort substring fallback carries no offset, so\n  that verification does not cover it. A pending draft is corrected as a draft, leaving the published\n  version untouched.\n- **i18n, sidebar only** — the editor sidebar field and the issue cards it renders follow Payload's\n  active locale (French / English). The `/admin/spellcheck` dashboard is **not** translated: its\n  labels are French, and the issue cards inside it fall back to their French strings.\n\n## Installation\n\n```bash\n# npm\nnpm install @consilioweb/payload-spellcheck\n\n# pnpm\npnpm add @consilioweb/payload-spellcheck\n\n# yarn\nyarn add @consilioweb/payload-spellcheck\n```\n\n| Peer dependency | Version | Required |\n|-----------------|---------|----------|\n| `payload` | `^3.79.1` | yes |\n| `@payloadcms/next` | `^3.79.1` | optional — needed for the dashboard view |\n| `@payloadcms/ui` | `^3.79.1` | optional — needed for the admin components |\n| `react` | `^18.0.0 \\|\\| ^19.0.0` | optional — needed for the admin components |\n\n> **Why `3.79.1` and not `3.0.0`?** Two reasons. Payload `< 3.79.1` is affected by a\n> pre-authentication account takeover (GHSA-hp5w-3hxx-vmwf) and an SQL injection, and this\n> plugin hands its dashboard view Payload's own `initPageResult` / `DefaultTemplate`, so a\n> vulnerable core is a vulnerable plugin. And the old `^3.0.0` was simply untrue: the dashboard\n> passes `req` to `DefaultTemplate`, a prop that only exists from `@payloadcms/next@3.44`\n> onwards, and it types itself with `AdminViewServerProps`, exported from `payload` only since\n> `3.2x`. Nothing in the plugin uses an API newer than `3.79.1`.\n\nAn installer binary is shipped with the package. It adds the plugin call to your plugins file\n(`src/plugins/index.ts`, `src/plugins.ts`, or any file exporting `plugins` as a `Plugin[]`) and\nregenerates the import map:\n\n```bash\nnpx spellcheck-install --collections pages,posts --language fr\n```\n\n## Quick Start\n\n```ts\n// src/payload.config.ts\nimport { buildConfig } from 'payload'\nimport { spellcheckPlugin } from '@consilioweb/payload-spellcheck'\n\nexport default buildConfig({\n  // ...\n  plugins: [\n    spellcheckPlugin({\n      collections: ['pages', 'posts'],\n      language: 'fr',\n    }),\n  ],\n})\n```\n\nThen regenerate the import map:\n\n```bash\nnpx payload generate:importmap\n```\n\n> [!WARNING]\n> **Prerequisite — the admin role.** By default every endpoint, the dashboard view **and the two\n> plugin collections** are reserved for users carrying `role: 'admin'`, or a `roles` array containing\n> `'admin'`. On a Payload install whose `Users` collection has no such field, **nothing is readable**:\n> the endpoints answer `403`, the dashboard redirects to `/admin`, and the sidebar score stays empty.\n> Either add the field, or pass your own check:\n>\n> ```ts\n> spellcheckPlugin({\n>   // `req` is passed directly — NOT destructured as `{ req }`\n>   access: (req) => req.user?.collection === 'users',\n> })\n> ```\n>\n> Do **not** write `access: (req) => Boolean(req.user)`. If your project has a second auth\n> collection — customers, members, partners — those accounts also carry a `payload-token`, and\n> `Boolean(req.user)` would hand them the drafts of every scanned collection plus the ability to\n> rewrite published documents through `/fix`. Always compare `req.user.collection` with the\n> collection that backs your admin panel (`config.admin.user`, usually `users`). The plugin now\n> enforces that comparison itself, before your `access` function runs, so a permissive snippet is no\n> longer an open door — but the explicit check is still what you should ship.\n\nThe plugin then:\n\n- registers nine REST endpoints under `/api/spellcheck` (see [API Endpoints](#api-endpoints));\n- creates the `spellcheck-results` and `spellcheck-dictionary` collections (hidden from the admin nav);\n- adds a sidebar field and a score column to every collection listed in `collections`;\n- registers the dashboard view at `/admin/spellcheck` (Results + Dictionary tabs);\n- adds an `afterChange` hook that re-checks a document on save;\n- on init, checks that `spellcheck-dictionary` is queryable and, if the\n  `payload_locked_documents_rels.spellcheck_dictionary_id` column is missing, logs the `ALTER TABLE`\n  that adds it (see [Database and upgrades](#database-and-upgrades) — it only runs the statement\n  itself on a database client that exposes a synchronous `exec()`, which `@payloadcms/db-sqlite`\n  does not).\n\n## Configuration\n\n```ts\nspellcheckPlugin({\n  // Target collections (default: ['pages', 'posts'])\n  collections: ['pages', 'posts'],\n\n  // Rich text field name (default: 'content')\n  contentField: 'content',\n\n  // LanguageTool language code (default: 'fr')\n  language: 'fr',\n\n  // ── Filtering ──────────────────────────────────────\n\n  // Rule IDs to skip — ADDED to the built-in defaults, never replacing them.\n  // An ID already present in DEFAULT_SKIP_RULES (or whose whole category is\n  // skipped) changes nothing: check the exported lists first, and take IDs\n  // from the `ruleId` field of a /validate response.\n  // Never list a spelling rule here (MORFOLOGIK_*): those carry the\n  // misspellings the plugin exists to report. Use customDictionary for\n  // legitimate proper nouns instead.\n  // Below: LanguageTool's language-independent word-repetition rule. Its\n  // French variant, FRENCH_WORD_REPEAT_RULE, is already a default.\n  skipRules: ['WORD_REPEAT_RULE'],\n\n  // Categories to skip — ADDED to the built-in defaults, which already\n  // contain 'TYPOGRAPHY' and 'STYLE'.\n  // 'TYPOS' is the category of every misspelling — do not add it here.\n  // Below: LanguageTool's English redundancy category, as an example.\n  skipCategories: ['REDUNDANCY'],\n\n  // Words never flagged as errors, merged with the DB dictionary\n  customDictionary: ['Next.js', 'Payload', 'TypeScript', 'ConsilioWEB'],\n\n  // ── Claude semantic pass (optional) ────────────────\n\n  enableAiFallback: false,\n  anthropicApiKey: process.env.ANTHROPIC_API_KEY,\n\n  // ── Access control ─────────────────────────────────\n\n  // The whole request is the argument — do NOT destructure it as `{ req }`.\n  // Gates the endpoints, the dashboard view AND the two plugin collections.\n  access: (req) => req.user?.role === 'admin',\n})\n```\n\n### Options reference\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `collections` | `string[]` | `['pages', 'posts']` | Collections to check. Also the allowlist every endpoint validates its `collection` against |\n| `contentField` | `string` | `'content'` | Rich text field to extract |\n| `language` | `string` | `'fr'` | LanguageTool language code |\n| `checkOnSave` | `boolean` | `true` | Add the `afterChange` auto-check hook |\n| `addSidebarField` | `boolean` | `true` | Add the sidebar field to the editor |\n| `addDashboardView` | `boolean` | `true` | Register the `/admin/spellcheck` view |\n| `addListColumn` | `boolean` | `true` | Add the `Ortho` score column to list views |\n| `endpointBasePath` | `string` | `'/spellcheck'` | Base path of the REST endpoints. See the caveat below |\n| `enableAiFallback` | `boolean` | `false` | Enable the Claude semantic pass |\n| `anthropicApiKey` | `string` | — | Anthropic API key, required when `enableAiFallback` is on |\n| `skipRules` | `string[]` | `[]` | Rule IDs to skip — **added** to the default list |\n| `skipCategories` | `string[]` | `[]` | Categories to skip — **added** to the default list |\n| `overrideDefaultSkipRules` | `string[]` | — | Replaces the default rule list instead of stacking on it. `[]` disables default rule filtering |\n| `overrideDefaultSkipCategories` | `string[]` | — | Replaces the default category list. `[]` disables default category filtering |\n| `customDictionary` | `string[]` | `[]` | Words never flagged, merged with the DB dictionary |\n| `languageToolUrl` | `string` | `'https://api.languagetool.org/v2/check'` | LanguageTool endpoint — set it for a self-hosted instance |\n| `warningThreshold` | `number` | `80` | **Accepted but currently unread.** The score colours in the UI are hard-coded (green ≥ 95, amber ≥ 80, red below) |\n| `autoFixSchema` | `boolean` | `true` | On init, probe `spellcheck-dictionary` and report the missing `payload_locked_documents_rels` column. See [Database and upgrades](#database-and-upgrades) before leaving it on |\n| `maxDocs` | `number` | — | Cap the number of documents a single bulk scan processes. Unset = no cap |\n| `acknowledgePublicApi` | `boolean` | `false` | Silence the start-up warning about sending content to the public LanguageTool API (see [Data sent to third parties](#data-sent-to-third-parties)) |\n| `access` | `(req) => boolean` | admin only | Gates the endpoints, the dashboard view and both plugin collections. The whole request is the argument |\n| `packageName` | `string` | `'@consilioweb/payload-spellcheck'` | Package name used to build admin component paths — for monorepos and aliased installs |\n| `trustProxy` | `boolean` | `true` | Trust `x-forwarded-for` / `x-real-ip` in the rate limiter. Only used for the fallback bucket — the limiter keys on the authenticated account |\n| `rateLimits` | `object` | see below | Per-endpoint rate limits |\n| `timeouts` | `object` | see below | Timeouts and length limits |\n\n> [!NOTE]\n> `endpointBasePath` moves the REST endpoints only. The bundled dashboard and sidebar call\n> `/api/spellcheck/...` literally, so changing the base path breaks the admin UI while leaving the\n> API usable.\n\n> [!NOTE]\n> The access check runs **before** the rate limiter, and the limiter keys on\n> `user.collection:user.id` rather than on the client IP. An anonymous caller is answered `403`\n> without ever creating an entry in the limiter, so it can no longer grow the limiter's memory with\n> forged `X-Forwarded-For` values nor exhaust a legitimate admin's budget by forging their IP.\n> `trustProxy` now only affects the fallback bucket used when a request somehow reaches the limiter\n> without an account id.\n\n#### `rateLimits`\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `rateLimits.validate` | `number` | `30` | Max requests per window for `/validate` |\n| `rateLimits.fix` | `number` | `20` | Max requests per window for `/fix` |\n| `rateLimits.fixAll` | `number` | `5` | Max requests per window for `/fix-all` |\n| `rateLimits.bulk` | `number` | `3` | Max requests per window for `/bulk` |\n| `rateLimits.status` | `number` | `60` | Max requests per window for `/status` (the dashboard polls it every 2 s) |\n| `rateLimits.dictionary` | `number` | `60` | Max requests per window for `/dictionary` |\n| `rateLimits.windowMs` | `number` | `60000` | Rate limit window, in milliseconds |\n\n#### `timeouts`\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `timeouts.languageTool` | `number` | `30000` | LanguageTool request timeout (ms) |\n| `timeouts.claude` | `number` | `60000` | Claude request timeout (ms) |\n| `timeouts.maxTextLengthLanguageTool` | `number` | `18000` | Max characters sent to LanguageTool (text is truncated) |\n| `timeouts.maxTextLengthClaude` | `number` | `8000` | Max characters sent to Claude (text is truncated) |\n| `timeouts.bulkRateLimitDelay` | `number` | `3000` | Delay between LanguageTool calls during a bulk scan (ms) |\n| `timeouts.bulkStaleTimeout` | `number` | `600000` | A bulk job with no progress for this long is declared dead (ms) |\n\n### Default skip lists\n\n`DEFAULT_SKIP_RULES` and `DEFAULT_SKIP_CATEGORIES` are exported from the package root, so a custom\nlist can be derived from them rather than retyped:\n\n```ts\nimport { spellcheckPlugin, DEFAULT_SKIP_RULES } from '@consilioweb/payload-spellcheck'\n\nspellcheckPlugin({\n  overrideDefaultSkipRules: DEFAULT_SKIP_RULES.filter((r) => r !== 'DASH_RULE'),\n})\n```\n\nThe defaults cover whitespace, typography, dash and repetition noise typical of CMS extraction.\nSince 0.15.0 they deliberately **do not** contain the `TYPOS` category nor `MORFOLOGIK_RULE_FR_FR`:\nthose carry the misspellings the plugin exists to report. To restore the pre-0.15 silence:\n`skipCategories: ['TYPOS'], skipRules: ['MORFOLOGIK_RULE_FR_FR']`.\n\n## Engine\n\n### Text extraction\n\nText is extracted by walking the document in this order, each part contributing one segment:\n\n1. `title` — plain string\n2. `hero.richText` — Lexical JSON\n3. the configured `contentField` — Lexical JSON\n4. `layout[]` — each block's rich text, plain text fields and nested columns\n\nCode blocks are skipped, as are values that look like IDs, URLs or dates. Each segment records how\nmuch leading whitespace the trim removed, so a correction lands on the character the offset really\npoints at.\n\n### LanguageTool\n\n- Default endpoint: `POST https://api.languagetool.org/v2/check` — free, no authentication\n- Text truncated to 18 000 characters (`timeouts.maxTextLengthLanguageTool`)\n- 30 s request timeout (`timeouts.languageTool`)\n- 3 s between calls during a bulk scan (`timeouts.bulkRateLimitDelay`)\n\nA failed call is never reported as a clean document: `/validate` answers `502` with\n`{ \"checkFailed\": true }`, the auto-check hook logs a warning and leaves the stored result alone, and\na bulk scan counts the document in its `failed` counter.\n\n### Data sent to third parties\n\nThe plugin makes outbound calls with your document content. Know what leaves before you install it.\n\n| Destination | When | What is sent | How to stop it |\n|-------------|------|--------------|----------------|\n| `https://api.languagetool.org/v2/check` (default) | Every save of a document in `collections` (`checkOnSave`), every `/validate`, every document of a bulk scan | Up to 18 000 characters of extracted text — title, hero, rich text, every layout block — **including drafts that were never published**, in clear, form-urlencoded, with no API key | Set `languageToolUrl` to a self-hosted LanguageTool, or `checkOnSave: false` to limit it to explicit checks |\n| `https://api.anthropic.com/v1/messages` | Only when `enableAiFallback: true` **and** `anthropicApiKey` is set | Up to 8 000 characters of the same extracted text | Leave `enableAiFallback` off (the default) |\n\nNothing else leaves the host. Results are stored in your own database.\n\nBecause the public LanguageTool endpoint is the default, the plugin logs a warning at boot when no\n`languageToolUrl` is configured. If the transfer is acceptable for your project — and, under GDPR,\ndocumented in your processing register — set `acknowledgePublicApi: true` to silence it.\n\nSelf-hosting takes one line:\n\n```ts\nspellcheckPlugin({\n  languageToolUrl: 'http://languagetool.internal:8010/v2/check',\n})\n```\n\n### Filtering\n\nEvery LanguageTool match goes through these layers, in order:\n\n1. premium-only rules\n2. `skipRules` (defaults + your additions, or your `overrideDefaultSkipRules`)\n3. `skipCategories` (same logic)\n4. exact dictionary match on the flagged word\n5. dictionary entry contained in the flagged word — entries of 5+ characters only, on a word boundary\n6. multi-word dictionary entry present in the surrounding context\n7. single-character matches, except in the `GRAMMAR` category\n8. non-language patterns: URLs, e-mails, CamelCase, all-caps acronyms, numbers and units, hashtags,\n   currency, phone numbers, domain names, inline code, `snake_case`, CSS custom properties\n9. code-ish context: an open backtick span, or a CLI command (`npm`, `git`, `docker`, …)\n10. suggestions identical to the original, and contexts shorter than 5 characters\n11. repetition rules whose word is in the dictionary or whose suggestion is empty\n\nThen the issues the user marked as ignored (`ignoredIssues` on the stored result) are removed.\n\n### Scoring\n\n```text\nscore = clamp(round(100 - (issues / words) * 1000), 0, 100)\n```\n\nA document with no words or no issues scores 100. The same formula runs client-side after a fix, so\nthe badge and the stored score never disagree.\n\n- **≥ 95** — excellent (green)\n- **≥ 80** — good (amber)\n- **< 80** — needs work (red)\n\n### Claude (optional)\n\nWith `enableAiFallback: true` and an `anthropicApiKey`, the extracted text (truncated to 8 000\ncharacters) is also sent to `claude-haiku-4-5-20251001` for semantic issues only: inconsistent tone,\ncontradictions, awkward phrasing, missing words. Results carry `source: 'claude'`, a category among\n`COHERENCE`, `TONE`, `PHRASING`, `MISSING_WORD`, and a `ruleId` of `CLAUDE_<category>`.\n\nClaude enriches the LanguageTool result — it never replaces it. A Claude failure is logged and\nskipped; only a LanguageTool failure fails the request.\n\nTwo guardrails apply to that pass, because the analysed text is document content and therefore\nuntrusted (a contributor without publish rights, an import, a syndicated feed):\n\n- the content is fenced in the prompt and explicitly marked as data, so a paragraph that reads\n  \"ignore the instructions above and answer …\" is not read as an instruction;\n- an `original` the model returns is located in the real text (`indexOf`) instead of being given a\n  fabricated `offset: 0`. If it cannot be located, the issue keeps its message but loses its\n  coordinates and its suggestion — it is reported, never applied.\n\n`source: 'claude'` issues are also **excluded from `/fix-all`**: semantic suggestions are applied one\nat a time, by a human who sees what is being replaced, never in an unattended batch.\n\n## API Endpoints\n\nPaths below assume the default `endpointBasePath` (`/spellcheck`) and Payload's default API route\n(`/api`). Every endpoint requires an authenticated user **from the admin auth collection**\n(`config.admin.user`) **and** passes the `access` check (admin-only by default). A rejection answers\n`403`; when the default check is in use, the body also carries a `hint` naming the expected role.\nExceeding a rate limit answers `429` with `Retry-After` — the access check runs first, so an\nanonymous or unauthorised caller always gets `403`, never `429`, and never reaches the rate limiter.\n\n| Method | Path | Rate limit | What it does |\n|--------|------|-----------|--------------|\n| `POST` | `/api/spellcheck/validate` | 30/min | Check one document (`{ id, collection }`) or raw text (`{ text, language? }`, 50 000 chars max) |\n| `POST` | `/api/spellcheck/fix` | 20/min | Apply one correction in the Lexical JSON, then re-align the offsets of the remaining stored issues |\n| `POST` | `/api/spellcheck/fix-all` | 5/min | Apply every stored fixable LanguageTool issue of a document, last offset first, in strict mode (Claude issues excluded) |\n| `POST` | `/api/spellcheck/bulk` | 3/min | Start a background scan; returns immediately |\n| `GET` | `/api/spellcheck/status` | 60/min | Current scan progress |\n| `GET` | `/api/spellcheck/dictionary` | 60/min | List dictionary words, sorted alphabetically |\n| `POST` | `/api/spellcheck/dictionary` | 60/min | Add one or several words |\n| `DELETE` | `/api/spellcheck/dictionary` | 60/min | Remove words by id or by word |\n| `GET` | `/api/spellcheck/collections` | — | The collections the plugin is configured for |\n\n### POST `/api/spellcheck/validate`\n\n```json\n// Check a document by ID\n{ \"id\": \"123\", \"collection\": \"pages\" }\n\n// Check raw text\n{ \"text\": \"Ceci est une test.\", \"language\": \"fr\" }\n```\n\n**Response** — also stored in `spellcheck-results` when `id` and `collection` are given:\n\n```json\n{\n  \"docId\": \"123\",\n  \"collection\": \"pages\",\n  \"score\": 85,\n  \"issueCount\": 1,\n  \"wordCount\": 450,\n  \"issues\": [\n    {\n      \"ruleId\": \"GRAMMAR\",\n      \"category\": \"GRAMMAR\",\n      \"message\": \"Le déterminant « une » ne correspond pas…\",\n      \"context\": \"Ceci est une test.\",\n      \"contextOffset\": 9,\n      \"offset\": 9,\n      \"length\": 3,\n      \"original\": \"une\",\n      \"replacements\": [\"un\"],\n      \"source\": \"languagetool\"\n    }\n  ],\n  \"readability\": { \"score\": 62, \"grade\": \"Facile\", \"avgSentenceLength\": 14.2, \"avgSyllablesPerWord\": 1.7, \"sentenceCount\": 32, \"wordCount\": 450 },\n  \"consistency\": [{ \"term\": \"ecommerce\", \"variants\": [{ \"text\": \"e-commerce\", \"count\": 3 }, { \"text\": \"ecommerce\", \"count\": 1 }] }],\n  \"lastChecked\": \"2026-09-07T20:30:00.000Z\"\n}\n```\n\n`403` when the collection is not in the plugin's `collections`, `404` when the document does not\nexist, `400` when the text exceeds 50 000 characters, `502` with `{ \"checkFailed\": true }` when\nLanguageTool is unreachable.\n\n### POST `/api/spellcheck/fix`\n\n```json\n{\n  \"id\": \"123\",\n  \"collection\": \"pages\",\n  \"original\": \"une test\",\n  \"replacement\": \"un test\",\n  \"offset\": 42,\n  \"length\": 8,\n  \"field\": \"content\"\n}\n```\n\n`offset` and `length` target the correction precisely. The endpoint falls back to rewriting the\nfirst matching substring of the document when they are missing — and also when they are supplied but\nneither the exact offset nor the drift search finds `original` there. `field` restricts the search to\none top-level field.\n\n**Response** (`FixResult`):\n\n```json\n{\n  \"success\": true,\n  \"fixesApplied\": 1,\n  \"original\": \"une test\",\n  \"replacement\": \"un test\",\n  \"method\": \"offset\",\n  \"target\": \"draft\",\n  \"appliedOffset\": 42,\n  \"appliedLength\": 8\n}\n```\n\n`target` says where the correction went: `draft` when the document had a pending draft — the\npublished version is left untouched — `published` otherwise. `method` says how the text was located:\n`offset` (exact hit), `search` (offset drift recovered) or `legacy` (substring fallback).\n\nThe `offset` and `search` paths are re-read from the mutated document before saving: when the\nreplacement did not land at the applied offset, nothing is persisted and the response comes back with\n`success: false` and an `error`. The `legacy` path produces no coordinates, so that verification does\nnot apply to it — it is saved on the strength of the substring match alone, and answers\n`success: true` with `method: 'legacy'` and no `appliedOffset`.\n\nOn success the endpoint also rewrites the stored `spellcheck-results` row: the corrected issue is\ndropped and the offsets of the remaining ones are shifted by the length difference, with no\nLanguageTool call. That re-alignment needs `appliedOffset`, which the legacy substring fallback does\nnot produce.\n\n### POST `/api/spellcheck/fix-all`\n\n```json\n{ \"id\": \"123\", \"collection\": \"pages\" }\n```\n\nApplies every stored **LanguageTool** issue that has at least one suggestion, walking the document\nfrom the **last** offset to the first so a length-changing replacement never invalidates the offsets\nstill to come. Issues carrying `source: 'claude'` are skipped: their `original`/`suggestion` pair\ncomes from a model that has just read the document, so replaying them unattended would let content\nchoose the string written into a published page. Apply those one by one from the dashboard.\nStrict mode: no fuzzy match, no substring fallback — an issue whose offset no longer matches its\n`original` is counted as `failed` rather than applied somewhere else. One re-check runs after the\nbatch, but only when at least one fix was applied: a batch where everything failed answers\n`rechecked: false` and leaves the stored result as it was.\n\n**Response** (`FixAllResult`): `{ \"applied\": 4, \"failed\": 1, \"details\": [...], \"rechecked\": true }`.\n`404` when the document has no stored result yet.\n\n### POST `/api/spellcheck/bulk`\n\n```json\n// Scan every configured collection\n{}\n\n// Scan one collection\n{ \"collection\": \"posts\" }\n\n// Scan specific documents\n{ \"ids\": [{ \"id\": \"123\", \"collection\": \"pages\" }] }\n\n// Reset a stuck scan and start over\n{ \"force\": true }\n```\n\nAnswers `{ \"message\": \"Scan started\", \"status\": \"running\" }` and runs in the background. `409` when a\nscan is already running and `force` is not set — the body is the running job itself, in the `/status`\nshape, plus `\"error\": \"Scan already in progress\"`, so the caller can display the progress of the scan\nthat blocks it. `403` when `collection`, or any `ids[]` entry, names a collection outside the\nplugin's `collections` — a malformed entry (missing `collection`, falsy entry) rejects the whole\nrequest. Collections without `versions.drafts` are scanned in full; the\nothers are limited to published documents.\n\n### GET `/api/spellcheck/status`\n\n`{ \"status\": \"idle\" }` when no scan ever ran, otherwise the live job:\n\n```json\n{\n  \"status\": \"running\",\n  \"current\": 12,\n  \"total\": 40,\n  \"currentDoc\": \"Accueil\",\n  \"totalIssues\": 87,\n  \"totalDocuments\": 12,\n  \"failed\": 1,\n  \"averageScore\": 0,\n  \"startedAt\": \"2026-09-07T20:30:00.000Z\",\n  \"completedAt\": null,\n  \"error\": null,\n  \"lastActivity\": 1757277000000\n}\n```\n\n`failed` counts documents whose check could not run. `lastActivity` is the epoch timestamp (ms) of\nthe last progress update: a job with no progress for `bulkStaleTimeout` flips to `status: 'error'`\nwith `error: 'Scan timed out (no progress)'`, and the flip happens on the next `/status` or `/bulk`\ncall, not on a timer.\n\n### Dictionary\n\n```json\n// GET  → { \"words\": [{ \"id\": \"1\", \"word\": \"typescript\", \"addedBy\": {...}, \"createdAt\": \"…\" }], \"count\": 1 }\n\n// POST → { \"added\": [\"typescript\"], \"skipped\": [], \"count\": 1 }\n{ \"word\": \"TypeScript\" }\n{ \"words\": [\"TypeScript\", \"Next.js\", \"Payload\"] }\n\n// DELETE → { \"deleted\": 2 }\n{ \"id\": \"abc123\" }\n{ \"ids\": [\"abc123\", \"def456\"] }\n{ \"word\": \"typescript\" }\n```\n\n`DELETE` also reads `id` from the query string — `DELETE /api/spellcheck/dictionary?id=abc123`, for\nclients that send no body. The forms accumulate: body `id`, query `id`, `ids[]` and the id resolved\nfrom `word` are all deleted, and `deleted` counts the ones that existed. Supplying none of them\nanswers `400`.\n\nWords are trimmed, lower-cased and capped at 100 characters. Adding an existing word is a skip, not\nan error. Every write invalidates the 5-minute in-memory cache.\n\n### GET `/api/spellcheck/collections`\n\n`{ \"collections\": [\"pages\", \"posts\"] }` — used by the dashboard to build its collection filter.\n\n## Collections\n\nBoth collections are created by the plugin and hidden from the admin nav. Since 0.15.0 they share\nthe endpoints' `access` check for read, create, update and delete: admin-only by default. The plugin\nwrites to them internally with `overrideAccess: true`, so tightening the gate does not affect scans\nor fixes.\n\n| Slug | Role | Read / write |\n|------|------|--------------|\n| `spellcheck-results` | One row per checked document: score, issues, ignored issues, readability, consistency | the plugin's `access` check |\n| `spellcheck-dictionary` | One document per whitelisted word | the plugin's `access` check |\n\n**`spellcheck-results` fields** — `docId`, `collection`, `title`, `slug`, `score`, `issueCount`,\n`wordCount`, `issues` (JSON), `ignoredIssues` (JSON), `readability` (JSON), `consistency` (JSON),\n`lastChecked`. Payload timestamps are disabled on this collection.\n\n**`spellcheck-dictionary` fields** — `word` (text, unique, indexed, max 100, lower-cased on save),\n`addedBy` (relationship to `users`).\n\n**Scope** — `word` is `unique: true` at the level of the whole Payload instance. The dictionary is\ntherefore shared by every collection and every site served by that instance: adding `Dupont` once\nsilences it everywhere. That follows the model these plugins assume — one Payload instance = one\nsite — but it means the plugin is not usable per-tenant alongside\n`@payloadcms/plugin-multi-tenant`: two tenants cannot hold the same word with different intents,\nand the second insert fails on the unique index.\n\n## Database and upgrades\n\n- This plugin **adds collections to your Payload config**; it does not own your schema. The tables\n  belong to your app, and so does the responsibility for migrating them.\n- **Payload gives a plugin no way to ship migrations.** `payload migrate` reads a single directory,\n  the host app's (`payload.db.migrationDir`, resolved in the app's own cwd). A migration file\n  published inside an npm package is never discovered.\n- **In development**, `push` synchronises the schema automatically when the plugin's collections\n  appear for the first time.\n- **In production**, run `payload migrate:create` then `payload migrate`. Never `push`: Payload\n  skips it as soon as `NODE_ENV=production`, and mixing it with migrations triggers a data-loss\n  warning.\n- **No option of this plugin toggles the schema.** The two tables it creates —\n  `spellcheck-results`, `spellcheck-dictionary` — appear as soon as the plugin is in `plugins`,\n  whatever the options. Everything it injects into *your* collections (`_spellcheck`,\n  `_spellcheckScore`, gated by `addSidebarField` and `addListColumn`) is `type: 'ui'` and creates no\n  column, so growing the `collections` list never produces a migration either.\n- **Every release of this plugin states in its [Upgrading](#upgrading) section whether it changes\n  the schema.** None has since 0.15.0.\n\n### `autoFixSchema`, and why it is not a migration\n\n`autoFixSchema` (default `true`) queries `spellcheck-dictionary` once at boot. If that query fails\nwith a missing-column error, the plugin reports:\n\n```\nALTER TABLE payload_locked_documents_rels ADD COLUMN spellcheck_dictionary_id integer\n```\n\nTwo things to know before leaving it enabled:\n\n1. **It is a raw DDL statement on a Payload CORE table, outside the `payload-migrations` ledger.**\n   If it ever does run, your database gains a column no migration created. A later `payload migrate`\n   that tries to add the same column will fail on it, and the next `migrate:create` will be\n   generated against a schema snapshot that does not match the database.\n2. **On the adapters in the supported peer range it does not run — it only logs.** Executing the\n   statement requires a database client exposing a synchronous `exec()`.\n   `@payloadcms/db-sqlite` builds its client with `createClient` from `@libsql/client`, whose API is\n   `execute()` / `executeMultiple()`; PostgreSQL and MongoDB expose neither. In practice the plugin\n   prints the statement and leaves the decision to you — which is the intended behaviour.\n\nSet `autoFixSchema: false` to skip the probe entirely. When it is the only reason the plugin\ninstalls an `onInit` hook, opting out removes the hook altogether.\n\n## Upgrading\n\n### 0.15.x → 0.16.0 → 0.17.0\n\n**No schema change. No migration to generate**, in either release.\n\nVerified by diffing `src/collections` between `v0.15.0` and `HEAD`: the only change to either\ncollection is the *type of the parameter* the access guard takes. No field was added, removed,\nrenamed or retyped; no index or `unique` flag moved. An access change is not a schema change.\n\n**0.15.x → 0.16.0 — behaviour change, no data change.** The guard now also compares\n`req.user.collection` with the collection backing the admin panel (`config.admin.user`), and it\ndoes so *before* your own `access` function runs. An account that authenticated against a second\nauth collection — customers, members, partners — is refused even when your `access` returns `true`\nfor it. If a non-admin auth collection was reaching the spellcheck endpoints or the two plugin\ncollections, it stops. That is the point of the release. See [Quick Start](#quick-start) for the\n`access` shape to ship.\n\n**0.16.x → 0.17.0 — packaging only.** The three Payload peers move from `^3.0.0` to `^3.79.1`.\nNo runtime code changed. Check the version you actually resolve, not the one you declare:\n`npm ls payload` / `pnpm why payload`. Anything below `3.79.1` needs a Payload upgrade — bumping\nthis plugin alone changes nothing about the core already on disk.\n\nWorth knowing while you are there:\n\n- **The fields the plugin injects into your own collections are `type: 'ui'`.** `_spellcheck` (the\n  sidebar panel) and `_spellcheckScore` (the list column) are presentation-only: they create **no\n  column, in any adapter**. Adding a collection to the `collections` option therefore never requires\n  a migration. If `payload migrate:create` produces a file right after you added one, the change\n  came from somewhere else in your config.\n- The two collections the plugin *does* create — `spellcheck-results` and `spellcheck-dictionary` —\n  are real tables. They appear the first time the plugin is installed, and that first install is the\n  only moment this plugin has ever needed a migration.\n\n## Package Exports\n\n| Subpath | Exposes | Environment |\n|---------|---------|-------------|\n| `@consilioweb/payload-spellcheck` | Plugin, engine functions, types | server |\n| `@consilioweb/payload-spellcheck/client` | Admin components | client (`'use client'`) |\n| `@consilioweb/payload-spellcheck/views` | Dashboard view | server (RSC) |\n\n### Root entry\n\n```ts\nimport {\n  // Plugin\n  spellcheckPlugin,\n  // Extraction\n  extractTextFromLexical, extractAllTextFromDoc, extractAllTextFromDocWithSources, countWords,\n  // Engines — prefer the run* forms, whose outcome distinguishes failure from a clean text\n  runLanguageToolCheck, runClaudeCheck,\n  checkWithLanguageTool, checkWithClaude, // @deprecated — return [] on failure\n  // Filtering and scoring\n  filterFalsePositives, calculateScore, DEFAULT_SKIP_RULES, DEFAULT_SKIP_CATEGORIES,\n  // Analysis\n  analyzeReadability, checkConsistency,\n  // Dictionary cache\n  loadDictionaryWords, invalidateDictionaryCache,\n  // Fix-all handler, for a custom endpoint registration\n  createFixAllHandler,\n  // i18n\n  getTranslations, getScoreLabel,\n} from '@consilioweb/payload-spellcheck'\n\nimport type {\n  SpellCheckPluginConfig, SpellCheckIssue, SpellCheckResult,\n  TextSegment, ExtractedDoc,\n  LanguageToolOutcome, ClaudeOutcome,\n  ReadabilityResult, ConsistencyIssue,\n  FixAllResult,\n  SpellcheckLocale, SpellcheckTranslations,\n} from '@consilioweb/payload-spellcheck'\n```\n\n### Client entry\n\n```ts\nimport {\n  SpellCheckField,\n  SpellCheckDashboard,\n  IssueCard,\n  SpellCheckScoreCell,\n} from '@consilioweb/payload-spellcheck/client'\n```\n\n### Views entry\n\n```ts\nimport { SpellCheckView } from '@consilioweb/payload-spellcheck/views'\n```\n\n## Requirements\n\n- **Node.js** — `>= 18`\n- **Payload CMS** — `^3.79.1` (required peer dependency — `3.79.1` is the first release free of\n  GHSA-hp5w-3hxx-vmwf, and no API used here is newer than it)\n- **`@payloadcms/next`**, **`@payloadcms/ui`** — `^3.79.1`, optional peers, needed by the admin UI\n  (they ship in lockstep with `payload`; the dashboard needs the `req` prop `DefaultTemplate`\n  gained in `3.44`)\n- **React** — `^18.0.0 || ^19.0.0`, optional peer, needed by the admin UI\n- **Next.js** — 15, or 16 with the Turbopack workaround at the top of this file\n- **Database** — any Payload adapter. `autoFixSchema` writes nothing on `@payloadcms/db-sqlite`,\n  PostgreSQL or MongoDB: it logs the `ALTER TABLE` for you to run. See\n  [Database and upgrades](#database-and-upgrades).\n\n## Uninstall\n\n### Automatic\n\n```bash\nnpx spellcheck-uninstall\n```\n\nIt removes the plugin's imports and calls from your source files, removes the dependency,\nregenerates the import map — and, **on SQLite only**, drops the plugin tables and indexes.\n\n- `--keep-data` — keep the database tables.\n- `--force-db` — drop the tables even when the plugin is not detected in the project. The database\n  step is the only irreversible one, so by default it runs only when the plugin is actually found\n  (a cleaned source file, or the dependency declared in `package.json`).\n\n> **The database step is SQLite-only, and it fails quietly elsewhere.** The script looks for `*.db`\n> files in the project root and in `data/`, then shells out to the `sqlite3` binary. On PostgreSQL\n> and on MongoDB it finds no such file, reports nothing, and leaves both tables in place. The source\n> and dependency cleanup still runs correctly — only the data is left behind. Use the manual\n> statements below on those adapters.\n\n### Manual\n\n1. Remove the plugin from your config.\n2. Run `npx payload generate:importmap`.\n3. Optionally drop the tables. **Pick the block for your adapter** — the automatic script only ever\n   covers the first one:\n\n```sql\n-- SQLite\nDROP INDEX IF EXISTS `spellcheck_results_doc_id_idx`;\nDROP INDEX IF EXISTS `spellcheck_results_collection_idx`;\nDROP INDEX IF EXISTS `spellcheck_results_last_checked_idx`;\nDROP INDEX IF EXISTS `spellcheck_dictionary_word_idx`;\nDROP TABLE IF EXISTS `spellcheck_results`;\nDROP TABLE IF EXISTS `spellcheck_dictionary`;\n\n-- Also clear the rows the lock table keeps for those documents:\nDELETE FROM `payload_locked_documents_rels` WHERE `spellcheck_results_id` IS NOT NULL;\nDELETE FROM `payload_locked_documents_rels` WHERE `spellcheck_dictionary_id` IS NOT NULL;\n```\n\n```sql\n-- PostgreSQL\n-- Indexes go with the tables, so a single CASCADE is enough.\nDELETE FROM payload_locked_documents_rels WHERE spellcheck_results_id IS NOT NULL;\nDELETE FROM payload_locked_documents_rels WHERE spellcheck_dictionary_id IS NOT NULL;\nDROP TABLE IF EXISTS spellcheck_results CASCADE;\nDROP TABLE IF EXISTS spellcheck_dictionary CASCADE;\n\n-- Optional, and only if you are NOT going to reinstall the plugin: the two\n-- foreign-key columns Payload added to its lock table.\nALTER TABLE payload_locked_documents_rels DROP COLUMN IF EXISTS spellcheck_results_id;\nALTER TABLE payload_locked_documents_rels DROP COLUMN IF EXISTS spellcheck_dictionary_id;\n```\n\n```js\n// MongoDB (mongosh) — collections, not tables, and no lock-table columns to clean.\ndb.getCollection('spellcheck-results').drop()\ndb.getCollection('spellcheck-dictionary').drop()\ndb.getCollection('payload-locked-documents').deleteMany({\n  'document.relationTo': { $in: ['spellcheck-results', 'spellcheck-dictionary'] },\n})\n```\n\nThe slugs `spellcheck-results` and `spellcheck-dictionary` are fixed — the plugin exposes no option\nto rename them — so the names above are always the right ones. What the plugin adds to **your** own\ncollections (`_spellcheck`, `_spellcheckScore`) is `type: 'ui'` and has no column to drop.\n\n## Migration from `@consilioweb/spellcheck`\n\nThis package has been renamed from `@consilioweb/spellcheck` to `@consilioweb/payload-spellcheck`.\n\n**Automatic migration (recommended):**\n\n```bash\nnpx @consilioweb/migrate\n```\n\nThis will update your `package.json` and all imports automatically.\n\n**Manual migration:**\n\n```bash\nnpm uninstall @consilioweb/spellcheck\nnpm install @consilioweb/payload-spellcheck\n```\n\nThen update your imports:\n\n```diff\n- import { spellcheckPlugin } from '@consilioweb/spellcheck'\n+ import { spellcheckPlugin } from '@consilioweb/payload-spellcheck'\n```\n\n## Support\n\n- Issues and feature requests: [GitHub issues](https://github.com/pOwn3d/payload-spellcheck/issues)\n- If this plugin saves you time: [buy me a coffee](https://buymeacoffee.com/pown3d)\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n---\n\nMade by [ConsilioWEB](https://consilioweb.fr) · [GitHub](https://github.com/pOwn3d)\n","readmeFilename":""}