Editorial QA for Craft CMS 5. RedPen enforces your house style guide on every entry — banned terms, brand capitalisation, readability, structure, accessibility and typography — and can add real grammar review from a self-hosted LanguageTool server or an LLM you bring your own key for.
It is not a spellchecker. Your browser already does that, for free, in every field. RedPen checks the things a browser cannot: the rules your organisation actually wrote down.
Requirements
Craft CMS 5.3+ and PHP 8.2+.
Editions
| Lite (free) | Pro | |
|---|---|---|
| Full rule library | ✅ | ✅ |
| Review panel on entry edit | ✅ | ✅ |
| One default style profile | ✅ | ✅ |
| Multiple profiles, per-section assignment | — | ✅ |
| Save-time validation (warn or block) | — | ✅ |
| Site-wide audit, report and dashboard widget | — | ✅ |
| LanguageTool and LLM backends | — | ✅ |
| Console command for CI | — | ✅ |
| Editorial sign-off | — | ✅ |
What it checks
Twelve rules ship in the box, across seven categories.
Terminology — banned and preferred terms (utilize => use), brand and product capitalisation
(Github → GitHub).
Grammar — repeated words.
Accessibility — missing or useless image alt text, link text that means nothing out of context (click here), empty links, links that go nowhere.
Structure — headings that skip a level, are empty, or repeat.
Typography — double spaces, spaces before punctuation, missing spaces after it.
Inclusivity — gendered job titles, ableist metaphors, and terms with settled neutral equivalents.
Style and readability — weasel words, passive voice, sentence length, Flesch-Kincaid grade level.
These four ship off or as notices, because they are matters of taste and a wall of warnings on day one teaches people to ignore the plugin.
Every rule can be enabled, disabled, re-severitied and configured per profile, and profiles can be scoped to a section, a site, or both.
Severities
- Notice — recorded and shown, never in the way.
- Warning — shown prominently, saves anyway.
- Error — blocks the save, if you have turned that on.
checkOnSave and blockOnError both default to off. A plugin that starts refusing saves the moment it is installed is a support ticket, so switching that on is a deliberate act.
Backends
RedPen runs up to three checkers, in order, and merges their findings:
- Style rules — the built-in library. Pure PHP, no network, no cost, works everywhere. Always on.
- LanguageTool — point
languageToolEndpointat a self-hosted server (docker run -p 8010:8010 erikvl87/languagetool). RedPen never calls LanguageTool's public API: their terms forbid automated requests, and defaulting to it would break every install at once the first time they enforce that. - LLM — a Claude or OpenAI key of your own. Real grammar rewriting and brand-voice review, billed to you. Suggestions land in a diff with per-item accept/reject; RedPen never rewrites copy silently.
An unconfigured backend reports itself unavailable and is skipped, so a missing endpoint never breaks a check the rule engine could have answered.
Extending it
Three events, all on the plugin's services:
use justinholtweb\redpen\events\RegisterRulesEvent;
use justinholtweb\redpen\services\Rules;
use yii\base\Event;
Event::on(Rules::class, Rules::EVENT_REGISTER_RULES, function(RegisterRulesEvent $event) {
$event->rules[] = new MyHouseRule();
});
Rules::EVENT_REGISTER_RULES adds or replaces a rule (reuse an id to override a built-in),
Extractors::EVENT_REGISTER_EXTRACTORS teaches RedPen to read a custom field type, and
Checkers::EVENT_REGISTER_CHECKERS adds a validation backend.
A rule is one small class:
class NoFirstPersonRule extends PhraseRule
{
public function id(): string { return 'style.no-first-person'; }
public function label(): string { return 'No first person'; }
public function category(): string { return Issue::CATEGORY_STYLE; }
protected function phrases(): array
{
return ['I think' => null, 'in my opinion' => null];
}
}
Rules take a Fragment and return Issues. They touch no database and no Craft services, which is why the whole library is testable without booting an application.
Settings
| Setting | Default | Notes |
|---|---|---|
language | en-US | Language rules evaluate against |
checkOnLoad | true | Check when an edit screen opens |
checkOnSave | false | Evaluate on save (Pro) |
blockOnError | false | Let an error actually block the save (Pro) |
backends | ['rules'] | Which checkers run, in order |
languageToolEndpoint | '' | Self-hosted LanguageTool base URL. Env-var friendly |
llmProvider | anthropic | anthropic or openai |
llmApiKey | '' | Env-var friendly |
maxFragmentLength | 20000 | Characters per field sent to a network backend |
cacheDuration | 300 | Seconds a check result is cached |
languageToolEndpoint, llmApiKey and llmModel all parse env vars, so credentials stay out of
project config.
Running it from the command line
php craft redpen/check # everything, summary only
php craft redpen/check --sections=news,blog --verbose # list every finding
php craft redpen/check --profile=houseStyle --limit=50 # pin a profile, sample the site
php craft redpen/check --fail-on=error # exit non-zero if anything is broken
php craft redpen/check --store # keep the run as an audit in the CP
--fail-on is the one that matters in CI: it exits non-zero when anything at that severity or worse is found, so a pipeline can refuse to deploy copy that breaks the style guide. Pro only.
Development
composer install
composer test # Pest
composer phpstan # level 5
composer ecs # coding standards; ecs-fix to apply
The suite runs without booting Craft — no database, no application, ~0.2s. That is deliberate: the text layer, the rule library and the response parsing are plain PHP over plain value objects, and the network backends sit behind HttpClientInterface, so the parts that carry real logic are directly reachable.
The parts that genuinely need Craft — extractors reading field values, project-config profile storage, save-time validation, the audit query — are verified by integration runs against a local Craft install instead, because faking an element well enough to be meaningful costs more than it proves.
To exercise the LanguageTool backend locally:
docker run -d -p 8010:8010 erikvl87/languagetool
then point languageToolEndpoint at it (http://host.docker.internal:8010 from inside DDEV).
To install this plugin, copy the command above to your terminal.
This plugin doesn't have any reviews.