it-blaster/typograph-bundle
Typograph Bundle integrates the Typograf typography engine into PHP projects, automatically correcting quotes, dashes, spacing, and punctuation in Russian and other texts. It cleans HTML output and delivers polished, professional-looking content with minimal setup.
Know what you're installing. This is a Symfony bundle, not a Laravel package — there is no service provider, no facade, no publishable config, and no config/typograph.php. It wraps E. Muravjev's Typograph, a Russian-language typography engine that fixes quotes (" → «»), hyphens → em dashes, spacing before punctuation, non-breaking spaces, and similar rules. If your content is Russian (or you need those rules), it's useful; if your content is English-only, the value is near zero.
Do not composer require it into a modern Laravel app blindly. Last release was 2015, it targets Symfony 2.x, and its transitive constraints will likely collide with Laravel 9/10/11's Symfony 6/7 components. Expect composer why-not conflicts. Two viable paths:
app/Support/Typograph/ and treat it as your own code. This is usually the right call for a 1-star, 9-year-old dependency.Where to look first (in the repo):
composer.json — check the exact symfony/* constraints and PHP requirement; this tells you instantly whether adaptation or vendoring is required.Resources/config/ — the service definition and the Twig extension registration. This is the whole "bundle" part; it's small, and it's what you'll port to a service provider.rules/ directory — that's where the actual behavior lives.First use case: beautifying a body / description text column before rendering an article, product description, or email. Minimal adaptation:
// config/app.php providers
App\Providers\TypographServiceProvider::class,
// app/Providers/TypographServiceProvider.php
public function register(): void
{
$this->app->singleton(Typograph::class, function () {
return new Typograph(rulesPath: base_path('resources/typograph/rules'));
});
}
Then app(Typograph::class)->process($text) anywhere.
Wrap it, don't sprinkle it. Bind one interface (App\Contracts\Typography) with the Muravjev implementation behind it. In 2015-era code you will eventually want to swap to JS Typograf (the actively maintained successor) via a sidecar or a Node call, and an interface makes that a one-line binding change.
Apply at the right layer — pick one:
body_raw (user input, for editing/diffs) + body (typographed, for display). This is the sane default for CMS content; editors get their original text back in the form, readers get clean text.Blade usage — escape first, typograph second, or typograph a plain-text field and let Blade escape it:
{{-- body is already typographed & escaped by an accessor --}}
{!! nl2br(e($post->body_typographed)) !!}
Do not do e(typograph($html)) and do not feed raw HTML containing tags into a plain-text mode — see gotchas.
Model accessor:
protected function bodyTypographed(): Attribute
{
return Attribute::get(fn () => app(Typography::class)->process($this->body));
}
Markdown pipeline. Order matters: markdown → HTML, then typograph only text nodes, never attributes or href/src. If the engine has an HTML-aware mode, use it; otherwise run typograph on the raw source before Markdown parsing so the syntax characters aren't mangled.
Backfill existing rows with a queued job, chunked:
Post::whereNull('body_typographed')->chunkById(500, function ($posts) {
foreach ($posts as $post) {
$post->update(['body_typographed' => app(Typography::class)->process($post->body)]);
}
});
Cache the expensive part. Typograph is regex-heavy. Hash-keyed memoization is trivial and effective:
return Cache::remember('tpg:'.md5($text), now()->addDay(), fn () => $engine->process($text));
Good for product descriptions, email templates, and anything rendered on every request.
Config knobs you'll actually want: rule set / locale, "light" vs "full" mode (light for short snippets like titles and meta descriptions, full for article bodies), and whether to enable NBSP insertion (some designers hate it in narrow columns).
PHP 8 will probably fatal on the original code. A 2015 codebase very likely uses create_function, curly-brace string/array offsets, or each(). Running php -l across every file before wiring it up saves an afternoon. If you vendor it, budget an hour of mechanical fixes.
mbstring and UTF-8 are non-negotiable. Set default_charset to utf8mb4 in config/database.php (Laravel's default, but verify), and make sure your DB columns are utf8mb4. Any latin1 column or utf8 (3-byte) column will silently corrupt the «» and NBSP output.
Idempotency is not guaranteed. Running the engine twice can double-apply non-breaking spaces or re-space punctuation. Never typograph in both the save path and the render path. Write a test asserting process(process($x)) === process($x) for a few representative strings — if it fails, that's a hard constraint on where you place the call.
HTML is the biggest trap. Running the plain-text mode over markup will mangle <a href="..."> quotes, break self-closing tags, and "fix" attribute values. Options: (a) use the engine's HTML mode, (b) strip tags → typograph → reinsert (lossy, don't), or (c) typograph the source before Markdown. Choose deliberately and write a test with a link, an image, and a code block.
Escaping order. Typograph → e() → render. Reversing it means the engine sees «-style entities and produces garbage.
The rules directory path must survive composer install. Old bundles often resolve rules relative to __DIR__ inside vendor/, which works — but if you copied only the class and forgot rules/, you get silent no-ops rather than errors. Assert that at least one known transformation happens in a smoke test.
No Laravel integrations exist. No facade, no config publishing, no php artisan vendor:publish. Everything is hand-wired. Don't look for magic.
Maintenance risk is the headline. 1 star, 0 dependents, last commit 2015. Treat any adoption as taking ownership of the code, not depending on a package. Pin it in composer.json only if you fork it under your own VCS; otherwise vendor-copy. Have a documented exit path: JS Typograf (actively maintained, same rule lineage) called via a queue worker or Node sidecar is the natural successor, and your Typography interface makes that migration painless.
Testing. Assert on exact bytes: « », — (U+2014), and \u{00A0}. Use assertSame, not assertEquals on strings, and include a Cyrillic fixture set — English-only fixtures won't exercise the interesting rules at all.
How can I help you explore Laravel packages today?