beyondcode/laravel-prose-linter
Lint the prose in your Laravel Blade views: this package checks spelling, grammar, and writing style, flags unclear or inconsistent wording, and runs as an Artisan command you can drop into CI so your app's copy stays polished, consistent, and readable.
Install as a dev dependency. This is authoring/CI tooling — it should never reach a production image:
composer require --dev beyondcode/laravel-prose-linter
Before you write any code, check two things.
illuminate/* constraints (the package is tiny), or vendor src/ into your own dev-tooling namespace. Do not reach for --ignore-platform-reqs; that hides the incompatibility until something breaks at runtime.proselint (Python) does the actual checking. If that's the case, every developer machine and every CI image needs it:pipx install proselint && proselint --version
Pin the proselint version in your CI image. Rule drift between local and CI is the single most common "works on my machine" complaint with tools like this.
Read these files in order: README.md → src/ (the Artisan command signature and, critically, its exit-code contract) → the published config. There is no runtime API you'll call from application code; the command and the test helpers are the integration.
Publish config, clear caches, confirm discovery.
php artisan vendor:publish --tag=prose-config # confirm the tag in the README
php artisan config:clear
php artisan list | grep prose
Auto-discovery handles registration. If the command is missing, it's a cache/discovery problem, not a config/app.php problem.
First run: one directory you already trust.
php artisan prose:lint --path=resources/views/marketing
First impressions decide adoption. If run #1 dumps 400 findings from inline email CSS and minified partials, the tool is uninstalled within the hour. Prove signal on a narrow, curated folder before going wide.
Best first use case is lang/, not views. Translation files are UI copy written by a developer at 23:00, reviewed by nobody, and shipped to every user. A lint pass over lang/en/*.php catches double spaces, inconsistent capitalization, hedging, and the awkward phrasing that only appears under release pressure. If the package exposes test assertions (per the description, mailable/notification linting), add one assertion to an existing feature test before touching CI at all — cheapest possible way to find out whether findings are useful.
Config scoping is where the entire value lives. Start from a whitelist, never from the default scan set.
// config/prose.php
'directories' => [
resource_path('views/marketing'),
lang_path('en'),
// resource_path('views/emails'), // only after markup is excluded
],
'exclude' => [
resource_path('views/vendor'),
resource_path('views/components/ui'),
public_path(),
],
Exclude vendor views, compiled assets, JS/Vue mount points disguised as Blade, and any template where the majority of the file is markup or <style>.
Baseline, then ratchet. This is the workflow that survives:
continue-on-error: true.git diff --name-only origin/main...HEAD -- 'resources/views/**/*.blade.php' 'lang/**/*.php' \
| xargs -r -n1 php artisan prose:lint --path=
Verify whether the command accepts a single file path or only directories; if the latter, run per-directory and filter the output.
Use the test assertions as the tight loop. A mailable's copy gets linted in the same run as its subject line, so a typo fails next to its cause rather than in a separate CI stage nobody reads. Put prose assertions in the same test class as the mailable/notification, not in a distant "lint" suite.
One checked-in analyzer config for everyone. proselint reads ~/.proselintrc from the user's home directory by default, which means every developer silently runs different rules. Set PROSELINTRC (or the equivalent --config flag) to a repo-root file, in local scripts and CI alike. Disable the checks that fight your product voice rather than blanket-ignoring individual lines.
Keep a reviewed allowlist for domain terms. Product names, feature names, and acronyms will trip style rules forever. Maintain a checked-in ignore list with a comment per entry, a reason, and a review date. An allowlist without a reviewer becomes a graveyard within two quarters.
Prefer a pre-commit hook over CI. Run on staged *.blade.php and lang/** files via CaptainHook or Husky. The developer fixes the sentence while it's still in their head; CI is only the backstop.
~/.proselintrc does not exist on a fresh runner. Always point at the checked-in config explicitly.{{ $user->name }}, @if, @foreach, @include, :attribute placeholders, and HTML entities all generate false positives. Test against a real file with nested directives to see how much the wrapper strips.{{-- --}} blocks and dead markup still count as text. Exclude or delete stale copy rather than ignoring it line by line.<style> blocks. Either exclude views/emails at first or lint only the plain-text variants.lang/ to en first and validate findings on each additional locale before wiring it in.src/. If every finding is a hard failure, start with continue-on-error: true and do not promote to blocking until false positives are under control.proselint directly on a file tells you instantly whether a miss is a proselint limitation, a Blade-stripping bug, or a config problem.How can I help you explore Laravel packages today?