Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Typograph Bundle Laravel Package

meniam/typograph-bundle

Symfony bundle that adds a Twig filter for typographing text using E. Muravjev's Typograph. Install via Composer, register the bundle, then apply {{ content | typograph }} in templates. Supports multiple named configs so different content types, like articles or news, can use their own typography se...

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. There is no composer.json extra.laravel block, no service provider, no facade, no config publish tag. composer require meniam/typograph-bundle in a Laravel app installs the code and does nothing else. Your job is to read the bundle's source and re-wire the one class that matters into Laravel's container.

Step 1 — install and look at what you got.

composer require meniam/typograph-bundle
find vendor/meniam/typograph-bundle -maxdepth 3 -type f -name '*.php' | sort

Read these three things, in this order:

  1. src/*Extension.php (the Twig extension behind {{ content | typograph }}). This is the reference implementation — it tells you the exact service/class that does the work, its constructor arguments, and how the named config is selected.
  2. DependencyInjection/Configuration.php. This is the only authoritative schema for the typograph.configs.* YAML tree shown in the README. The README truncates mid-sentence at "The...", so do not trust it.
  3. Resources/config/services.yml (or .xml). This shows the service definitions and arguments you need to reproduce by hand.

Step 2 — translate the config tree into config/typograph.php.

// config/typograph.php
return [
    'default' => [
        'Text.paragraphs'          => 'on',
        'OptAlign.oa_oquote'       => 'off',
        'OptAlign.oa_obracket_coma'=> 'off',
    ],
    'articles' => [
        'Text.paragraphs' => 'on',
    ],
    'news' => [
        'Text.paragraphs' => 'off',
    ],
];

Keep the keys verbatim from Configuration.php — they are Muravjev's rule identifiers (Text.*, OptAlign.*, Dash.*, Space.*, Quote.*), not Laravel-style snake_case, and the underlying processor expects them untouched.

Step 3 — a singleton service. Confirm the real FQCN in vendor/ rather than copying this blindly; the wrapper class name differs between the bundle and the underlying meniam/typograph library.

namespace App\Services;

use TypographBundle\Typograph; // VERIFY in vendor/ before trusting this

class TypographService
{
    /** @var array<string, array<string, string>> */
    private array $configs;

    public function __construct(array $configs)
    {
        $this->configs = $configs;
    }

    public function format(?string $text, string $profile = 'default'): string
    {
        if ($text === null || $text === '') {
            return '';
        }

        $config = $this->configs[$profile] ?? $this->configs['default'];

        return (new Typograph($config))->process($text);
    }
}
// app/Providers/TypographServiceProvider.php
public function register(): void
{
    $this->app->singleton(TypographService::class, fn ($app) =>
        new TypographService($app['config']->get('typograph', []))
    );
}

Register it in bootstrap/providers.php (Laravel 11+) or config/app.php providers (10 and below).

Step 4 — your first real use case. Almost always this: you have editor-authored or user-submitted Russian text going into a Blade view, and you want «ёлочки» instead of "", an em dash instead of a hyphen, a non-breaking space before г. / № / prepositions, and correct ellipses.

// app/helpers.php — autoload via composer.json "autoload": { "files": ["app/helpers.php"] }
function typograph(?string $text, string $profile = 'default'): string
{
    return app(\App\Services\TypographService::class)->format($text, $profile);
}
<h1>{{ typograph($post->title) }}</h1>
<div class="prose">{!! typograph($post->body, 'articles') !!}</div>

Note the escaping asymmetry: use {{ }} for headline/inline text, {!! !!} only for bodies that are already trusted HTML — and see the HTML/plain-text rule in Gotchas before you do that.

Implementation Patterns

Pattern 1 — Helper function as the primary ergonomic layer. Blade has no pipe filters, so the Twig | typograph example from the README cannot be translated literally. A global helper is the closest equivalent and the one you will actually type all day. Register through composer.json "files" autoload, composer dump-autoload, done.

Pattern 2 — Keep raw and formatted side by side. Typography is presentation, not storage. Do not mutate body on save. Either format at read time (fine for detail pages, expensive for list pages) or add an indexed body_typographed column filled by a job.

// read-time accessor, good for single-record views
public function getBodyFormattedAttribute(): string
{
    return app(TypographService::class)->format($this->body, 'articles');
}

For a list of 200 items this becomes 200 regex passes per request. Move to a column + queue once you exceed a handful of records.

Pattern 3 — Queued bulk backfill. The standard retrofit when you adopt typography after content already exists.

class TypographArticle implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public function __construct(public int $articleId) {}

    public function handle(TypographService $typograph): void
    {
        $article = Article::findOrFail($this->articleId);

        if ($article->body_typographed !== null) {
            return; // idempotent — safe to re-run
        }

        $article->update(['body_typographed' => $typograph->format($article->body, 'articles')]);
    }
}

Article::query()
    ->whereNull('body_typographed')
    ->chunkById(500, fn ($rows) => $rows->each(
        fn ($row) => TypographArticle::dispatch($row->id)
    ));

Make the job idempotent and the config profile explicit — you will re-run this after tuning rules.

Pattern 4 — Cache the formatted result, keyed by content hash. Typographing is regex-heavy; a 4 ms typographer on 20 list items is 80 ms you did not budget.

public function formattedBody(Article $article): string
{
    return Cache::remember(
        "article:{$article->id}:typographed:{$article->updated_at?->timestamp}",
        now()->addDay(),
        fn () => app(TypographService::class)->format($article->body, 'articles')
    );
}

Including updated_at (or a content hash) in the key means you never need explicit busting on saved.

Pattern 5 — Named profiles per content type. The bundle's best idea: default, articles, news, and a titles profile where you disable Text.paragraphs because wrapping a heading in <p> is nonsense.

'anchor'  => ['Text.paragraphs' => 'off'],  // single-line contexts
'articles'=> ['Text.paragraphs' => 'on', 'OptAlign.oa_oquote' => 'on'],
'email'   => ['Text.paragraphs' => 'off', 'Space.nb_nbsp' => 'on'],

Expose the profile name as a parameter on the helper rather than duplicating services.

Pattern 6 — Form request normalization, into a separate field. If typography must be stored, do it in passedValidation() and write to a different column. Overwriting body in prepareForValidation() destroys the author's straight quotes and dashes permanently — the author then cannot type a literal -- in a code sample ever again.

protected function passedValidation(): void
{
    $this->merge([
        'body_typographed' => app(TypographService::class)->format($this->input('body'), 'articles'),
    ]);
}

Pattern 7 — Resource layer, not middleware. Tempting to write a TypographResponse middleware that walks the JSON and formats every string leaf. Don't — it will happily rewrite slugs, emails, URLs, ISO dates, and hex colour codes. Format in the ArticleResource where you know which keys are prose.

public function toArray(Request $request): array
{
    return [
        'id'    => $this->id,
        'slug'  => $this->slug,                              // never typographed
        'title' => typograph($this->title, 'anchor'),
        'body'  => typograph($this->body, 'articles'),
    ];
}

Pattern 8 — Facade for IDE ergonomics. Typograph::format($text) reads better than app(...) inside services. Add @method static string format(?string $text, string $profile = 'default') on the class docblock so static analysis is satisfied.

Pattern 9 — Respect the Markdown/HTML boundary. Typograph operates on plain text. Run it before Markdown conversion, or only on the text nodes of a rich-text document. Running it over stored HTML rewrites class="..." quotes into «», mangles alt attributes, and destroys code blocks. If the editor outputs HTML, either (a) typograph the plain-text source and re-render, or (b) walk the DOM with DOMDocument, format only text nodes, and serialise back.

Gotchas and Tips

  • new TypographBundle\FenrizbesTypographBundle() will fatal in Laravel. There is no AppKernel.php, no bundle kernel, and Symfony's DI compiler pass will not run. Do not attempt to register the bundle — extract the worker class and write your own provider. Budget half a day of reading vendor/.
  • The README is truncated. The configuration section cuts off at "The..." in the source you have. DependencyInjection/Configuration.php is the only source of truth for rule keys and defaults. If a key you guessed is silently ignored, that is why.
  • Maturity is a real risk. Zero stars, zero dependents, last release 2021-12-17, and the repo reads as it-blaster/typograph-bundle renamed to meniam. Pin the exact version in composer.json, commit composer.lock, and plan to vendor a fork. Strongly consider depending on the underlying meniam/typograph library directly and skipping the Symfony bundle wrapper entirely — you are not using any of its Symfony integration anyway.
  • It pulls in Twig. The interesting code is a Twig extension, so twig/twig lands in your composer.json as a transitive dependency for a feature Blade will never use. Harmless, but it will show up in dependency audits and will confuse the next developer.
  • Language and charset are the top failure mode. This is Muravjev's Russian typographer (mdash.ru). If your content is English, you get marginal benefit and occasional oddities — non-breaking spaces in places English readers find strange. Set locale and charset explicitly; unset values default to something you did not pick.
  • Assume silent no-ops, not exceptions. Typographers of this vintage return the input unchanged on internal error. "No exception" is not "it worked". Assert on a known fixture:
public function test_typographer_actually_runs(): void
{
    $this->assertSame(
        '«Привет» — мир',
        app(TypographService::class)->format('"Привет" - мир')
    );
}

If that test passes with the input string, your config keys are wrong or your charset is misconfigured.

  • UTF-8 must hold end-to-end. MySQL charset/collation, the HTTP request, PHP's default_charset, and the typographer's configured charset all have to agree. When they don't, you get mojibake that looks like a typography bug and sends you hunting in the rule config for hours.
  • Do not typograph technical content. Code samples, CLI examples, JSON, regexes, URLs, and email addresses must be excluded. -->, -->, straight quotes, and ... inside code are correct and the typographer will "fix" them into corruption. Maintain an allowlist of prose fields — never a denylist.
  • Blade has no | filters. Every README example ({{ content | typograph }}, {{ article.content | typograph('articles') }}) is a syntax error in Blade, not a typography bug. Rewrite as typograph($content) or typograph($article->content, 'articles').
  • Caching is the difference between a fast and slow page. Typographing is regex-heavy. On a 20-item index, uncached formatting commonly dominates render time. Cache::remember on the formatted output is non-optional at any real scale.
  • There are essentially no extension points. One Twig filter and a YAML config tree. No middleware, no queue integration, no events, no caching hooks, no Artisan commands. Everything operational — backfill, cache, fallback — is yours to build. That is fine, just don't go looking for docs that don't exist.
  • Fixture-test before deploying. Capture 25–30 real strings from production content, run them through the typographer, and eyeball the diff. Non-breaking spaces are invisible in a browser and in a code review, so regressions ship silently. A dataProvider of before/after pairs committed to the repo is the cheapest insurance you have.
  • Fail open, always. Wrap the service so that any exception or unexpected output falls back to the input string. A cosmetic layer must never be the reason a page 500s.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route