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

To Inline Style Email Bundle Laravel Package

vysokeskoly/to-inline-style-email-bundle

Symfony bundle that inlines CSS styles directly into HTML email markup, helping you send styled emails that render reliably across mail clients without relying on external stylesheets.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. This is a Symfony 2-era bundle (vysokeskoly/to-inline-style-email-bundle), not a Laravel package. It has 1 star, 0 known dependents, and its last tag is from 2022 — it is a thin DI wrapper around one class: TijsVerkoyen\CssToInlineStyles\CssToInlineStyles. In a Laravel app you get nothing from the bundle wrapper (no auto-discovery, no service provider, Symfony Bundle registration in AppKernel instead of config/app.php). Treat the repo as documentation for the underlying library, not as a dependency.

Step 1 — check what you already have. The converter ships inside Laravel's own dependency tree (it powers Markdown mail theming), so it is very often already installed:

composer show tijsverkoyen/css-to-inline-styles

If it's there, skip installation entirely. If not:

composer require tijsverkoyen/css-to-inline-styles

Step 2 — first use case. Inline a Blade-rendered email before it goes on the wire:

use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;

$html    = view('mail.orders.shipped', $data)->render();
$inlined = (new CssToInlineStyles())->convert($html);

return new HtmlString($inlined);

That's the whole feature. convert() reads <style> blocks and <link rel="stylesheet"> from the document and rewrites matching elements with style="..." attributes — which is what Outlook, Gmail and friends actually respect.

For actual Symfony users (the bundle's intended audience): register the bundle in AppKernel::registerBundles() / config/bundles.php, then find the service id with bin/console debug:container | grep -i inline and autowire the converter rather than guessing the service name. Verify the bundle's composer constraint against your Symfony version before committing — the DI definition format of the Symfony 2 era does not survive Symfony 5+ unchanged.

Implementation Patterns

Global hook — do it once, in a service provider. This is the pattern worth standardising on, because it works for Mailables, Mail::raw, notifications, and anything else routed through the mailer:

// AppServiceProvider::boot()
use Illuminate\Mail\Events\MessageSending;

Event::listen(MessageSending::class, function (MessageSending $event) {
    $message = $event->message;

    if (! $message instanceof \Symfony\Component\Mime\Email) {
        return;
    }

    $html = $message->getHtmlBody();

    // getHtmlBody() can return a stream resource (attachment-only messages)
    if (! is_string($html) || trim($html) === '') {
        return;
    }

    $message->html((new CssToInlineStyles())->convert($html), $message->getHtmlCharset());
});

MessageSending is the reliable seam. Do not override Mailable::render() expecting it to affect delivery — Mailable::send() calls $mailer->send($this->buildView(), $this->buildViewData(), ...) directly, so render() is used only by previews, Mail::fake() assertions, and your own tests.

Per-mailable opt-out. Inlining costs CPU and bloats markup, so give yourself an escape hatch:

abstract class InlinedMailable extends Mailable
{
    public bool $skipInlining = false;
}

…and check it inside the listener via a marker interface or a static registry keyed by the mailable class.

Local preview route. The single highest-value debugging tool:

Route::get('/dev/mail/{mailable}', function (string $mailable) {
    abort_unless(app()->isLocal(), 404);
    return $mailable::preview();
});

Render the raw HTML and the inlined HTML side by side, then open it in a browser and in Litmus/Email on Acid. Inlining bugs are invisible until you see the DOM.

Symfony/mailer integration (bundle users). Autowire the converter into a mailer service and apply convert() in the same "before send" position — MessageEvent / MessageListener for Symfony\Component\Mailer\Event\MessageEvent.

Test it, don't trust it. Assert the inlined output in a feature test:

$html = (new OrderShipped($order))->render();

$this->assertStringContainsString('style="color:#111;', $html);
$this->assertStringNotContainsString('<style', $html); // if you strip originals

Gotchas and Tips

Don't install the bundle into Laravel. Composer will resolve it, and then nothing happens: no provider, no autoloaded service, no config. You spend a dependency for ~10 lines of glue you can write yourself.

Selector support is narrower than you think. The inliner uses a CSS selector engine that handles type/class/id/descendant/child selectors — but pseudo-classes (:hover), pseudo-elements (::before), @media, @supports, @font-face, and attribute selectors are either dropped or left in the <style> block. Media queries are inherently un-inlinable: keep them in a <head> <style> block and check that the inliner's "strip original style tags" behaviour (if you enable it) doesn't delete them. Look at the setters on the CssToInlineStyles instance (setStripOriginalStyleTags(), setExcludeMediaQueries(), setCleanup()) — names have drifted across versions, so read the class you actually installed.

Specificity collisions. Inlining produces a flat style attribute per element, so anything you leave in a <head> <style> block only wins where the inline declaration is absent. Keep one source of truth; don't define the same rule twice.

Gmail clips at ~102 KB. Inlining duplicates declarations onto every element, and a four-column layout with generous rules blows past the limit fast — the message gets truncated with "View entire message". Measure the final HTML size in CI.

Encoding. Pass the charset explicitly for non-ASCII content — convert($html, null, 'UTF-8') — otherwise emoji and accented copy come back mangled. And pass the correct charset back to $message->html(...) as shown above.

Remote <link> tags are fetched. If you ever feed user-controlled HTML through convert(), you have an SSRF vector and a slow-request vector. Strip <link> tags or run the conversion with a hard timeout on untrusted input.

Relative url() in CSS is not rewritten. The inliner does not know your asset host. Either use absolute URLs in your email CSS from the start, or post-process with a base-URL rewrite before conversion.

Watch for double-inlining. Laravel's Markdown mailables (Mail::markdown()) already inline their theme CSS through the same library. Stacking your listener on top means running the converter twice on the same document — idempotent in practice but wasteful, and it can duplicate style attributes. Skip Markdown mailables in the listener, or move to Markdown mail and delete the listener entirely.

Debugging tip: convert() fails silently — unmatched selectors just do nothing. Write the pre- and post-conversion HTML to storage/logs/ (or storage/framework/testing/) during development and diff them; that answers 90% of "why is my padding missing" questions in one look.

Extension points worth knowing: the converter is a single, stateless, cheap-to-instantiate class — wrap it in your own CssInliner service, add caching keyed on md5($html), or swap it out for pelago/emogrifier later without touching call sites. Do not build that abstraction around the Symfony bundle's service definition.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console